乐趣区

关于vue.js:vue面试常见考察点总结

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 钩子函数中调用异步申请有以下长处:

  • 能更快获取到服务端数据,缩小页面 loading 工夫;
  • ssr 不反对 beforeMount、mounted 钩子函数,所以放在 created 中有助于一致性;

Vue 中 diff 算法原理

DOM操作是十分低廉的,因而咱们须要尽量地缩小 DOM 操作。这就须要找出本次 DOM 必须更新的节点来更新,其余的不更新,这个找出的过程,就须要利用 diff 算法

vuediff 算法是平级比拟,不思考跨级比拟的状况。外部采纳 深度递归的形式 + 双指针 (头尾都加指针) 的形式进行比拟。

简略来说,Diff 算法有以下过程

  • 同级比拟,再比拟子节点(依据 keytag标签名判断)
  • 先判断一方有子节点和一方没有子节点的状况 (如果新的children 没有子节点,将旧的子节点移除)
  • 比拟都有子节点的状况(外围diff)
  • 递归比拟子节点
  • 失常 Diff 两个树的工夫复杂度是 O(n^3),但理论状况下咱们很少会进行跨层级的挪动DOM,所以VueDiff进行了优化,从 O(n^3) -> O(n),只有当新旧children 都为多个子节点时才须要用外围的 Diff 算法进行同层级比拟。
  • Vue2的外围 Diff 算法采纳了 双端比拟 的算法,同时从新旧 children 的两端开始进行比拟,借助 key 值找到可复用的节点,再进行相干操作。相比 ReactDiff算法,同样状况下能够缩小挪动节点次数,缩小不必要的性能损耗,更加的优雅
  • 在创立 VNode 时就确定其类型,以及在 mount/patch 的过程中采纳位运算来判断一个 VNode 的类型,在这个根底之上再配合外围的 Diff 算法,使得性能上较 Vue2.x 有了晋升

vue3 中采纳最长递增子序列来实现 diff 优化

答复范例

思路

  • diff算法是干什么的
  • 它的必要性
  • 它何时执行
  • 具体执行形式
  • 拔高:说一下 vue3 中的优化

答复范例

  1. Vue中的 diff 算法称为 patching 算法,它由 Snabbdo m 批改而来,虚构DOM 要想转化为实在 DOM 就须要通过 patch 办法转换
  2. 最后 Vue1.x 视图中每个依赖均有更新函数对应,能够做到精准更新,因而并不需要虚构 DOMpatching算法反对,然而这样粒度过细导致 Vue1.x 无奈承载较大利用;Vue 2.x中为了升高 Watcher 粒度,每个组件只有一个 Watcher 与之对应,此时就须要引入 patching 算法能力准确找到发生变化的中央并高效更新
  3. vuediff 执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行 render 函数取得最新的虚构 DOM,而后执行patc h 函数,并传入新旧两次虚构 DOM,通过比对两者找到变动的中央,最初将其转化为对应的DOM 操作
  4. patch过程是一个递归过程,遵循深度优先、同层比拟的策略;以 vue3patch为例
  5. 首先判断两个节点是否为雷同同类节点,不同则删除从新创立
  6. 如果单方都是文本则更新文本内容
  7. 如果单方都是元素节点则递归更新子元素,同时更新元素属性
  8. 更新子节点时又分了几种状况

    • 新的子节点是文本,老的子节点是数组则清空,并设置文本;
    • 新的子节点是文本,老的子节点是文本则间接更新文本;
    • 新的子节点是数组,老的子节点是文本则清空文本,并创立新子节点数组中的子元素;
    • 新的子节点是数组,老的子节点也是数组,那么比拟两组子节点,更新细节 blabla
  9. vue3中引入的更新策略:动态节点标记等

vdom 中 diff 算法的繁难实现

以下代码只是帮忙大家了解 diff 算法的原理和流程

  1. 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;
}
  1. 用繁难 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 形式

  • 配置路由格局:/router,也就是一般配置
  • 传递的形式:对象中应用 query 的 key 作为传递形式
  • 传递后造成的门路:/route?id=123

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)获取参数

