Vue2.x中的虚拟DOM和Diff算法

568 阅读4分钟

目标

掌握虚拟DOM和Diff算法

知识点

  1. 虚拟dom,可以借鉴snabbdom
  2. patch对比。

Vue 虚拟dom

虚拟dom.jpeg

  • 虚拟dom(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象。能够描述DOM结构和关系。应用各种状态变化会作用于虚拟DOM,最终映射到DOM上。

  • Vue中虚拟dom基于snabbdom实现的。

  • 虚拟DOM轻量,快速:当它们发生变化时通过新旧虚拟DOM对比可以得到最小的DOM操作量,配合异步更新策略减少刷新频率,从而提升性能。·

    patch(vnode, h('div', obj.foo))
    
  • 跨平台;将虚拟DOM更新转换不同运行时特殊操作实现跨平台。

  • 兼容性:可以加入兼容代码增强操作的兼容性。

  • 必要性:vue1.x版本有细粒度的数据变化侦测,它是不需要虚拟DOM的。但是细粒度灰造成大量的开销,这对于大型项目来说是不可接受的。因此Vue2.x版本引进了虚拟DOM的概念,因此,Vue2.x选择了中等粒度的解决方案,每一组件一个watch实例,这样的状态变化时只能通知到组件,再通过虚拟DOM去进行对比和渲染。

Vue Diff

src/core/instance/lifecycle.js

mountComponent 渲染,更新组件

// 定义更新函数
updateComponent = () => {
  // 调用lifeCycleMixin中定义的_update和renderMixin中定义的_render
  vm._update(vm._render(), hydrating);
}

src/core/instance/render.js

_render 生成虚拟DOM

src/core/instance/lifecycle.js

_update 负责更新dom,将vnode转换为dom

src/platforms/web/runtime/index.js

patch是在平台特有代码中指定的

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

流程明细

Watch.run() => componentUpdate() => render() => update() => patch()

Diff算法

同层比较,深度优先遍历

patch原理

  • src/core/vdom/patch.js

patch对比.png

-   oldVnode老节点有值, 但是vnode新节点是空,销毁oldVnode老节点

-   oldVnode老节点是空,vnode新节点是有值情况,创建根节点

-   oldVnode老节点有值,vnode新节点也有值的情况,进行**patchVnode**打补丁

    -   **patchVnode** 比较两个vnode,包括三种类型操作:**属性更新****文本更新****子节点更新**        -   oldVnode老节点和vnode新节点一样不用diff

        -   新老节点都有children,比较children,调用**updateChildren**            -   **updateChildren** 主要的作用是一种高效的方式对比新旧两个VNode的children得出最小的补丁。执行一个双循环的传统的方式,vue种针对web场景特点做了特别的算法优化。

updateChildren.png

                -   创建4个游标,4个游标不能相交,相交停止循环。
                -   oldStartVnode是空的情况,++oldStartIdx,右移一位
                -   oldEndVnode是空的,--oldEndIdx,左移一位
                -   oldStartVnode和newStartVnode对比完成,++oldStartIdx, ++newStartIdx, 同时右移一位
                -   oldEndVnode和newEndVnode对比完成,--oldEndIdx, --newEndIdx, 同时左移一位
                -   oldStartVnode和newEndVnode对比完成执行插入操作,++oldStartIdx, 右移一位,--newEndIdx,左移一位
                -   oldEndVnode和newStartVnode对比完成执行插入操作,--oldEndIdx, 左移一位,++newStartIdx, 右移一位
                -   上面几种情况都没有找到的话,就老老实实查找, 循环节点进行对比。
                -   最后oldStartIdx > oldEndIdx 批量添加节点, newStartIdx > newEndIdx 批量删除节点。

        -   oldCh老节点的children是空的,ch新节点的children不是空的,批量添加节点。

        -   oldCh老节点的children不是空的,ch新节点的children是空的,批量删除节点。

        -   oldVnode老节点的内容不是空的,vnode新节点是空的,清空内容。

        -   oldVnode老节点和vnode新节点都是文本,对比文本不一样进行替换。

-   oldVnode老节点有元素,vnode新节点没有元素,清空oldVnode元素

结语

  1. 在Vue2.x版本中引入虚拟DOM是必然的选择,Vue的响应式关联到多个Watcher更新,一旦Watcher过多性能就会降低,这也是Vue不能支持大型项目的原因。引入虚拟DOM之后一个组件对应一个Watcher这样的话颗粒度就会变小相应的会提升性能,但是如果一个组件对应一个Watcher实例的话,使用单纯的循环操作数据量大的话复杂度就会变高,所以Vue2.x版本的Diff算法巧妙的解决了这种问题。
  2. Vue Diff算法在新老两组VNode节点的左右头尾两侧做了标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。这样对比一圈之后大部分的节点就会对比完成。如果还有没有对比到VNode节点就进行遍历对比判断。最后如果新VNode节点多于老VNode节点就批量添加。如果新VNode节点少于老VNode节点就批量删除。