Vue.js 2.0引入Virtual DOM,比Vue.js 1.0的初始渲染速度提高了2-4倍,并大大下降了内存消耗。那么,什么是Virtual DOM?为何须要Virtual DOM?它是经过什么方式去提高页面渲染效率的呢?这是本文所要探讨的问题。html
在正式介绍 Virtual Dom以前,咱们有必要先了解下模板转换成视图的过程整个过程,以下图(参考《深刻浅出vue.js》):前端
简单点讲,在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue可以智能地计算出从新渲染组件的最小代价并应到DOM操做上。vue
Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)做为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终能够经过一系列操做使这棵树映射到真实环境上。node
简单来讲,能够把Virtual DOM 理解为一个简单的JS对象,而且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不一样的框架对这三个属性的命名会有点差异。git
对于虚拟DOM,我们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟DOM树 → 真实DOM
的一个过程 github
虚拟DOM的最终目标是将虚拟节点渲染到视图上。可是若是直接使用虚拟节点覆盖旧节点的话,会有不少没必要要的DOM操做。例如,一个ul标签下不少个li标签,其中只有一个li有变化,这种状况下若是使用新的ul去替代旧的ul,由于这些没必要要的DOM操做而形成了性能上的浪费。面试
为了不没必要要的DOM操做,虚拟DOM在虚拟节点映射到视图的过程当中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)作对比,找出真正须要更新的节点来进行DOM操做,从而避免操做其余无需改动的DOM。算法
其实虚拟DOM在Vue.js主要作了两件事:浏览器
因为 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,因此使它具备了跨平台的能力,好比说浏览器平台、Weex、Node 等。缓存
由于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须要更新的节点来更新,其余的不更新。那diff 算法的实现过程是怎样的?
Vue的diff算法是基于snabbdom改造过来的,仅在同级的vnode间作diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新。由于跨层级的操做是很是少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。
diff 算法包括几个步骤:
diff 算法自己很是复杂,实现难度很大。本文去繁就简,粗略介绍如下两个核心函数实现流程:
经过这个函数可让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
}
复制代码
这里咱们只考虑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,欢迎免费试用!
欢迎关注公众号:前端工匠,你的成长咱们一块儿见证!