vue 编译方式分为两种,
一种是离线编译 工程化 + vue-loader
一种是在线编译 runtime + compiler
vue2 的模板编译是基于正则表达式的, 所以vue 2 运行时间长会有卡顿, 正则表达式会有回溯的过程,造成性能问题
vue3 的模板编译是状态机

vue 是编译时优化,+ 静态更新
react 是动态优化+ fiber

vue2 Objetc.defineProperty 是重写对象的某一个key
Objetc.defineProperty 新增的属性都不能触发set get 可以处理数组的变化 不是不能处理数组的变化,是因为会出现频繁的get set

  1. vm._self = vm
  2. initLifecycle(vm)
  3. initEvents(vm)
  4. initRender(vm)
  5. callHook(vm, 'beforeCreate') // 所以在这个声明周期拿不到data
  6. initInjections(vm) // resolve injections before data/props
  7. initState(vm) // 初始化数据
  8. initProvide(vm)
  9. callHook(vm, 'created') // 最早能拿到data的声明周期

浏览器的渲染流程:

vue 的渲染机制采用了虚拟dom 的方式,避免直接去操作dom 首先生成虚拟dom 然后在统一进行操作dom,提升性能。
vm._render()方法将render函数转化为Virtual DOM,然后再调用vm._update() 方法将虚拟dom转化成真实dom 。

NEW 一个实例渲染到页面的过程:

new vue 实例,首先是对options 进行merage操作,然后在进行一系列的init 操作,然后在调用$mount 进行挂载,$mount触发mountComponent的执行 ,mountComponent触发watch 中的 vmupdate进行页面渲染。
vm_update 触发 vm.
patch__ vm._patch 触发patch 函数 然后渲染到真是dom 树_

  1. /**
  2. * @description:
  3. * @param {*} vnode // vnode
  4. * @param {*} hydrating //是否是服务端渲染
  5. * @return {*}
  6. * @author: liushuhao
  7. */
  8. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  9. const vm: Component = this
  10. const prevEl = vm.$el
  11. const prevVnode = vm._vnode
  12. const restoreActiveInstance = setActiveInstance(vm)
  13. vm._vnode = vnode
  14. // Vue.prototype.__patch__ is injected in entry points
  15. // based on the rendering backend used.
  16. if (!prevVnode) {
  17. // initial render
  18. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  19. }
  20. }
  1. return function patch (oldVnode, vnode, hydrating, removeOnly) {
  2. if (isUndef(vnode)) {
  3. if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
  4. return
  5. }
  6. let isInitialPatch = false
  7. const insertedVnodeQueue = []
  8. if (isUndef(oldVnode)) {
  9. // empty mount (likely as component), create new root element
  10. isInitialPatch = true
  11. createElm(vnode, insertedVnodeQueue)
  12. } else {
  13. const isRealElement = isDef(oldVnode.nodeType)
  14. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  15. // patch existing root node
  16. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  17. } else {
  18. if (isRealElement) {
  19. // mounting to a real element
  20. // check if this is server-rendered content and if we can perform
  21. // a successful hydration.
  22. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
  23. oldVnode.removeAttribute(SSR_ATTR)
  24. hydrating = true
  25. }
  26. if (isTrue(hydrating)) {
  27. if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
  28. invokeInsertHook(vnode, insertedVnodeQueue, true)
  29. return oldVnode
  30. } else if (process.env.NODE_ENV !== 'production') {
  31. warn(
  32. 'The client-side rendered virtual DOM tree is not matching ' +
  33. 'server-rendered content. This is likely caused by incorrect ' +
  34. 'HTML markup, for example nesting block-level elements inside ' +
  35. '<p>, or missing <tbody>. Bailing hydration and performing ' +
  36. 'full client-side render.'
  37. )
  38. }
  39. }
  40. // either not server-rendered, or hydration failed.
  41. // create an empty node and replace it
  42. oldVnode = emptyNodeAt(oldVnode)
  43. }
  44. // replacing existing element
  45. const oldElm = oldVnode.elm
  46. const parentElm = nodeOps.parentNode(oldElm)
  47. // create new node
  48. createElm(
  49. vnode,
  50. insertedVnodeQueue,
  51. // extremely rare edge case: do not insert if old element is in a
  52. // leaving transition. Only happens when combining transition +
  53. // keep-alive + HOCs. (#4590)
  54. oldElm._leaveCb ? null : parentElm,
  55. nodeOps.nextSibling(oldElm)
  56. )
  57. // update parent placeholder node element, recursively
  58. if (isDef(vnode.parent)) {
  59. let ancestor = vnode.parent
  60. const patchable = isPatchable(vnode)
  61. while (ancestor) {
  62. for (let i = 0; i < cbs.destroy.length; ++i) {
  63. cbs.destroy[i](ancestor)
  64. }
  65. ancestor.elm = vnode.elm
  66. if (patchable) {
  67. for (let i = 0; i < cbs.create.length; ++i) {
  68. cbs.create[i](emptyNode, ancestor)
  69. }
  70. // #6513
  71. // invoke insert hooks that may have been merged by create hooks.
  72. // e.g. for directives that uses the "inserted" hook.
  73. const insert = ancestor.data.hook.insert
  74. if (insert.merged) {
  75. // start at index 1 to avoid re-invoking component mounted hook
  76. for (let i = 1; i < insert.fns.length; i++) {
  77. insert.fns[i]()
  78. }
  79. }
  80. } else {
  81. registerRef(ancestor)
  82. }
  83. ancestor = ancestor.parent
  84. }
  85. }
  86. // destroy old node
  87. if (isDef(parentElm)) {
  88. removeVnodes([oldVnode], 0, 0)
  89. } else if (isDef(oldVnode.tag)) {
  90. invokeDestroyHook(oldVnode)
  91. }
  92. }
  93. }
  94. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  95. return vnode.elm
  96. }

