揭秘Vue中的Virtual-Dom

47次阅读

共计 3596 个字符,预计需要花费 9 分钟才能阅读完成。

前言

Vue.js 2.0 引入 Virtual DOM, 比 Vue.js 1.0 的初始渲染速度提升了 2 - 4 倍,并大大降低了内存消耗。那么,什么是 Virtual DOM?为什么需要 Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?这是本文所要探讨的问题。

模板转换成视图的过程

在正式介绍 Virtual Dom 之前,我们有必要先了解下模板转换成视图的过程整个过程(如下图):

  • Vue.js 通过编译将 template 模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树
  • 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行 DOM 操作来更新视图。

简单点讲,在 Vue 的底层实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合 Vue 自带的响应系统,在状态改变时,Vue 能够智能地计算出重新渲染组件的最小代价并应到 DOM 操作上。

我们先对上图几个概念加以解释:

  • 渲染函数:渲染函数是用来生成 Virtual DOM 的。Vue 推荐使用模板来构建我们的应用界面,在底层实现中 Vue 会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
  • VNode 虚拟节点 :它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode 可以理解成 节点描述对象,它描述了应该怎样去创建真实的 DOM 节点。
  • patch(也叫做 patching 算法):虚拟 DOM 最核心的部分,它可以将 vnode 渲染成真实的 DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出,patch 本身就有补丁、修补的意思,其实际作用是在现有 DOM 上进行修改来实现更新视图的目的。Vue 的 Virtual DOM Patching 算法是基于 Snabbdom 的实现,并在些基础上作了很多的调整和改进。

Virtual DOM 是什么?

Virtual DOM 其实就是一棵以 JavaScript 对象 (VNode 节点) 作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。

简单来说,可以把 Virtual DOM 理解为一个简单的 JS 对象,并且最少包含标签名 (tag)、属性(attrs) 和子元素对象 (children) 三个属性。不同的框架对这三个属性的命名会有点差别。

对于虚拟 DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了 模板 → 渲染函数 → 虚拟 DOM 树 → 真实 DOM的一个过程

Virtual DOM 作用是什么?

虚拟 DOM 的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的 DOM 操作。例如,一个 ul 标签下很多个 li 标签,其中只有一个 li 有变化,这种情况下如果使用新的 ul 去替代旧的 ul, 因为这些不必要的 DOM 操作而造成了性能上的浪费。

为了避免不必要的 DOM 操作,虚拟 DOM 在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行 DOM 操作,从而避免操作其他无需改动的 DOM。

其实虚拟 DOM 在 Vue.js 主要做了两件事:

  • 提供与真实 DOM 节点所对应的虚拟节点 vnode
  • 将虚拟节点 vnode 和旧虚拟节点 oldVnode 进行对比,然后更新视图

为何需要 Virtual DOM?

  • 具备跨平台的优势

由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

  • 操作 DOM 慢,js 运行效率高。我们可以将 DOM 对比操作放在 JS 层,提高效率。

因为 DOM 操作的执行速度远不如 Javascript 的运算速度快,因此,把大量的 DOM 操作搬运到 Javascript 中,运用 patching 算法来计算出真正需要更新的节点,最大限度地减少 DOM 操作,从而显著提高性能。

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

  • 提升渲染性能

Virtual DOM 的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

为了实现高效的 DOM 操作,一套高效的虚拟 DOM diff 算法显得很有必要。我们通过 patch 的核心 —-diff 算法,找出本次 DOM 需要更新的节点来更新,其他的不更新。比如修改某个 model 100 次,从 1 加到 100,那么有了 Virtual DOM 的缓存之后,只会把最后一次修改 patch 到 view 上。那 diff 算法的实现过程是怎样的?

diff 算法

Vue 的 diff 算法是基于 snabbdom 改造过来的,仅在同级的 vnode 间做 diff,递归地进行同级 vnode 的 diff,最终实现整个 DOM 树的更新 。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从 O(n3) 变成 O(n)。

diff 算法包括几个步骤:

  • 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
  • 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  • 把所记录的差异应用到所构建的真正的 DOM 树上,视图就更新了

diff 算法的实现过程

diff 算法本身非常复杂,实现难度很大。本文去繁就简,粗略介绍以下两个核心函数实现流程:

  • patch(container,vnode) : 初次渲染的时候,将 VDOM 渲染成真正的 DOM 然后插入到容器里面。
  • patch(vnode,newVnode): 再次渲染的时候,将新的 vnode 和旧的 vnode 相对比,然后之间差异应用到所构建的真正的 DOM 树上。

1.patch(container,vnode)

通过这个函数可以让 VNode 渲染成真正的 DOM,我们通过以下模拟代码,可以了解大致过程:

function createElement(vnode) {    
var tag = vnode.tag  
var attrs = vnode.attrs || {}    
var children = vnode.children || []    
if (!tag) {return null}    
// 创建真实的 DOM 元素    
var elem = document.createElement(tag)   
 // 属性    
var attrName    
for (attrName in attrs) {if (attrs.hasOwnProperty(attrName)) { 
           // 给 elem 添加属性
           elem.setAttribute(attrName, attrs[attrName])
        }
    }
    // 子元素
    children.forEach(function (childVnode) {
        // 给 elem 添加子元素,如果还有子节点,则递归的生成子节点。elem.appendChild(createElement(childVnode))  // 递归
    })    // 返回真实的 DOM 元素   
 return elem
}

2.patch(vnode,newVnode)

这里我们只考虑 vnode 与 newVnode 如何对比的情况:

function updateChildren(vnode, newVnode) {var children = vnode.children || []
    var newChildren = newVnode.children || []
  // 遍历现有的 children
    children.forEach(function (childVnode, index) {var newChildVnode = newChildren[index]
  // 两者 tag 一样
        if (childVnode.tag === newChildVnode.tag) {
            // 深层次对比,递归
            updateChildren(childVnode, newChildVnode)
        } else { 
  // 两者 tag 不一样
           replaceNode(childVnode, newChildVnode) 
       }
    }
)}

给大家推荐一个好用的 BUG 监控工具 Fundebug,欢迎免费试用!

欢迎关注公众号:前端工匠,你的成长我们一起见证!

参考文章和书籍

  • 珠峰架构课(强烈推荐)
  • 剖析 Vue.js 内部运行机制
  • 深入浅出 Vue.js
  • Vue 2.0 学习笔记:Vue 的 render 函数
  • 揭秘一线互联网企业前端 JavaScript 高级面试
  • 对 virtual-dom 的一些理解
  • 深度剖析:如何实现一个 Virtual DOM 算法

正文完
 0