通过 $route.query 获取传递的值

router-link 和 router-view 是如何起作用的

剖析

vue-router中两个重要组件 router-linkrouter-view,别离起到导航作用和内容渲染作用,然而答复如何失效还真有肯定难度

答复范例

  1. vue-router中两个重要组件 router-linkrouter-view,别离起到路由导航作用和组件内容渲染作用
  2. 应用中 router-link 默认生成一个 a 标签,设置 to 属性定义跳转 path。实际上也能够通过custom 和插槽自定义最终的展示模式。router-view是要显示组件的占位组件,能够嵌套,对应路由配置的嵌套关系,配合 name 能够显示具名组件,起到更强的布局作用。
  3. 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,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下

  • deep:深度监听对象,为对象的每一个属性创立一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。次要起因在于对象属于援用类型,单个属性的更新并不会触发对象 setter,因而引入 deep 可能很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,防止性能节约。
  • immediate:在初始化时间接调用回调函数,能够通过在 created 阶段手动调用回调函数实现雷同的成果

Vue 是如何实现数据双向绑定的

Vue 数据双向绑定次要是指:数据变动更新视图,视图变动更新数据,如下图所示:

  • 输入框内容变动时,Data 中的数据同步变动。即 View => Data 的变动。
  • Data 中的数据变动时,文本节点的内容同步变动。即 Data => View 的变动

Vue 次要通过以下 4 个步骤来实现数据双向绑定的

  • 实现一个监听器 Observer:对数据对象进行遍历,包含子属性对象的属性,利用 Object.defineProperty() 对属性都加上 settergetter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变动
  • 实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,而后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,增加监听数据的订阅者,一旦数据有变动,收到告诉,调用更新函数进行数据更新
  • 实现一个订阅者 WatcherWatcher 订阅者是 ObserverCompile 之间通信的桥梁,次要的工作是订阅 Observer 中的属性值变动的音讯,当收到属性值变动的音讯时,触发解析器 Compile 中对应的更新函数
  • 实现一个订阅器 Dep:订阅器采纳 公布 - 订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行对立治理

Vue 数据双向绑定原理图

双向绑定的原理是什么

咱们都晓得 Vue 是数据双向绑定的框架,双向绑定由三个重要局部形成

  • 数据层(Model):利用的数据及业务逻辑
  • 视图层(View):利用的展现成果,各类 UI 组件
  • 业务逻辑层(ViewModel):框架封装的外围,它负责将数据与视图关联起来

而下面的这个分层的架构计划,能够用一个专业术语进行称说:MVVM这里的管制层的外围性能便是“数据双向绑定”。天然,咱们只需弄懂它是什么,便能够进一步理解数据绑定的原理

了解 ViewModel

它的主要职责就是:

  • 数据变动后更新视图
  • 视图变动后更新数据

当然,它还有两个次要局部组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数

参考:前端 vue 面试题具体解答

Vue 的事件绑定原理

原生事件绑定是通过 addEventListener 绑定给实在元素的,组件事件绑定是通过 Vue 自定义的$on 实现的。如果要在组件上应用原生事件,须要加.native 修饰符,这样就相当于在父组件中把子组件当做一般 html 标签,而后加上原生事件。

$on$emit 是基于公布订阅模式的,保护一个事件核心,on 的时候将事件按名称存在事件中心里,称之为订阅者,而后 emit 将对应的事件进行公布,去执行事件中心里的对应的监听器

EventEmitter(公布订阅模式 – 简略版)

// 手写公布订阅模式 EventEmitter
class 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");

源码剖析

  1. 原生 dom 的绑定
  2. Vue 在创立真是 dom 时会调用 createElm , 默认会调用 invokeCreateHooks
  3. 会遍历以后平台下绝对的属性解决代码, 其中就有 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 元素的

  1. 组件中绑定事件
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) }

什么是递归组件?举个例子阐明下?

剖析

递归组件咱们用的比拟少,然而在 TreeMenu 这类组件中会被用到。

体验

组件通过组件名称援用它本人,这种状况就是递归组件