然后走到createElm方法中 这个方法最后就是调用insert 方法将 生成的dom 插到dom 树中
vm._update内部执行patch实现vnode到真实dom的映射过程,在patch中通过createElm去映射,映射其实就是根据vnode调用原生的操作dom的方法去创建dom节点并挂载到页面上,然后遍历它的children,递归调用createElm创建真实的dom节点;对于组件vnode会调用createComponent。
总结:

  • options合并以及一些初始化操作
  • $mount中如果是entry-runtime-with-compiler模式且没有render就编译
  • render生成vnode
  • patch vnode到真实dom

image.png
patch的过程就是vnode映射到真实dom的过程

组件相关:

首先创建组件的vnode 然后patch 到dom树

  1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  2. let i = vnode.data
  3. if (isDef(i)) {
  4. const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
  5. if (isDef(i = i.hook) && isDef(i = i.init)) {
  6. i(vnode, false /* hydrating */)
  7. }
  8. // after calling the init hook, if the vnode is a child component
  9. // it should've created a child instance and mounted it. the child
  10. // component also has set the placeholder vnode's elm.
  11. // in that case we can just return the element and be done.
  12. if (isDef(vnode.componentInstance)) {
  13. initComponent(vnode, insertedVnodeQueue)
  14. insert(parentElm, vnode.elm, refElm)
  15. if (isTrue(isReactivated)) {
  16. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  17. }
  18. return true
  19. }
  20. }
  21. }

上面代码先获取vnode.data上的init hook,获取到了就执行init hook,然后判断vnode.componentInstance是否存在,存在就调用initComponent,然后调用insert插入到dom树实现挂载。
在_render阶段生成了组件vnode,在patch阶段执行createElm遇到了组件vnode将执行createComponent,通过在createComponent中执行组件的init hook创建组件实例,并走完组件从init-patch的阶段,然后在createComponent最后调用insert把组件的dom插入到dom树中。

异步组件:

普通异步对象: 实质是一个Promise 内部定义了resolve, reject,在resolve中通过ensureCtor获取到异步组件的构造函数,然后在sync为false的情况下执行forceRender更新视图

  1. if (isObject(res)) {
  2. if (isPromise(res)) {
  3. if (isUndef(factory.resolved)) {
  4. res.then(resolve, reject)
  5. }
  6. } else if (isPromise(res.component)) {}
  7. }

高级异步组件: 实质也是promise,判断res对象的component是否是一个Promise对象,如果是就表明这是一个高级异步组件;loader 实质settimeout

响应式对象:

