Vue 2.x 的虚拟DOM原理

Posted Qunar技术沙龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue 2.x 的虚拟DOM原理相关的知识,希望对你有一定的参考价值。


Vue 2.x 的虚拟DOM原理


背景


当下随着 Vue、React 等框架的流行,虚拟 DOM 大行其道。第一次听到这个词还是在15年学习 React 的时候。当时 React 以其明显高于 Angular 的性能优势迅速走红,什么虚拟 DOM 啦?什么修改 DOM 完全是操作内存中虚拟 DOM,然后再一次性更新到浏览器的真实 DOM 中?当时一听,瞬间感觉好高大上,可要问虚拟 DOM 是什么?怎么实现的?那真是一脸懵逼。时隔两年,我们来说说到底何为虚拟 DOM。


为何采用虚拟 DOM


众所周知,频繁访问和操作 DOM 是一种很浪费性能的行为,即使浏览器已经采取了部分措施来优化。通常性能优化的经验就是减少 DOM 的访问 和 批量更新,例如使元素脱离文档流。 但随着应用规模的扩大,前端业务处理逻辑的增强,这些远远不够。而为了更好的优化性能,大牛们就联想到了虚拟 DOM。

如今有很多流行的前端框架都内置了虚拟 DOM 算法来优化性能,例如 Reactpreactinferno等,而本文接下来的内容将从 Vue 框架中的所采用的虚拟 DOM 算法 snabbdom 入手来说明何为虚拟 DOM。


虚拟节点


首先,snabbdom 通过如下的数据结构来存储一个虚拟节点。


{

    sel: 'div.page',

    key: 0,

    data: {

        "props": {

            className: "container"

        }

    },

    children: [],

    text: 'hello world',

    elm: Element

}


其中,sel 属性指代该虚拟节点映射到真实 DOM 中的容器节点。key 是唯一索引值,在列表排序中可以通过该唯一值节省非必要渲染的性能。data 是该节点的一些属性,例如上述显示该节点的 className 包含 containerchildren 存储该节点下面的所有子节点,每个子节点又是同样的虚拟节点结构。text 属性仅当该节点仅有一个文本子节点时,值为该文本内容。elm 属性指向该虚拟节点在对应的真实 DOM 元素。

所以,现在我们应该明白,所谓的虚拟 DOM 本质上是通过恰当的数据结构和算法,来达到优化 DOM 操作性能的目的。嗯,至少目前看来,它确实是这样的。


DOM 节点的创建


首先我们通过框架暴露的 init 方法初始化一个指定的渲染函数 patch


var patch= snabbdom.init([

  require('../../modules/class').default,

  require('../../modules/hero').default,

  require('../../modules/style').default,

 require('../../modules/eventlisteners').default,

]);


然后与大多数虚拟 DOM 算法类似,需要指定渲染到真实 DOM 节点的容器 patch(container, vnode),框架内部通过深度遍历上文中的虚拟节点结构创建真实的 DOM 树。最后通过 container.parentElement.replaceChild(vnode.ele, container) 方式成功转换成真实的浏览器 DOM 节点。


虚拟节点的更新


在第一次创建真实 DOM 节点之后,接下来的 DOM 更新 patch(oldVnode, vnode) 就需要 diff 新老虚拟节点,具体的处理逻辑如下:

//...

var elm =vnode.elm = oldVnode.elm;

//...

if(isUndef(vnode.text)) {

    if (isDef(oldCh) && isDef(ch))

 {

        // 更新子节点即 children 属性

        if (oldCh !== ch) updateChildren(elm,oldCh, ch, insertedVnodeQueue);

    } else if (isDef(ch)) {

        // 添加新节点
        addVnodes(elm, null, ch, 0, ch.length -1, insertedVnodeQueue);

    } else if (isDef(oldCh)) {

        // 删除没有的旧节点

        removeVnodes(elm, oldCh, 0,oldCh.length - 1);

    }

} else if(oldVnode.text !== vnode.text) {

    // 更新节点的 textContent

    elm.textContent = vnode.text;

}

//...

此处摘录过来的第一行代码含有一个隐藏逻辑:并没有通过 document.createElement() 为新的虚拟节点创建真实的 elm,仍然使用的是第一步创建过的真实 DOM 容器元素。而子节点数组的 diff 更新流程如下:

Vue 2.x 的虚拟DOM原理

上述详细的处理流程在 updateChildren 函数中。而对于子节点再包含子节点,则是通过递归循环遍历所有情形。


最后


snabbdom 的核心是虚拟 DOM 库,其他例如样式和事件都是通过第三方模块的方式完成的,使用者通过 init 传入需要的功能模块。而这些第三方模块实际上是通过一种生命周期钩子的方式注入的,例如:class 模块内部源码就是下面这样:

exportconst classModule = { create: updateClass, update: updateClass};

在 create 和 update 生命周期阶段更新元素的 class。而框架内部支持如下几种生命周期钩子:['create', 'update', 'remove', 'destroy', 'pre', 'post']


总结


本文简单的描述了 Vue 框架里面引用的 snabbdom 的虚拟 DOM 思想,而这只是当下流行的虚拟 DOM 的冰山一角,下篇我们会对 React 库里面采用的虚拟 DOM 思路进行分析,这也是是令 React 大行其道的最重要原因。


以上是关于Vue 2.x 的虚拟DOM原理的主要内容,如果未能解决你的问题,请参考以下文章

vue虚拟dom实现原理

vue渲染虚拟dom树原理

深入剖析:Vue核心之虚拟DOM

快来看看Vue的虚拟DOM和Diff算法的原理

Vue原理解析(五):彻底搞懂虚拟Dom到真实Dom的生成过程

前端技能树,面试复习第 27 天—— React Diff 算法的原理,和 Vue 有什么区别 | 虚拟 DOM | key 的原理,为什么要用