<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>

答复范例

  1. 如果某个组件通过组件名称援用它本人,这种状况就是递归组件。
  2. 理论开发中相似 TreeMenu 这类组件,它们的节点往往蕴含子节点,子节点构造和父节点往往是雷同的。这类组件的数据往往也是树形构造,这种都是应用递归组件的典型场景。
  3. 应用递归组件时,因为咱们并未也不能在组件外部导入它本人,所以设置组件 name 属性,用来查找组件定义,如果应用 SFC,则能够通过SFC 文件名推断。组件外部通常也要有递归完结条件,比方 model.children 这样的判断。
  4. 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给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 顺次调用 watcherupdatequeueWatcherwatcher 去重放入队列,nextTickflushSchedulerQueue)在下一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 办法 批量的进行更新 
        } 
    } 
}

子组件能够间接扭转父组件的数据么,阐明起因

这是一个实际知识点,组件化开发过程中有个 单项数据流准则,不在子组件中批改父组件是个常识问题

思路

  • 讲讲单项数据流准则,表明为何不能这么做
  • 举几个常见场景的例子说说解决方案
  • 联合实际讲讲如果须要批改父组件状态应该如何做

答复范例

  1. 所有的 prop 都使得其父子之间造成了一个单向上行绑定:父级 prop 的更新会向下流动到子组件中,然而反过来则不行。这样会避免从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以了解。另外,每次父级组件产生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件外部扭转 prop。如果你这样做了,Vue 会在浏览器控制台中收回正告
const props = defineProps(['foo'])
// ❌ 上面行为会被正告, props 是只读的!
props.foo = 'bar'
  1. 理论开发过程中有两个场景会想要批改一个属性:

这个 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())
  1. 实际中如果的确想要扭转父组件属性应该 emit 一个事件让父组件去做这个变更。留神尽管咱们不能间接批改一个传入的对象或者数组类型的prop,然而咱们还是可能间接改内嵌的对象或属性

keep-alive 应用场景和原理

  • keep-aliveVue 内置的一个组件,能够实现组件缓存 ,当组件切换时不会对以后组件进行卸载。 个别联合路由和动静组件一起应用,用于缓存组件
  • 提供 includeexclude 属性,容许组件有条件的进行缓存。两者都反对字符串或正则表达式,include 示意只有名称匹配的组件会被缓存,exclude 示意任何名称匹配的组件都不会被缓存,其中 exclude 的优先级比 include
  • 对应两个钩子函数 activateddeactivated,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated
  • keep-alive 的中还使用了 LRU(最近起码应用) 算法,抉择最近最久未应用的组件予以淘汰
  • <keep-alive></keep-alive> 包裹动静组件时,会缓存不流动的组件实例, 次要用于保留组件状态或防止从新渲染
  • 比方有一个列表和一个详情,那么用户就会常常执行关上详情 => 返回列表 => 关上详情…这样的话列表和详情都是一个频率很高的页面,那么就能够对列表组件应用 <keep-alive></keep-alive> 进行缓存,这样用户每次返回列表的时候,都能从缓存中疾速渲染,而不是从新渲染

对于 keep-alive 的根本用法

<keep-alive>
  <component :is="view"></component>
</keep-alive>

应用 includesexclude

<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 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > … … > beforeRouteLeave > deactivated
  • 再次进入组件时:beforeRouteEnter >activated > … … > beforeRouteLeave > 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:每次组件渲染的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
    next(vm=>{console.log(vm)
        // 每次进入路由执行
        vm.getData()  // 获取数据})
},
  • actived:在 keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子
// 留神:服务器端渲染期间 avtived 不被调用
activated(){this.getData() // 获取数据
},

扩大补充:LRU 算法是什么?

LRU 的核心思想是如果数据最近被拜访过,那么未来被拜访的几率也更高,所以咱们将命中缓存的组件 key 从新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据行将来被拜访几率越低,所以当缓存数量达到最大值时,咱们就删除未来被拜访几率最低的数据,即 this.keys 中第一个缓存的组件

相干代码

keep-alivevue 中内置的一个组件

源码地位: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 钩子函数中观测 includeexclude 的变动,如下:

mounted () {
  this.$watch('include', val => {pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))
  })
}

