用过 pinia 吗?有什么优点?
1. pinia 是什么?
2. 为什么要使用 pinia?
- Vue2和- Vue3都支持,这让我们同时使用- Vue2和- Vue3的小伙伴都能很快上手。
 
- pinia中只有- state、- getter、- action,抛弃了- Vuex中的- Mutation,- Vuex中- mutation一直都不太受小伙伴们的待见,- pinia直接抛弃它了,这无疑减少了我们工作量。
 
- pinia中- action支持同步和异步,- Vuex不支持
 
- 良好的- Typescript支持,毕竟我们- Vue3都推荐使用- TS来编写,这个时候使用- pinia就非常合适了
 
- 无需再创建各个模块嵌套了,- Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而- pinia中每个- store都是独立的,互相不影响。
 
- 体积非常小,只有- 1KB左右。
 
- pinia支持插件来扩展自身功能。
 
- 支持服务端渲染 
3. pinna 使用
pinna文档(opens new window)
- 准备工作 
我们这里搭建一个最新的Vue3 + TS + Vite项目
 npm create vite@latest my-vite-app --template vue-ts
   复制代码
 
- pinia基础使用
 
 // main.tsimport { createApp } from "vue";import App from "./App.vue";import { createPinia } from "pinia";const pinia = createPinia();
const app = createApp(App);app.use(pinia);app.mount("#app");
   复制代码
 
2.1 创建store
 //sbinsrc/store/user.tsimport { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 idexport const useUsersStore = defineStore('users', {  // 其它配置项})
   复制代码
 
创建store很简单,调用 pinia中的defineStore函数即可,该函数接收两个参数:
我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的
2.2 使用store
 <!-- src/App.vue --><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();console.log(store);</script>
   复制代码
 
2.3 添加state
 export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },});
   复制代码
 
2.4 读取state数据
 <template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p></template><script setup lang="ts">import { ref } from "vue";import { useUsersStore } from "../src/store/user";const store = useUsersStore();const name = ref<string>(store.name);const age = ref<number>(store.age);const sex = ref<string>(store.sex);</script>
   复制代码
 
上段代码中我们直接通过store.age等方式获取到了store存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点
 import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
   复制代码
 
2.5 修改state数据
 <template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p>  <button @click="changeName">更改姓名</button></template><script setup lang="ts">import child from './child.vue';import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store);const changeName = () => {  store.name = "张三";  console.log(store);};</script>
   复制代码
 
2.6 重置state
 <button @click="reset">重置store</button>// 重置storeconst reset = () => {  store.$reset();};
   复制代码
 
当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新
2.7 批量更改state数据
如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store的$patch方法,修改app.vue代码,添加一个批量更改数据的方法
 <button @click="patchStore">批量修改数据</button>// 批量修改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};
   复制代码
 
 store.$patch((state) => {  state.items.push({ name: 'shoes', quantity: 1 })  state.hasChanged = true})
   复制代码
 
2.8 直接替换整个state
pinia提供了方法让我们直接替换整个state对象,使用store的$state方法
 store.$state = { counter: 666, name: '张三' }
   复制代码
 
上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少
- getters属性
 
3.1 添加getter
 export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 10,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },  },})
   复制代码
 
上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个getAddAge方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据
3.2 使用getter
 <template>  <p>新年龄:{{ store.getAddAge }}</p>  <button @click="patchStore">批量修改数据</button></template><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();// 批量修改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};</script>
   复制代码
 
上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式
3.3 getter中调用其它getter
 export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
   复制代码
 
3.3 getter传参
 export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
   复制代码
 
 <p>新年龄:{{ store.getAddAge(1100) }}</p>
   复制代码
 
- actions属性
 
- 前面我们提到的- state和- getters 属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的- data数据和- computed计算属性一样。
 
- 那么,如果我们有业务代码的话,最好就是卸载- actions属性里面,该属性就和我们组件代码中的- methods相似,用来放置一些处理业务逻辑的方法。
 
- actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法
 
