Vue 的生命周期方法有哪些 一般在哪一步发请求
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有el,如果非要想与Dom进行交互,可以通过vm.nextTick 来访问 Dom
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
Vue 中 diff 算法原理
DOM操作是非常昂贵的,因此我们需要尽量地减少DOM操作。这就需要找出本次DOM必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用 diff 算法
vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)的方式进行比较。
简单来说,Diff 算法有以下过程
同级比较,再比较子节点(根据key和tag标签名判断)
先判断一方有子节点和一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
比较都有子节点的情况(核心diff)
递归比较子节点
正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅
在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升
vue3 中采用最长递增子序列来实现diff优化
回答范例
思路
diff算法是干什么的
它的必要性
它何时执行
具体执行方式
拔高:说一下vue3中的优化
回答范例
Vue中的diff算法称为patching算法,它由Snabbdom 修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换
最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新
vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch 函数,并传入新旧两次虚拟 DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作
patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3的patch为例
首先判断两个节点是否为相同同类节点,不同则删除重新创建
如果双方都是文本则更新文本内容
如果双方都是元素节点则递归更新子元素,同时更新元素属性
更新子节点时又分了几种情况
新的子节点是文本,老的子节点是数组则清空,并设置文本;
新的子节点是文本,老的子节点是文本则直接更新文本;
新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节 blabla
vue3中引入的更新策略:静态节点标记等
vdom 中 diff 算法的简易实现
以下代码只是帮助大家理解diff算法的原理和流程
将vdom转化为真实dom:
 const createElement = (vnode) => {  let tag = vnode.tag;  let attrs = vnode.attrs || {};  let children = vnode.children || [];  if(!tag) {    return null;  }  //创建元素  let elem = document.createElement(tag);  //属性  let attrName;  for (attrName in attrs) {    if(attrs.hasOwnProperty(attrName)) {      elem.setAttribute(attrName, attrs[attrName]);    }  }  //子元素  children.forEach(childVnode => {    //给elem添加子元素    elem.appendChild(createElement(childVnode));  })
  //返回真实的dom元素  return elem;}
       复制代码
 
用简易diff算法做更新操作
 function updateChildren(vnode, newVnode) {  let children = vnode.children || [];  let newChildren = newVnode.children || [];
  children.forEach((childVnode, index) => {    let newChildVNode = newChildren[index];    if(childVnode.tag === newChildVNode.tag) {      //深层次对比, 递归过程      updateChildren(childVnode, newChildVNode);    } else {      //替换      replaceNode(childVnode, newChildVNode);    }  })}
       复制代码
 
</details>
如何定义动态路由?如何获取传过来的动态参数?
(1)param 方式
配置路由格式:/router/:id
传递的方式:在 path 后面跟上对应的值
传递后形成的路径:/router/123
1)路由定义
 //在APP.vue中<router-link :to="'/user/'+userId" replace>用户</router-link>    
//在index.js{   path: '/user/:userid',   component: User,},
       复制代码
 
2)路由跳转
 // 方法1:<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
// 方法2:this.$router.push({name:'users',params:{uname:wade}})
// 方法3:this.$router.push('/user/' + wade)
       复制代码
 
3)参数获取通过 $route.params.userid 获取传递的值
(2)query 方式
1)路由定义
 //方式1:直接在router-link 标签上以对象的形式<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式<button @click='profileClick'>我的</button>    
profileClick(){  this.$router.push({    path: "/profile",    query: {        name: "kobi",        age: "28",        height: 198    }  });}
       复制代码
 
2)跳转方法
 // 方法1:<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:this.$router.push('/user?uname=' + jsmes)
       复制代码
 
3)获取参数
router-link 和 router-view 是如何起作用的
分析
vue-router中两个重要组件router-link和router-view,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度
回答范例
vue-router中两个重要组件router-link和router-view,分别起到路由导航作用和组件内容渲染作用
使用中router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom和插槽自定义最终的展现形式。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。
router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果,router-view则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。
Watch 中的 deep:true 是如何实现的
当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
 get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }
       复制代码
 Vue 组件渲染和更新过程
渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用$mount() 进行挂载。更新组件时会进行 patchVnode 流程,核心就是diff算法
watch 原理
watch 本质上是为每个监听属性 setter 创建了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep 和 immediate,对应原理如下
deep:深度监听对象,为对象的每一个属性创建一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter,因此引入 deep 能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。
immediate:在初始化时直接调用回调函数,可以通过在 created 阶段手动调用回调函数实现相同的效果
Vue 是如何实现数据双向绑定的
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
Vue 主要通过以下 4 个步骤来实现数据双向绑定的
实现一个监听器 Observer :对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
实现一个解析器 Compile :解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新
实现一个订阅者 Watcher :Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数
实现一个订阅器 Dep :订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理
Vue 数据双向绑定原理图
双向绑定的原理是什么
我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解 ViewModel
它的主要职责就是:
当然,它还有两个主要部分组成
监听器(Observer):对所有数据的属性进行监听
解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
参考:前端vue面试题详细解答
Vue 的事件绑定原理
原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的$on 实现的。如果要在组件上使用原生事件,需要加.native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件。
$on、$emit 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器
EventEmitter(发布订阅模式--简单版)
 // 手写发布订阅模式 EventEmitterclass EventEmitter {  constructor() {    this.events = {};  }  // 实现订阅  on(type, callBack) {    if (!this.events) this.events = Object.create(null);
    if (!this.events[type]) {      this.events[type] = [callBack];    } else {      this.events[type].push(callBack);    }  }  // 删除订阅  off(type, callBack) {    if (!this.events[type]) return;    this.events[type] = this.events[type].filter(item => {      return item !== callBack;    });  }  // 只执行一次订阅事件  once(type, callBack) {    function fn() {      callBack();      this.off(type, fn);    }    this.on(type, fn);  }  // 触发事件  emit(type, ...rest) {    this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));  }}
// 使用如下const event = new EventEmitter();
const handle = (...rest) => {  console.log(rest);};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => {  console.log(123456);});event.emit("dbClick");event.emit("dbClick");
       复制代码
 
源码分析
原生 dom 的绑定
Vue 在创建真是 dom 时会调用 createElm ,默认会调用 invokeCreateHooks
会遍历当前平台下相对的属性处理代码,其中就有 updateDOMListeners 方法,内部会传入 add 方法
 function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {     if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {         return     }    const on = vnode.data.on || {}     const oldOn = oldVnode.data.on || {}     target = vnode.elm normalizeEvents(on)     updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)     target = undefined }function add ( name: string, handler: Function, capture: boolean, passive: boolean ) {    target.addEventListener( // 给当前的dom添加事件         name,         handler,         supportsPassive ? { capture, passive } : capture     ) }
       复制代码
 
vue 中绑定事件是直接绑定给真实 dom 元素的
组件中绑定事件
 export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) {    target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)    target = undefined }function add (event, fn) {     target.$on(event, fn) }
       复制代码
 
组件绑定事件是通过 vue 中自定义的 $on 方法来实现的
什么是作用域插槽
插槽
创建组件虚拟节点时,会将组件儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类{a:[vnode],b[vnode]}
渲染组件时会拿对应的 slot 属性的节点进行替换操作。(插槽的作用域为父组件)
 <app>    <div slot="a">xxxx</div>    <div slot="b">xxxx</div></app> 
slot name="a" slot name="b"
       复制代码
 
作用域插槽
 // 插槽