如果 includeexclude 产生了变动,即示意定义须要缓存的组件的规定或者不须要缓存的组件的规定产生了变动,那么就执行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 实现原理?

  • 虚构 DOM 实质上是 JavaScript 对象, 是对实在 DOM 的形象
  • 状态变更时,记录新树和旧树的差别
  • 最初把差别更新到真正的 dom 中

Vue 的 diff 算法详细分析

1. 是什么

diff 算法是一种通过同层的树节点进行比拟的高效算法

其有两个特点:

  • 比拟只会在同层级进行, 不会跨层级比拟
  • 在 diff 比拟的过程中,循环从两边向两头比拟

diff 算法在很多场景下都有利用,在 vue 中,作用于虚构 dom 渲染成实在 dom 的新旧 VNode 节点比拟

2. 比拟形式

diff整体策略为:深度优先,同层比拟

  1. 比拟只会在同层级进行, 不会跨层级比拟
  1. 比拟的过程中,循环从两边向两头收拢

上面举个 vue 通过 diff 算法更新的例子:

新旧 VNode 节点如下图所示:

第一次循环后,发现旧节点 D 与新节点 D 雷同,间接复用旧节点 D 作为 diff 后的第一个实在节点,同时旧节点 endIndex 挪动到 C,新节点的 startIndex 挪动到了 C

第二次循环后,同样是旧节点的开端和新节点的结尾 (都是 C) 雷同,同理,diff 后创立了 C 的实在节点插入到第一次创立的 D 节点前面。同时旧节点的 endIndex 挪动到了 B,新节点的 startIndex 挪动到了 E

第三次循环中,发现 E 没有找到,这时候只能间接创立新的实在节点 E,插入到第二次创立的 C 节点之后。同时新节点的 startIndex 挪动到了 A。旧节点的 startIndexendIndex 都放弃不动

第四次循环中,发现了新旧节点的结尾 (都是 A) 雷同,于是 diff 后创立了 A 的实在节点,插入到前一次创立的 E 节点前面。同时旧节点的 startIndex 挪动到了 B,新节点的startIndex 挪动到了 B

第五次循环中,情景同第四次循环一样,因而 diff 后创立了 B 实在节点 插入到前一次创立的 A 节点前面。同时旧节点的 startIndex挪动到了 C,新节点的 startIndex 挪动到了 F

新节点的 startIndex 曾经大于 endIndex 了,须要创立 newStartIdxnewEndIdx 之间的所有节点,也就是节点 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函数前两个参数位为oldVnodeVnode,别离代表新的节点和之前的旧节点,次要做了四个判断:

  • 没有新节点,间接触发旧节点的 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
  • 如果都不满足以上四种情景,那阐明没有雷同的节点能够复用,则会分为以下两种状况:

    • 从旧的 VNodekey 值,对应 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 组件复用导致路由参数生效怎么办?

解决办法:

  1. 通过 watch 监听路由参数再发申请
watch: { // 通过 watch 来监听路由变动
 "$route": function(){this.getData(this.$route.params.xxx);
 }
}
  1. :key 来阻止“复用”
<router-view :key="$route.fullPath" />

答复范例

  1. 很多时候,咱们须要将给定匹配模式的路由映射到同一个组件,这种状况就须要定义动静路由
  2. 例如,咱们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router中,咱们能够在门路中应用一个动静字段来实现,例如:{path: '/users/:id', component: User},其中 :id 就是门路参数
  3. 门路参数 用冒号 : 示意。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的模式裸露进去。
  4. 参数还能够有多个,例如 /users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其余有用的信息,如 $route.query$route.hash

什么是 mixin?

  • Mixin 使咱们可能为 Vue 组件编写可插拔和可重用的性能。
  • 如果心愿在多个组件之间重用一组组件选项,例如生命周期 hook、办法等,则能够将其编写为 mixin,并在组件中简略的援用它。
  • 而后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
退出移动版