4.1 添加actions
 export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },  actions: {    // 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store    saveName(name: string) {      this.name = name;    },  },});
   复制代码
 
4.2 使用actions
使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法
 const saveName = () => {  store.saveName("poetries");};
   复制代码
 
总结
pinia的知识点很少,如果你有 Vuex 基础,那么学起来更是易如反掌
pinia 无非就是以下 3 个大点:
computed 和 watch 区别
- 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性- computed
 
 
Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理
 <template>{{fullName}}</template>export default {    data(){        return {            firstName: 'zhang',            lastName: 'san',        }    },    computed:{        fullName: function(){            return this.firstName + ' ' + this.lastName        }    }}
   复制代码
 
- watch用于观察和监听页面上的 vue 实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么- watch为最佳选择
 
Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销
 <template>{{fullName}}</template>export default {    data(){        return {            firstName: 'zhang',            lastName: 'san',            fullName: 'zhang san'        }    },    watch:{        firstName(val) {            this.fullName = val + ' ' + this.lastName        },        lastName(val) {            this.fullName = this.firstName + ' ' + val        }    }}
   复制代码
 
computed:
- computed是计算属性,也就是计算值,它更多用于计算值的场景
 
- computed具有缓存性,- computed的值在- getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取- computed的值时才会重新调用对应的- getter来计算
 
- computed适用于计算比较消耗性能的计算场景
 
watch:
小结:
- computed和- watch都是基于- watcher来实现的
 
- computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行
 
- watch是监控值的变化,当值发生变化时调用其对应的回调函数
 
- 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为- computed
 
 
- 如果你需要在某个数据变化时做一些事情,使用- watch来观察这个数据变化
 
回答范例
思路分析
computed特点:具有响应式的返回值
 const count = ref(1)const plusOne = computed(() => count.value + 1)
   复制代码
 
watch特点:侦测变化,执行回调
 const state = reactive({ count: 0 })watch(  () => state.count,  (count, prevCount) => {    /* ... */  })
   复制代码
 
回答范例
- 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,- computed和- methods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch 没有返回值,但可以执行异步操作等复杂逻辑
 
- 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的 DOM 操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性. 
- 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。- watch可以传递对象,设置- deep、- immediate等选项
 
- vue3中- watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式;- reactivity API中新出现了- watch、- watchEffect可以完全替代目前的- watch选项,且功能更加强大
 
基本使用
 // src/core/observer:45;
// 渲染watcher  /  computed watcher  /  watchconst vm = new Vue({    el: '#app',    data: {        firstname:'张',        lastname:'三'    },    computed:{ // watcher  =>   firstname lastname        // computed 只有取值时才执行
        // Object.defineProperty .get        fullName(){ // firstName lastName 会收集fullName计算属性            return this.firstname + this.lastname        }    },    watch:{        firstname(newVal,oldVal){            console.log(newVal)        }    }});
setTimeout(() => {    debugger;    vm.firstname = '赵'}, 1000);
   复制代码
 
相关源码
 // 初始化statefunction initState (vm: Component) {  vm._watchers = []  const opts = vm.$options  if (opts.props) initProps(vm, opts.props)  if (opts.methods) initMethods(vm, opts.methods)  if (opts.data) {    initData(vm)  } else {    observe(vm._data = {}, true /* asRootData */)  }
  // 初始化计算属性  if (opts.computed) initComputed(vm, opts.computed) 
  // 初始化watch  if (opts.watch && opts.watch !== nativeWatch) {     initWatch(vm, opts.watch)  }}
// 计算属性取值函数function createComputedGetter (key) {  return function computedGetter () {    const watcher = this._computedWatchers && this._computedWatchers[key]    if (watcher) {      if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值        watcher.evaluate(); // this.firstname lastname      }      if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher        watcher.depend()      }      return watcher.value    }  }}
// watch的实现Vue.prototype.$watch = function (    expOrFn: string | Function,    cb: any,    options?: Object  ): Function {    const vm: Component = this    debugger;    if (isPlainObject(cb)) {      return createWatcher(vm, expOrFn, cb, options)    }    options = options || {}    options.user = true    const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb    if (options.immediate) {      try {        cb.call(vm, watcher.value)      } catch (error) {        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)      }    }    return function unwatchFn () {      watcher.teardown()    }}
   复制代码
 
Vue 修饰符有哪些
事件修饰符
- .stop 阻止事件继续传播 
- .prevent 阻止标签默认行为 
- .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 
- .self 只当在 event.target 是当前元素自身时触发处理函数 
- .once 事件将只会触发一次 
- .passive 告诉浏览器你不想阻止事件的默认行为 
v-model 的修饰符
键盘事件的修饰符
- .enter 
- .tab 
- .delete (捕获“删除”和“退格”键) 
- .esc 
- .space 
- .up 
- .down 
- .left 
- .right 
系统修饰键
鼠标按钮修饰符
Vue 模板编译原理
Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步
 第一步是将 模板字符串 转换成 element ASTs(解析器)第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
   复制代码
 v-model 可以被用在自定义组件上吗?如果可以,如何使用?
可以。v-model 实际上是一个语法糖,如:
 <input v-model="searchText">
   复制代码
 
实际上相当于:
 <input  v-bind:value="searchText"  v-on:input="searchText = $event.target.value">
   复制代码
 
用在自定义组件上也是同理:
 <custom-input v-model="searchText">
   复制代码
 
相当于:
 <custom-input  v-bind:value="searchText"  v-on:input="searchText = $event"></custom-input>
   复制代码
 
显然,custom-input 与父组件的交互如下:
- 父组件将- searchText变量传入 custom-input 组件,使用的 prop 名为- value;
 
- custom-input 组件向父组件传出名为- input的事件,父组件将接收到的值赋值给- searchText;
 
所以,custom-input 组件的实现应该类似于这样:
 Vue.component('custom-input', {  props: ['value'],  template: `    <input      v-bind:value="value"      v-on:input="$emit('input', $event.target.value)"    >  `})
   复制代码
 谈谈对 keep-alive 的了解
keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性 include/exclude ,2个生命周期 activated , deactivated
参考 前端进阶面试题详细解答
extend 有什么作用
这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。
 // 创建组件构造器let Component = Vue.extend({ template: "<div>test</div>" });// 挂载到 #app 上new Component().$mount('#app')// 除了上面的方式,还可以用来扩展已有的组件let SuperComponent = Vue.extend(Component);new SuperComponent({  created() {    console.log(1);  },});new SuperComponent().$mount("#app");
   复制代码
 action 与 mutation 的区别
如何保存页面的当前的状态
既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:
那么可以按照这两种情况分别得到以下方法:
组件会被卸载:
(1)将状态存储在 LocalStorage / SessionStorage
只需要在组件即将被销毁的生命周期 componentWillUnmount (react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。
比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。
优点:
- 兼容性好,不需要额外库或工具。 
- 简单快捷,基本可以满足大部分需求。 
缺点:
(2)路由传值
通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。
在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。
优点:
缺点:
组件不会被卸载:
(1)单页面渲染
要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。
优点:
缺点:
- 增加 A 组件维护成本 
- 需要传入额外的 prop 到 B 组件 
- 无法利用路由定位页面 
除此之外,在 Vue 中,还可以是用 keep-alive 来缓存页面,当组件在 keep-alive 内被切换时组件的 activated、deactivated 这两个生命周期钩子函数会被执行被包裹在 keep-alive 中的组件的状态将会被保留:
 <keep-alive>    <router-view v-if="$route.meta.keepAlive"></router-view></kepp-alive>
   复制代码
 
router.js
 {  path: '/',  name: 'xxx',  component: ()=>import('../src/views/xxx.vue'),  meta:{    keepAlive: true // 需要被缓存  }},
   复制代码
 什么是 mixin ?
- Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。 
- 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。 
- 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。 
vue2.x 详细
1. 分析
首先找到vue的构造函数
源码位置:src\core\instance\index.js
 function Vue (options) {  if (process.env.NODE_ENV !== 'production' &&    !(this instanceof Vue)  ) {    warn('Vue is a constructor and should be called with the `new` keyword')  }  this._init(options)}
   复制代码
 
options是用户传递过来的配置项,如data、methods等常用的方法
vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法
 initMixin(Vue);     // 定义 _initstateMixin(Vue);    // 定义 $set $get $delete $watch 等eventsMixin(Vue);   // 定义事件  $on  $once $off $emitlifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroyrenderMixin(Vue);   // 定义 _render 返回虚拟dom
   复制代码
 
首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法
源码位置:src\core\instance\init.js
 Vue.prototype._init = function (options?: Object) {    const vm: Component = this    // a uid    vm._uid = uid++    let startTag, endTag    /* istanbul ignore if */    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {      startTag = `vue-perf-start:${vm._uid}`      endTag = `vue-perf-end:${vm._uid}`      mark(startTag)    }
    // a flag to avoid this being observed    vm._isVue = true    // merge options    // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法    if (options && options._isComponent) {      // optimize internal component instantiation      // since dynamic options merging is pretty slow, and none of the      // internal component options needs special treatment.      initInternalComponent(vm, options)    } else { // 合并vue属性      vm.$options = mergeOptions(        resolveConstructorOptions(vm.constructor),        options || {},        vm      )    }    /* istanbul ignore else */    if (process.env.NODE_ENV !== 'production') {      // 初始化proxy拦截器      initProxy(vm)    } else {      vm._renderProxy = vm    }    // expose real self    vm._self = vm    // 初始化组件生命周期标志位    initLifecycle(vm)    // 初始化组件事件侦听    initEvents(vm)    // 初始化渲染方法    initRender(vm)    callHook(vm, 'beforeCreate')    // 初始化依赖注入内容,在初始化data、props之前    initInjections(vm) // resolve injections before data/props    // 初始化props/data/method/watch/methods    initState(vm)    initProvide(vm) // resolve provide after data/props    callHook(vm, 'created')
    /* istanbul ignore if */    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {      vm._name = formatComponentName(vm, false)      mark(endTag)      measure(`vue ${vm._name} init`, startTag, endTag)    }    // 挂载元素    if (vm.$options.el) {      vm.$mount(vm.$options.el)    }  }
   复制代码
 
仔细阅读上面的代码,我们得到以下结论:
initState方法是完成props/data/method/watch/methods的初始化
源码位置:src\core\instance\state.js
 export function initState (vm: Component) {  // 初始化组件的watcher列表  vm._watchers = []  const opts = vm.$options  // 初始化props  if (opts.props) initProps(vm, opts.props)  // 初始化methods方法  if (opts.methods) initMethods(vm, opts.methods)  if (opts.data) {    // 初始化data      initData(vm)  } else {    observe(vm._data = {}, true /* asRootData */)  }  if (opts.computed) initComputed(vm, opts.computed)  if (opts.watch && opts.watch !== nativeWatch) {    initWatch(vm, opts.watch)  }}
   复制代码
 
我们和这里主要看初始化data的方法为initData,它与initState在同一文件上
 function initData (vm: Component) {  let data = vm.$options.data  // 获取到组件上的data  data = vm._data = typeof data === 'function'    ? getData(data, vm)    : data || {}  if (!isPlainObject(data)) {    data = {}    process.env.NODE_ENV !== 'production' && warn(      'data functions should return an object:\n' +      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',      vm    )  }  // proxy data on instance  const keys = Object.keys(data)  const props = vm.$options.props  const methods = vm.$options.methods  let i = keys.length  while (i--) {    const key = keys[i]    if (process.env.NODE_ENV !== 'production') {      // 属性名不能与方法名重复      if (methods && hasOwn(methods, key)) {        warn(          `Method "${key}" has already been defined as a data property.`,          vm        )      }    }    // 属性名不能与state名称重复    if (props && hasOwn(props, key)) {      process.env.NODE_ENV !== 'production' && warn(        `The data property "${key}" is already declared as a prop. ` +        `Use prop default value instead.`,        vm      )    } else if (!isReserved(key)) { // 验证key值的合法性      // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据      proxy(vm, `_data`, key)    }  }  // observe data  // 响应式监听data是数据的变化  observe(data, true /* asRootData */)}
   复制代码
 
仔细阅读上面的代码,我们可以得到以下结论:
关于数据响应式在这就不展开详细说明
上文提到挂载方法是调用vm.$mount方法
源码位置:
 Vue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  // 获取或查询元素  el = el && query(el)
  /* istanbul ignore if */  // vue 不允许直接挂载到body或页面文档上  if (el === document.body || el === document.documentElement) {    process.env.NODE_ENV !== 'production' && warn(      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`    )    return this  }
  const options = this.$options  // resolve template/el and convert to render function  if (!options.render) {    let template = options.template    // 存在template模板,解析vue模板文件    if (template) {      if (typeof template === 'string') {        if (template.charAt(0) === '#') {          template = idToTemplate(template)          /* istanbul ignore if */          if (process.env.NODE_ENV !== 'production' && !template) {            warn(              `Template element not found or is empty: ${options.template}`,              this            )          }        }      } else if (template.nodeType) {        template = template.innerHTML      } else {        if (process.env.NODE_ENV !== 'production') {          warn('invalid template option:' + template, this)        }        return this      }    } else if (el) {      // 通过选择器获取元素内容      template = getOuterHTML(el)    }    if (template) {      /* istanbul ignore if */      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {        mark('compile')      }      /**       *  1.将temmplate解析ast tree       *  2.将ast tree转换成render语法字符串       *  3.生成render方法       */      const { render, staticRenderFns } = compileToFunctions(template, {        outputSourceRange: process.env.NODE_ENV !== 'production',        shouldDecodeNewlines,        shouldDecodeNewlinesForHref,        delimiters: options.delimiters,        comments: options.comments      }, this)      options.render = render      options.staticRenderFns = staticRenderFns
      /* istanbul ignore if */      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {        mark('compile end')        measure(`vue ${this._name} compile`, 'compile', 'compile end')      }    }  }  return mount.call(this, el, hydrating)}
   复制代码
 
阅读上面代码,我们能得到以下结论:
对template的解析步骤大致分为以下几步:
- 将- html文档片段解析成- ast描述符
 
- 将- ast描述符解析成字符串
 
- 生成- render函数
 
生成render函数,挂载到vm上后,会再次调用mount方法
源码位置:src\platforms\web\runtime\index.js
 // public mount methodVue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && inBrowser ? query(el) : undefined  // 渲染组件  return mountComponent(this, el, hydrating)}
   复制代码
 
调用mountComponent渲染组件
 export function mountComponent (  vm: Component,  el: ?Element,  hydrating?: boolean): Component {  vm.$el = el  // 如果没有获取解析的render函数,则会抛出警告  // render是解析模板文件生成的  if (!vm.$options.render) {    vm.$options.render = createEmptyVNode    if (process.env.NODE_ENV !== 'production') {      /* istanbul ignore if */      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||        vm.$options.el || el) {        warn(          'You are using the runtime-only build of Vue where the template ' +          'compiler is not available. Either pre-compile the templates into ' +          'render functions, or use the compiler-included build.',          vm        )      } else {        // 没有获取到vue的模板文件        warn(          'Failed to mount component: template or render function not defined.',          vm        )      }    }  }  // 执行beforeMount钩子  callHook(vm, 'beforeMount')
  let updateComponent  /* istanbul ignore if */  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {    updateComponent = () => {      const name = vm._name      const id = vm._uid      const startTag = `vue-perf-start:${id}`      const endTag = `vue-perf-end:${id}`
      mark(startTag)      const vnode = vm._render()      mark(endTag)      measure(`vue ${name} render`, startTag, endTag)
      mark(startTag)      vm._update(vnode, hydrating)      mark(endTag)      measure(`vue ${name} patch`, startTag, endTag)    }  } else {    // 定义更新函数    updateComponent = () => {      // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render      vm._update(vm._render(), hydrating)    }  }  // we set this to vm._watcher inside the watcher's constructor  // since the watcher's initial patch may call $forceUpdate (e.g. inside child  // component's mounted hook), which relies on vm._watcher being already defined  // 监听当前组件状态,当有数据变化时,更新组件  new Watcher(vm, updateComponent, noop, {    before () {      if (vm._isMounted && !vm._isDestroyed) {        // 数据更新引发的组件更新        callHook(vm, 'beforeUpdate')      }    }  }, true /* isRenderWatcher */)  hydrating = false
  // manually mounted instance, call mounted on self  // mounted is called for render-created child components in its inserted hook  if (vm.$vnode == null) {    vm._isMounted = true    callHook(vm, 'mounted')  }  return vm}
   复制代码
 
阅读上面代码,我们得到以下结论:
updateComponent方法主要执行在vue初始化时声明的render,update方法
render的作用主要是生成vnode
源码位置:src\core\instance\render.js
 // 定义vue 原型上的render方法Vue.prototype._render = function (): VNode {    const vm: Component = this    // render函数来自于组件的option    const { render, _parentVnode } = vm.$options
    if (_parentVnode) {        vm.$scopedSlots = normalizeScopedSlots(            _parentVnode.data.scopedSlots,            vm.$slots,            vm.$scopedSlots        )    }
    // set parent vnode. this allows render functions to have access    // to the data on the placeholder node.    vm.$vnode = _parentVnode    // render self    let vnode    try {        // There's no need to maintain a stack because all render fns are called        // separately from one another. Nested component's render fns are called        // when parent component is patched.        currentRenderingInstance = vm        // 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode        vnode = render.call(vm._renderProxy, vm.$createElement)    } catch (e) {        handleError(e, vm, `render`)        // return error render result,        // or previous vnode to prevent render error causing blank component        /* istanbul ignore else */        if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {            try {                vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)            } catch (e) {                handleError(e, vm, `renderError`)                vnode = vm._vnode            }        } else {            vnode = vm._vnode        }    } finally {        currentRenderingInstance = null    }    // if the returned array contains only a single node, allow it    if (Array.isArray(vnode) && vnode.length === 1) {        vnode = vnode[0]    }    // return empty vnode in case the render function errored out    if (!(vnode instanceof VNode)) {        if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {            warn(                'Multiple root nodes returned from render function. Render function ' +                'should return a single root node.',                vm            )        }        vnode = createEmptyVNode()    }    // set parent    vnode.parent = _parentVnode    return vnode}
   复制代码
 
_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中
源码位置:src\core\instance\lifecycle.js
 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {    const vm: Component = this    const prevEl = vm.$el    const prevVnode = vm._vnode    // 设置当前激活的作用域    const restoreActiveInstance = setActiveInstance(vm)    vm._vnode = vnode    // Vue.prototype.__patch__ is injected in entry points    // based on the rendering backend used.    if (!prevVnode) {      // initial render      // 执行具体的挂载逻辑      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)    } else {      // updates      vm.$el = vm.__patch__(prevVnode, vnode)    }    restoreActiveInstance()    // update __vue__ reference    if (prevEl) {      prevEl.__vue__ = null    }    if (vm.$el) {      vm.$el.__vue__ = vm    }    // if parent is an HOC, update its $el as well    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {      vm.$parent.$el = vm.$el    }    // updated hook is called by the scheduler to ensure that children are    // updated in a parent's updated hook.  }
   复制代码
 
2. 结论
- new Vue的时候调用会调用- _init方法
 
- 定义 - $set、- $get、- $delete、- $watch等方法
 
- 定义 - $on、- $off、- $emit、- $off等事件
 
- 定义 - _update、- $forceUpdate、- $destroy生命周期
 
- 调用- $mount进行页面的挂载
 
- 挂载的时候主要是通过- mountComponent方法
 
- 定义- updateComponent更新函数
 
- 执行- render生成虚拟- DOM
 
 
- _update将虚拟- DOM生成真实- DOM结构,并且渲染到页面中
 
Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。
Vue3.0 为什么要用 proxy?
在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶
- 不需用使用 - Vue.$set或- Vue.$delete触发响应式。
 
- 全方位的数组变化检测,消除了 Vue2 无效的边界情况。 
- 支持 Map,Set,WeakMap 和 WeakSet。 
Proxy 实现的响应式原理与 Vue2 的实现原理相同,实现方式大同小异∶
如果让你从零开始写一个 vue 路由,说说你的思路
思路分析:
首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。
- 借助- hash或者 h- istory api实现- url跳转页面不刷新
 
- 同时监听- hashchange事件或者- popstate事件处理跳转
 
- 根据- hash值或者- state值从- routes表中匹配对应- component并渲染
 
回答范例:
一个SPA应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:
- 首先我会定义一个- createRouter函数,返回路由器实例,实例内部做几件事
 
- 保存用户传入的配置项 
- 监听- hash或者- popstate事件
 
- 回调里根据- path匹配对应路由
 
- 将- router定义成一个- Vue插件,即实现- install方法,内部做两件事
 
Vue 的性能优化有哪些
(1)编码阶段
- 尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher 
- v-if 和 v-for 不能连用 
- 如果需要使用 v-for 给每项元素绑定事件时使用事件代理 
- SPA 页面采用 keep-alive 缓存组件 
- 在更多的情况下,使用 v-if 替代 v-show 
- key 保证唯一 
- 使用路由懒加载、异步组件 
- 防抖、节流 
- 第三方模块按需导入 
- 长列表滚动到可视区域动态加载 
- 图片懒加载 
(2)SEO 优化
(3)打包优化
(4)用户体验
v-if 和 v-show 的区别
- 手段:v-if 是动态的向 DOM 树内添加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 样式属性控制显隐; 
- 编译过程:v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换; 
- 编译条件:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且 DOM 元素保留; 
- 性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗; 
- 使用场景:v-if 适合运营条件不大可能改变;v-show 适合频繁切换。 
v-if、v-show、v-html 的原理
- v-if 会调用 addIfCondition 方法,生成 vnode 的时候会忽略对应节点,render 的时候就不会渲染; 
- v-show 会生成 vnode,render 的时候也会渲染成真实节点,只是在 render 过程中会在节点的属性中修改 show 属性值,也就是常说的 display; 
- v-html 会先移除节点下的所有节点,调用 html 方法,通过 addProp 添加 innerHTML 属性,归根结底还是设置 innerHTML 为 v-html 的值。 
谈谈你对 SPA 单页面的理解
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载
优点:
缺点:
- 初次加载耗时多:为实现单页 - Web应用功能及显示效果,需要在加载页面的时候将- JavaScript、- CSS统一加载,部分页面按需加载;
 
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理; 
- SEO难度较大:由于所有的内容都在一个页面中动态替换显示,所以在- SEO上其有着天然的弱势
 
单页应用与多页应用的区别
实现一个 SPA
- hash 模式 :核心通过监听- url中的- hash来进行路由跳转
 
 // 定义 Router  class Router {      constructor () {          this.routes = {}; // 存放路由path及callback          this.currentUrl = '';  
        // 监听路由change调用相对应的路由回调          window.addEventListener('load', this.refresh, false);          window.addEventListener('hashchange', this.refresh, false);      }  
    route(path, callback){          this.routes[path] = callback;      }  
    push(path) {          this.routes[path] && this.routes[path]()      }  }  
// 使用 router  window.miniRouter = new Router();  miniRouter.route('/', () => console.log('page1'))  miniRouter.route('/page2', () => console.log('page2'))  
miniRouter.push('/') // page1  miniRouter.push('/page2') // page2  
   复制代码
 
- history 模式 :- history模式核心借用- HTML5 history api,- api提供了丰富的- router相关属性先了解一个几个相关的 api
 
- history.pushState浏览器历史纪录添加记录
 
- history.replaceState修改浏览器历史纪录中当前纪录
 
- history.popState当- history发生变化时触发
 
 // 定义 Router  class Router {      constructor () {          this.routes = {};          this.listerPopState()      }  
    init(path) {          history.replaceState({path: path}, null, path);          this.routes[path] && this.routes[path]();      }  
    route(path, callback){          this.routes[path] = callback;      }  
    push(path) {          history.pushState({path: path}, null, path);          this.routes[path] && this.routes[path]();      }  
    listerPopState () {          window.addEventListener('popstate' , e => {              const path = e.state && e.state.path;              this.routers[path] && this.routers[path]()          })      }  }  
// 使用 Router  
window.miniRouter = new Router();  miniRouter.route('/', ()=> console.log('page1'))  miniRouter.route('/page2', ()=> console.log('page2'))  
// 跳转  miniRouter.push('/page2')  // page2  
   复制代码
 
题外话:如何给 SPA 做 SEO
- SSR 服务端渲染 
将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js
- 静态化 
目前主流的静态化主要有两种:
- 使用- Phantomjs针对爬虫处理
 
原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图
子组件可以直接改变父组件的数据么,说明原因
这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题
思路
- 讲讲单项数据流原则,表明为何不能这么做 
- 举几个常见场景的例子说说解决方案 
- 结合实践讲讲如果需要修改父组件状态应该如何做 
回答范例
- 所有的 - prop都使得其父子之间形成了一个单向下行绑定:父级- prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的- prop都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变- prop。如果你这样做了,- Vue会在浏览器控制台中发出警告
 
 const props = defineProps(['foo'])// ❌ 下面行为会被警告, props是只读的!props.foo = 'bar'
   复制代码
 
- 实际开发过程中有两个场景会想要修改一个属性: 
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:
 const props = defineProps(['initialCounter'])const counter = ref(props.initialCounter)
   复制代码
 
这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
 const props = defineProps(['size'])// prop变化,计算属性自动更新const normalizedSize = computed(() => props.size.trim().toLowerCase())
   复制代码
 
- 实践中如果确实想要改变父组件属性应该- emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的- prop,但是我们还是能够直接改内嵌的对象或属性
 
Vue 路由 hash 模式和 history 模式
1. hash模式
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search'
 https://interview2.poetries.top#search
   复制代码
 
hash 路由模式的实现主要是基于下面几个特性
- URL中- hash值只是客户端的一种状态,也就是说当向服务器端发出请求时,- hash部分不会被发送;
 
- hash值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制- hash的切换;
 
- 可以通过 - a标签,并设置- href属性,当用户点击这个标签后,- URL的- hash值会发生改变;或者使用- JavaScript来对- loaction.hash进行赋值,改变- URL的- hash值;
 
- 我们可以使用 - hashchange事件来监听- hash值的变化,从而对页面进行跳转(渲染)
 
 window.addEventListener("hashchange", funcRef, false);
   复制代码
 
每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点 :兼容性好但是不美观
2. history模式
history采用HTML5的新特性;且提供了两个新方法: pushState(), replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更
 window.history.pushState(null, null, path);window.history.replaceState(null, null, path);
   复制代码
 
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
history 路由模式的实现主要基于存在下面几个特性:
- pushState和- repalceState两个- API来操作实现- URL的变化 ;
 
- 我们可以使用 - popstate事件来监听- url的变化,从而对页面进行跳转(渲染);
 
- history.pushState()或- history.replaceState()不会触发- popstate事件,这时我们需要手动触发页面跳转(渲染)。
 
特点 :虽然美观,但是刷新会出现 404 需要后端进行配置
评论