核心是Object.definproperty,能代理对象和数组的变化,重新定义get,set。
也是挂载vue实例时候初始化init 在initState方法中进行挂载的,方法中进行判断,options.data是否存在,如果不存在就新建一个observe 新建一个{},如果data中有值,就initData 进行初始化, initData中调用observe 方法进行数据初始化, 实际上就是对于对象进行代理,通过watch 添加dep 进行维护关系进行响应式对象的构建,对于数组vue 对能改变数组索引的方法进行了重写,因为Object.definproperty不能处理改变数组索引的变化,所以重写了数组的方法,新增的数据也进行了响应式处理。
依赖收集是发生在触发了响应式数据的getter特性函数的时候

domdiff:

Vue.js 中 Diff算法是通过同层的树节点进行比较。

比对标签

Diff算法首先会对根节点进行比较,如果 tag 标签不一致,直接用新的 VNode 生成真实的 Dom 节点替换旧的 VNode 生成的Dom节点。

  1. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  2. // patch existing root node
  3. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  4. }

如果标签一致,有可能都是文本节点,那就比较文本的内容即可。
如果是文本,文本都没有 tag,文本内容不一致,直接修改文本即可。
如果标签一致且不是文本就要比对属性。

比对属性

比对属性核心是复用Dom节点,并且将新的VNode中的属性更新或新增到Dom节点中,并且删除多余的属性。
简单实现代码如下:

  1. // 复用标签,并且更新属性
  2. let el = vnode.el = oldVnode.el;
  3. updateProperties(vnode,oldVnode.data);
  4. function updateProperties(vnode,oldProps={}) {
  5. let newProps = vnode.data || {};
  6. let el = vnode.el;
  7. // 比对样式
  8. let newStyle = newProps.style || {};
  9. let oldStyle = oldProps.style || {};
  10. for(let key in oldStyle){
  11. if(!newStyle[key]){
  12. el.style[key] = ''
  13. }
  14. }
  15. // 删除多余属性
  16. for(let key in oldProps){
  17. if(!newProps[key]){
  18. el.removeAttribute(key);
  19. }
  20. }
  21. for (let key in newProps) {
  22. if (key === 'style') {
  23. for (let styleName in newProps.style) {
  24. el.style[styleName] = newProps.style[styleName];
  25. }
  26. } else if (key === 'class') {
  27. el.className = newProps.class;
  28. } else {
  29. el.setAttribute(key, newProps[key]);
  30. }
  31. }
  32. }

属性比对完,就需要递归比对子节点

比对子节点

比对子节点只要存在4种情况:

  • 新老 VNode 都有子节点,需要比对里面的子节点。核心是updateChildren方法
  • 新的 VNode 节点有子节点,老的 VNode 节点没有子节点,则直接将 VNode 子节点转成真实节点,插入真实的父节点即可
  • 新的 VNode 节点没有子节点,老的 VNode 节点有子节点,则真实的父节点删除子节点即可
  • 新老 VNode 都没有子节点,不需进行处理

updateChildren方法是dom-diff 的核心

  1. let oldStartIdx = 0
  2. let newStartIdx = 0
  3. let oldEndIdx = oldCh.length - 1
  4. let oldStartVnode = oldCh[0]
  5. let oldEndVnode = oldCh[oldEndIdx]
  6. let newEndIdx = newCh.length - 1
  7. let newStartVnode = newCh[0]
  8. let newEndVnode = newCh[newEndIdx]
  9. let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  10. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  11. if (isUndef(oldStartVnode)) {
  12. } else if (isUndef(oldEndVnode)) {
  13. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  14. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  15. } else if (sameVnode(oldStartVnode, newEndVnode)) {
  16. } else if (sameVnode(oldEndVnode, newStartVnode)) {
  17. } else {
  18. }
  19. }

updateChildren共分为七种条件

  1. oldStartVnode 为undefined ,oldStartIdx 递增,oldStartVnode指向下一节点。
  2. oldEndVnode 为undefined, oldEndIdx递减,oldEndVnode指向上一节点。
  3. oldStartVnode = newStartVnode ,真实dom中的相应节点会移到Vnode相应的位置,oldStartIdx 递增,newStartIdx递增,oldStartVnode指向下一节点,newStartVnode指向下一节点。
  4. oldStartVnode = newEndVnode 真实dom中的相应节点会移到Vnode相应的位置,oldStartIdx 递增,newEndIdx递减 oldStartVnode指向下一节点。newEndVnode指向上一节点。
  5. oldEndVnode和newEndVnode相似 ,真实dom中的相应节点会移到Vnode相应的位置,oldEndIdx递减,oldEndVnode指向上一节点。newEndIdx递减, newEndVnode指向上一节点。
  6. oldEndVnode = newStartVnode ,真实dom中的相应节点会移到Vnode相应的位置,newStartIdx递增,newStartVnode指向下一节点。oldEndIdx递减,oldEndVnode指向上一节点。
  7. 都不是,在情况7中在旧子节点数组中找newStartIndex指向的节点,因此就把它对应的dom节点移到insert到第一个位置,并把之前所在位置为undefined;

