一、原理介绍
Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。数据改变时,Vue能够计算出重新渲染组件的最小代价并应用到DOM操作上。


diff 算法包括几个步骤:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中;
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异;
- 把所记录的差异应用到所构建的真正的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的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
二、源码解读
Vue实例化时调用update方法(src\core\instance\lifecycle.js),第一个参数为render函数生成的vnode,将vnode赋值给实例vm的_vnode属性,用于更新时的diff运算。然后调用实例的__patch__方法将vnode转化为真实的dom。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
__patch__方法在不同的平台上分别定义,以浏览器端为例,__pathch__方法被定义为patch方法(src\platforms\web\runtime\index.js),patch方法是createPatchFunction({ nodeOps, modules })的返回值。createPatchFunction方法在src\core\vdom\patch.js中定义,返回patch函数
function patch (oldVnode, vnode, hydrating, removeOnly) {
// oldVnode存在,vnode不存在时,调用destroy钩子
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// ……
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
//第一次渲染isInitialPatch为true
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
首次加载直接调用createElm(vnode,insertedVnodeQueue),之后的更新如果是同类型节点直接调用patchVnode进行更新,不同类型节点调用createElm (vnode, insertedVnodeQueue, parentElm, refElm)创建新节点,并插入到正确的位置。
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) {
// ……
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) { // 元素节点
// ……
vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ……
} else {
createChildren(vnode, children, insertedVnodeQueue) // 递归创建子节点
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else if (isTrue(vnode.isComment)) { // 注释
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else { // 文本节点
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
创建节点后调用insert()将生成的 DOM 元素挂载到页面中。
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
patchVnode和updateChildren定义在src\core\vdom\patch.js中,其中用到的DOM操作在src\platforms\weex\runtime\node-ops.js中定义,其他条件判断的操作在src\shared\util.js中。关于patchVnode和updateChildren的过程,感兴趣的话可以查看下面的文章