const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(`     <my-component>         <div slot="header">node</div>         <div>react</div>         <div slot="footer">vue</div>     </my-component> `)
// with(this) { //     return _c('my-component', [_c('div', { //         attrs: { "slot": "header" },//         slot: "header" //     }, [_v("node")] // _文本及诶点 )//     , _v(" "), //     _c('div', [_v("react")]), _v(" "), _c('div', { //         attrs: { "slot": "footer" },//         slot: "footer" }, [_v("vue")])]) // }
const VueTemplateCompiler = require('vue-template-compiler');let ele = VueTemplateCompiler.compile(`     <div>        <slot name="header"></slot>         <slot name="footer"></slot>         <slot></slot>     </div> `);
with(this) {     return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "), _t("default")], 2) }//  _t定义在 core/instance/render-helpers/index.js
       复制代码
 
 // 作用域插槽:let ele = VueTemplateCompiler.compile(` <app>        <div slot-scope="msg" slot="footer">{{msg.a}}</div>     </app> `);
// with(this) { //     return _c('app', { scopedSlots: _u([{ //         // 作用域插槽的内容会被渲染成一个函数 //         key: "footer", //         fn: function (msg) { //             return _c('div', {}, [_v(_s(msg.a))]) } }]) //         })//     } // }
const VueTemplateCompiler = require('vue-template-compiler');VueTemplateCompiler.compile(` <div><slot name="footer" a="1" b="2"></slot> </div> `);
// with(this) { return _c('div', [_t("footer", null, { "a": "1", "b": "2" })], 2) }
       复制代码
 什么是递归组件?举个例子说明下?
分析
递归组件我们用的比较少,但是在Tree、Menu这类组件中会被用到。
体验
组件通过组件名称引用它自己,这种情况就是递归组件
 <template>  <li>    <div> {{ model.name }}</div>    <ul v-show="isOpen" v-if="isFolder">      <!-- 注意这里:组件递归渲染了它自己 -->      <TreeItem        class="item"        v-for="model in model.children"        :model="model">      </TreeItem>    </ul>  </li><script>export default {  name: 'TreeItem',  // ...}</script>
       复制代码
 
回答范例
如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
实际开发中类似Tree、Menu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。
查看生成渲染函数可知,递归组件查找时会传递一个布尔值给resolveComponent,这样实际获取的组件就是当前组件本身
原理
递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
 const _component_Comp = _resolveComponent("Comp", true)
       复制代码
 
就是在传递maybeSelfReference
 export function resolveComponent(  name: string,  maybeSelfReference?: boolean): ConcreteComponent | string {  return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name}
       复制代码
 
resolveAsset中最终返回的是组件自身:
 if (!res && maybeSelfReference) {    // fallback to implicit self-reference    return Component}
       复制代码
 为什么 Vue 采用异步渲染
Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick
源码相关
dep.notify() 通知 watcher进行更新, subs[i].update 依次调用 watcher 的 update , queueWatcher 将watcher 去重放入队列, nextTick( flushSchedulerQueue )在下一tick中刷新watcher队列(异步)
 update () { /* istanbul ignore else */     if (this.lazy) {         this.dirty = true     }     else if (this.sync) {         this.run()     }     else {         queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新     }}
export function queueWatcher (watcher: Watcher) {     const id = watcher.id // 会对相同的watcher进行过滤     if (has[id] == null) {         has[id] = true         if (!flushing) {             queue.push(watcher)         } else {             let i = queue.length - 1             while (i > index && queue[i].id > watcher.id) {                 i--             }            queue.splice(i + 1, 0, watcher)         }        // queue the flush         if (!waiting) {             waiting = true             if (process.env.NODE_ENV !== 'production' && !config.async) {                 flushSchedulerQueue()                 return             }            nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新         }     } }
       复制代码
 子组件可以直接改变父组件的数据么,说明原因
这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题
思路
讲讲单项数据流原则,表明为何不能这么做
举几个常见场景的例子说说解决方案
结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
所有的 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,但是我们还是能够直接改内嵌的对象或属性
keep-alive 使用场景和原理
keep-alive 是 Vue 内置的一个组件, 可以实现组件缓存 ,当组件切换时不会对当前组件进行卸载。 一般结合路由和动态组件一起使用 ,用于缓存组件
提供 include 和 exclude 属性, 允许组件有条件的进行缓存 。两者都支持字符串或正则表达式,include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高
对应两个钩子函数 activated 和deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated
keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰
<keep-alive></keep-alive> 包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染
比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive></keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染
关于 keep-alive 的基本用法
 <keep-alive>  <component :is="view"></component></keep-alive>
       复制代码
 
使用includes和exclude:
 <keep-alive include="a,b">  <component :is="view"></component></keep-alive>
<!-- 正则表达式 (使用 `v-bind`) --><keep-alive :include="/a|b/">  <component :is="view"></component></keep-alive>
<!-- 数组 (使用 `v-bind`) --><keep-alive :include="['a', 'b']">  <component :is="view"></component></keep-alive>
       复制代码
 
匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated与deactivated):
使用场景
使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
举个栗子:
当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive
从首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive
在路由中设置keepAlive属性判断是否需要缓存
 {  path: 'list',  name: 'itemList', // 列表页  component (resolve) {    require(['@/pages/item/list'], resolve) }, meta: {  keepAlive: true,  title: '列表页' }}
       复制代码
 
使用<keep-alive>
 <div id="app" class='wrapper'>    <keep-alive>        <!-- 需要缓存的视图组件 -->         <router-view v-if="$route.meta.keepAlive"></router-view>     </keep-alive>      <!-- 不需要缓存的视图组件 -->     <router-view v-if="!$route.meta.keepAlive"></router-view></div>
       复制代码
 
思考题:缓存后如何获取数据
解决方案可以有以下两种:
 beforeRouteEnter(to, from, next){    next(vm=>{        console.log(vm)        // 每次进入路由执行        vm.getData()  // 获取数据    })},
       复制代码
 
 // 注意:服务器端渲染期间avtived不被调用activated(){  this.getData() // 获取数据},
       复制代码
 
扩展补充:LRU 算法是什么?
LRU 的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key 重新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys 中第一个缓存的组件
相关代码
keep-alive是vue中内置的一个组件
源码位置:src/core/components/keep-alive.js
 export default {  name: "keep-alive",  abstract: true, //抽象组件
  props: {    include: patternTypes, //要缓存的组件    exclude: patternTypes, //要排除的组件    max: [String, Number], //最大缓存数  },
  created() {    this.cache = Object.create(null); //缓存对象  {a:vNode,b:vNode}    this.keys = []; //缓存组件的key集合 [a,b]  },
  destroyed() {    for (const key in this.cache) {      pruneCacheEntry(this.cache, key, this.keys);    }  },
  mounted() {    //动态监听include  exclude    this.$watch("include", (val) => {      pruneCache(this, (name) => matches(val, name));    });    this.$watch("exclude", (val) => {      pruneCache(this, (name) => !matches(val, name));    });  },
  render() {    const slot = this.$slots.default; //获取包裹的插槽默认值 获取默认插槽中的第一个组件节点    const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件    // 获取该组件节点的componentOptions    const componentOptions: ?VNodeComponentOptions =      vnode && vnode.componentOptions;    if (componentOptions) {      // 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag      const name: ?string = getComponentName(componentOptions);      const { include, exclude } = this;      // 不走缓存 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode      if (        // not included  不包含        (include && (!name || !matches(include, name))) ||        // excluded  排除里面        (exclude && name && matches(exclude, name))      ) {        //返回虚拟节点        return vnode;      }
      const { cache, keys } = this;      // 获取组件的key值      const key: ?string =        vnode.key == null          ? // same constructor may get registered as different local components            // so cid alone is not enough (#3269)            componentOptions.Ctor.cid +            (componentOptions.tag ? `::${componentOptions.tag}` : "")          : vnode.key;      // 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存      if (cache[key]) {        //通过key 找到缓存 获取实例        vnode.componentInstance = cache[key].componentInstance;        // make current key freshest        remove(keys, key); //通过LRU算法把数组里面的key删掉        keys.push(key); //把它放在数组末尾      } else {        cache[key] = vnode; //没找到就换存下来        keys.push(key); //把它放在数组末尾        // prune oldest entry  //如果超过最大值就把数组第0项删掉        if (this.max && keys.length > parseInt(this.max)) {          pruneCacheEntry(cache, keys[0], keys, this._vnode);        }      }
      vnode.data.keepAlive = true; //标记虚拟节点已经被缓存    }    // 返回虚拟节点    return vnode || (slot && slot[0]);  },};
       复制代码
 
可以看到该组件没有template,而是用了render,在组件渲染的时候会自动执行render函数
this.cache是一个对象,用来存储需要缓存的组件,它将以如下形式存储:
 this.cache = {  'key1':'组件1',  'key2':'组件2',  // ...}
       复制代码
 
在组件销毁的时候执行pruneCacheEntry函数
 function pruneCacheEntry (  cache: VNodeCache,  key: string,  keys: Array<string>,  current?: VNode) {  const cached = cache[key]  /* 判断当前没有处于被渲染状态的组件,将其销毁*/  if (cached && (!current || cached.tag !== current.tag)) {    cached.componentInstance.$destroy()  }  cache[key] = null  remove(keys, key)}
       复制代码
 
在mounted钩子函数中观测 include 和 exclude 的变化,如下:
 mounted () {  this.$watch('include', val => {      pruneCache(this, name => matches(val, name))  })  this.$watch('exclude', val => {      pruneCache(this, name => !matches(val, name))  })}
       复制代码
 
如果include 或exclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下
 function pruneCache (keepAliveInstance, filter) {  const { cache, keys, _vnode } = keepAliveInstance  for (const key in cache) {    const cachedNode = cache[key]    if (cachedNode) {      const name = getComponentName(cachedNode.componentOptions)      if (name && !filter(name)) {        pruneCacheEntry(cache, key, keys, _vnode)      }    }  }}
       复制代码
 
在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象剔除即可
关于keep-alive的最强大缓存功能是在render函数中实现
首先获取组件的key值:
 const key = vnode.key == null? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : ''): vnode.key
       复制代码
 
拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:
 /* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */if (cache[key]) {    vnode.componentInstance = cache[key].componentInstance    /* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */    remove(keys, key)    keys.push(key)} 
       复制代码
 
直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个
this.cache对象中没有该key值的情况,如下:
 /* 如果没有命中缓存,则将其设置进缓存 */else {    cache[key] = vnode    keys.push(key)    /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */    if (this.max && keys.length > parseInt(this.max)) {        pruneCacheEntry(cache, keys[0], keys, this._vnode)    }}
       复制代码
 
表明该组件还没有被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys中
此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉
虚拟 DOM 实现原理?
Vue 的 diff 算法详细分析
1. 是什么
diff 算法是一种通过同层的树节点进行比较的高效算法
其有两个特点:
比较只会在同层级进行, 不会跨层级比较
在 diff 比较的过程中,循环从两边向中间比较
diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较
2. 比较方式
diff整体策略为:深度优先,同层比较
比较只会在同层级进行, 不会跨层级比较
比较的过程中,循环从两边向中间收拢
下面举个vue通过diff算法更新的例子:
新旧VNode节点如下图所示:
第一次循环后,发现旧节点 D 与新节点 D 相同,直接复用旧节点 D 作为diff后的第一个真实节点,同时旧节点endIndex移动到 C,新节点的 startIndex 移动到了 C
第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E
第三次循环中,发现 E 没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不动
第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的startIndex 移动到了 B
第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex移动到了 C,新节点的 startIndex 移动到了 F
新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdx 和 newEndIdx 之间的所有节点,也就是节点 F,直接创建 F 节点对应的真实节点放到 B 节点后面
3. 原理分析
当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图
源码位置:src/core/vdom/patch.js
 function patch(oldVnode, vnode, hydrating, removeOnly) {    if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)        return    }
    let isInitialPatch = false    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {        isInitialPatch = true        createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素    } else {        const isRealElement = isDef(oldVnode.nodeType)        if (!isRealElement && sameVnode(oldVnode, vnode)) {            // 判断旧节点和新节点自身一样,一致执行patchVnode            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)        } else {            // 否则直接销毁及旧节点,根据新节点生成dom元素            if (isRealElement) {
                if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {                    oldVnode.removeAttribute(SSR_ATTR)                    hydrating = true                }                if (isTrue(hydrating)) {                    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {                        invokeInsertHook(vnode, insertedVnodeQueue, true)                        return oldVnode                    }                }                oldVnode = emptyNodeAt(oldVnode)            }            return vnode.elm        }    }}
       复制代码
 
patch函数前两个参数位为oldVnode 和 Vnode ,分别代表新的节点和之前的旧节点,主要做了四个判断:
没有新节点,直接触发旧节点的destory钩子
没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode去处理这两个节点
旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点
下面主要讲的是patchVnode部分
 function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {    // 如果新旧节点一致,什么都不做    if (oldVnode === vnode) {      return    }
    // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化    const elm = vnode.elm = oldVnode.elm
    // 异步占位符    if (isTrue(oldVnode.isAsyncPlaceholder)) {      if (isDef(vnode.asyncFactory.resolved)) {        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)      } else {        vnode.isAsyncPlaceholder = true      }      return    }    // 如果新旧都是静态节点,并且具有相同的key    // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上    // 也不用再有其他操作    if (isTrue(vnode.isStatic) &&      isTrue(oldVnode.isStatic) &&      vnode.key === oldVnode.key &&      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))    ) {      vnode.componentInstance = oldVnode.componentInstance      return    }
    let i    const data = vnode.data    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {      i(oldVnode, vnode)    }
    const oldCh = oldVnode.children    const ch = vnode.children    if (isDef(data) && isPatchable(vnode)) {      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)    }    // 如果vnode不是文本节点或者注释节点    if (isUndef(vnode.text)) {      // 并且都有子节点      if (isDef(oldCh) && isDef(ch)) {        // 并且子节点不完全一致,则调用updateChildren        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        // 如果只有新的vnode有子节点      } else if (isDef(ch)) {        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')        // elm已经引用了老的dom节点,在老的dom节点上添加子节点        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh      } else if (isDef(oldCh)) {        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        // 如果老节点是文本节点      } else if (isDef(oldVnode.text)) {        nodeOps.setTextContent(elm, '')      }
      // 如果新vnode和老vnode是文本节点或注释节点      // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以    } else if (oldVnode.text !== vnode.text) {      nodeOps.setTextContent(elm, vnode.text)    }    if (isDef(data)) {      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)    }  }
       复制代码
 
patchVnode主要做了几个判断:
新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容
新节点和旧节点如果都有子节点,则处理比较更新子节点
只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点
只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除
子节点不完全一致,则调用updateChildren
 function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {    let oldStartIdx = 0 // 旧头索引    let newStartIdx = 0 // 新头索引    let oldEndIdx = oldCh.length - 1 // 旧尾索引    let newEndIdx = newCh.length - 1 // 新尾索引    let oldStartVnode = oldCh[0] // oldVnode的第一个child    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child    let newStartVnode = newCh[0] // newVnode的第一个child    let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    // removeOnly is a special flag used only by <transition-group>    // to ensure removed elements stay in correct relative positions    // during leaving transitions    const canMove = !removeOnly
    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {      // 如果oldVnode的第一个child不存在      if (isUndef(oldStartVnode)) {        // oldStart索引右移        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      // 如果oldVnode的最后一个child不存在      } else if (isUndef(oldEndVnode)) {        // oldEnd索引左移        oldEndVnode = oldCh[--oldEndIdx]
      // oldStartVnode和newStartVnode是同一个节点      } else if (sameVnode(oldStartVnode, newStartVnode)) {        // patch oldStartVnode和newStartVnode, 索引左移,继续循环        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)        oldStartVnode = oldCh[++oldStartIdx]        newStartVnode = newCh[++newStartIdx]
      // oldEndVnode和newEndVnode是同一个节点      } else if (sameVnode(oldEndVnode, newEndVnode)) {        // patch oldEndVnode和newEndVnode,索引右移,继续循环        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)        oldEndVnode = oldCh[--oldEndIdx]        newEndVnode = newCh[--newEndIdx]
      // oldStartVnode和newEndVnode是同一个节点      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right        // patch oldStartVnode和newEndVnode        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)        // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))        // oldStart索引右移,newEnd索引左移        oldStartVnode = oldCh[++oldStartIdx]        newEndVnode = newCh[--newEndIdx]
      // 如果oldEndVnode和newStartVnode是同一个节点      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left        // patch oldEndVnode和newStartVnode        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)        // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)        // oldEnd索引左移,newStart索引右移        oldEndVnode = oldCh[--oldEndIdx]        newStartVnode = newCh[++newStartIdx]
      // 如果都不匹配      } else {        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode        idxInOld = isDef(newStartVnode.key)          ? oldKeyToIdx[newStartVnode.key]          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 如果未找到,说明newStartVnode是一个新的节点        if (isUndef(idxInOld)) { // New element          // 创建一个新Vnode          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
        // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove        } else {          vnodeToMove = oldCh[idxInOld]          /* istanbul ignore if */          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {            warn(              'It seems there are duplicate keys that is causing an update error. ' +              'Make sure each v-for item has a unique key.'            )          }
          // 比较两个具有相同的key的新节点是否是同一个节点          //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。          if (sameVnode(vnodeToMove, newStartVnode)) {            // patch vnodeToMove和newStartVnode            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)            // 清除            oldCh[idxInOld] = undefined            // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm            // 移动到oldStartVnode.elm之前            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          // 如果key相同,但是节点不相同,则创建一个新的节点          } else {            // same key but different element. treat as new element            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)          }        }
        // 右移        newStartVnode = newCh[++newStartIdx]      }    }
       复制代码
 
while循环主要处理了以下五种情景:
当新老 VNode 节点的 start 相同时,直接 patchVnode ,同时新老 VNode 节点的开始索引都加 1
当新老 VNode 节点的 end相同时,同样直接 patchVnode ,同时新老 VNode 节点的结束索引都减 1
当老 VNode 节点的 start 和新 VNode 节点的 end 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1
当老 VNode 节点的 end 和新 VNode 节点的 start 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减 1,新 VNode 节点的开始索引加 1
如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
从旧的 VNode 为 key 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode,同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面
调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置
小结
当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补丁
通过isSameVnode进行判断,相同则调用patchVnode方法
patchVnode做了以下操作:
找到对应的真实dom,称为el
如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节点
如果oldVnode有子节点而VNode没有,则删除el子节点
如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el
如果两者都有子节点,则执行updateChildren函数比较子节点
updateChildren主要做了以下操作:
设置新旧VNode的头尾指针
新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode 节点再分情况操作
v-if 和 v-show 的区别
v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
vue-router 动态路由是什么
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果
 const User = {  template: "<div>User</div>",};
const router = new VueRouter({  routes: [    // 动态路径参数 以冒号开头    { path: "/user/:id", component: User },  ],});
       复制代码
 
问题: vue-router 组件复用导致路由参数失效怎么办?
解决方法:
通过 watch 监听路由参数再发请求
 watch: { //通过watch来监听路由变化 "$route": function(){  this.getData(this.$route.params.xxx); }}
       复制代码
 
用 :key 来阻止“复用”
 <router-view :key="$route.fullPath" />
       复制代码
 
回答范例
很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由
例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数
路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。
参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query、$route.hash 等
什么是 mixin ?
Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
评论