例子

const vm = new Vue({
    el: '#app',
  template: `<div>
        <ul>
            <li v-for="item in list" :key="itemy">{{item}}</li>
        </ul>
        <button @click="changeList">click me</button>
    </div>`,
  data() {
      return {
        list: [1, 3, 5, 4]
    }
  },
  methods: {
      changeList() {
        this.list.push(2);
      this.list.sort((a, b) => b - a);
    }
  }
});

就上面这个例子,list一开始是[1, 3, 5, 4];点击按钮执行changeList后,list变为[5, 4, 3, 2, 1];

list更新后,会进入到updateChildren的逻辑中,接下来我们通过图片来看一下patch diff的过程:

step1:
image.png
一开始oldStartIndex指向oldVnode_1,oldEndIndex指向oldVnode_4;newStartIndex指向newVnode_5,newEndIndex指向newVnode_1;第一步,因为oldStartVnode等于newEndVnode,所以会进入情况5,就变成如下:

step2:
image.png**
在情况5中,通过把newVnode_5的配置更新到oldVnode_1上,然后调用原生的insertBefore方法把dom_1插入到dom_4后面,最后oldStartIndex加1,newEndIndex减1;接着进入下一个while循环,进入情况7;

step3:
image.png
在情况7中在旧子节点数组中找newStartIndex指向的节点,这里是newVnode_5,因此就把它对应的dom节点移到insert到第一个位置,并把之前所在位置即oldVnode_5置为undefined;newStartIndex指向newVnode_4;进入下一个循环,因为此时oldEndIndex指向与newStartIndex相同,所以会进入情况6:

step4:
image.png
在情况6中,更新newVnode_4的配置到oldVnode_4,然后把oldVnode_4对应的dom节点insert到oldStartVnode对应dom节点前面,这里是dom_3的前面;接着进入下一个循坏;因为oldEndIndex为undefined,所以会进入情况2:

step5:
image.png
如上,oldEndIndex会指向oldVnode_3,下一个循环因为oldStartIndex和newStartIndex是相等的,所以会进入情况3:

step6:
image.png
在情况3中,首先更新oldVnode_3的配置并patch到dom上,然后oldStartIndex和newStartIndex递增,当这一个循环结束后,因为oldStartIndex大于oldEndIndex,所以不会进入while循环了;

接下来就进行undateChildren的最后一步:

if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

此时oldStartIdx是2,oldEndIdx是1.所以会进入第一个if逻辑,这里会把newCh[newEndIdx + 1]的elm节点赋值给refElm,这里就是newVnode_1这个节点;接着执行addVnodes:

function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
  for (; startIdx <= endIdx; ++startIdx) {
    createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
  }
}

addVnodes逻辑不复杂,就是把还在newVnodes中的却不再oldVnodes中,即新创建的节点insert到上面获取到的refElm前面,这里就是把新创建的节点dom_2添加到dom_1前面,如下:
image.png

此时页面就已经完全更新了;

从上面我们可以看到,patch diff不会修改旧vnode数组的数据顺序,只有一个置undefined的操作,更改视图是直接对比新旧vnode数组操作dom节点来实现更新;新生成的vnode数组在_update中挂载到了vm._vnode上,当再次触发更新的时候,旧vnode就是从vm._vnode上获取,与新生成的vnode做一个比较;

为什么要进行一个patch diff而不是重新挂载所有dom节点呢?
因为创建一个dom的开销是比较大的,除了创建dom,我们还有挂载在dom上的一些基本属性,还有监听事件等,当更新的地方很多时,重新创建一遍dom跟刷新浏览器重新请求资源加载无异,而对我们上面的例子,仅仅是更改了dom的顺序和新增一个dom节点,性能上来说明显比重新创建一遍dom要好得多。