写在前面
Virtual DOM的概念相信大家都不会陌生,Vritual DOM是相对与DOM(文档对象模型)来说的,上关于DOM的定义:“DOM模型用一个逻辑树来表示一个文档,树的每个分支的终点都是一个节点(node),每个节点都包含着对象(objects)。DOM的方法(methods)让你可以用特定方式操作这个树,用这些方法你可以改变文档的结构、样式或者内容”。相对于频繁地去操作DOM引起的性能问题,Vritual DOM很好地将DOM做了一层映射关系,将原来需要在DOM上的一系列操作,映射到来操作Virtual DOM。
“昂贵”的DOM
为了有更直观地感受“昂贵”的DOM,现在将一个简单的div元素的所有属性值打印出来:
let div = document.createElement('div')let str = ''for (let key in div) { str += key + ' '}复制代码
打印出来的str值为:
可见,真正的DOM元素是非常庞大的,因为浏览器把DOM设计地非常复杂,所以当我们频繁地去更新DOM时,会产生一定的性能问题。可以想象,用简单粗暴的方法将整个DOM结构用innerHTML修改到页面上,这样进行重绘整个视图层是相当消耗性能的。那我们更新DOM时,能不能只更新修改的地方呢?
VNode
我们知道,经历过render function之后会得到VNode节点,对这张图不太明白的话可以看下我写的这两篇文章 和 Vritual DOM其实就是以VNode节点(JavaScript对象)作为基础,用对象属性来描述节点,实际上它是一层对真实DOM的封装。Vritual DOM上定义了关于真实DOM的一些关键的信息,Vritual DOM完全是用JS去实现,和宿主浏览器没有任何联系,此外得益于js的执行速度,将原本需要在真实DOM进行的创建节点,删除节点,添加节点等一系列复杂的DOM操作全部放到Vritual DOM中进行。这样相对与用innerHTML粗暴地重绘整个视图性能将大大提高。将Virtual DOM修改的地方用diff算法来更新只修改地方,这样就能避免很多无谓的DOM修改,从而提高了性能。来看一下Vue.js源码中关于VNode的定义,定义在src/core/vdom/vnode.js中:
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array , text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance }}复制代码
其中:
tag: 当前节点的标签名
data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
children: 当前节点的子节点,是一个数组
text: 当前节点的文本
elm: 当前虚拟节点对应的真实dom节点
ns: 当前节点的名字空间
context: 当前节点的编译作用域
functionalContext: 函数化组件作用域
key: 节点的key属性,被当作节点的标志,用以优化
componentOptions: 组件的option选项
componentInstance: 当前节点对应的组件的实例
parent: 当前节点的父节点
raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
isStatic: 是否为静态节点
isRootInsert: 是否作为跟节点插入
isComment: 是否为注释节点
isCloned: 是否为克隆节点
isOnce: 是否有v-once指令
举个例子,我们现在有这样一个Vritual DOM:
{ tag: 'div' data: { class: 'outer' }, children: [ { tag: 'div', data: { class: 'inner' } text: 'Virtual DOM' } ]}复制代码
渲染之后的真实DOM为:
Virtual DOM复制代码
创建一个空VNode节点
export const createEmptyVNode = (text: string = '') => { const node = new VNode() node.text = text node.isComment = true return node}复制代码
创建一个文本节点
export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val))}复制代码
克隆一个VNode节点
export function cloneVNode (vnode: VNode): VNode { const cloned = new VNode( vnode.tag, vnode.data, vnode.children, vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ) cloned.ns = vnode.ns cloned.isStatic = vnode.isStatic cloned.key = vnode.key cloned.isComment = vnode.isComment cloned.fnContext = vnode.fnContext cloned.fnOptions = vnode.fnOptions cloned.fnScopeId = vnode.fnScopeId cloned.asyncMeta = vnode.asyncMeta cloned.isCloned = true return cloned}复制代码
总的来说,VNode 就是一个 JavaScript 对象,用 JavaScript 对象的属性来描述当前节点的一些状态,用 VNode 节点的形式来模拟一棵 Virtual DOM 树。
更新视图
我们知道,Vue.js通过数据绑定来更新视图,其中会调用updateComponent方法,对这一流程不太明白的话可以看一下上边提到的两篇文章。updateComponent方法定义如下:
updateComponent = () => { vm._update(vm._render(), hydrating)}复制代码
该方法会调用vm._update方法,该方法接受的第一个参数是刚生成的VNode,定义在src/core/instance/lifecycle.js中:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = 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) } activeInstance = prevActiveInstance // 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. }复制代码
其中在关键的地方加上注释:
// 新的vnode vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. // 如果需要diff的prevVnode不存在,那么就用新的vnode创建一个真实dom节点 if (!prevVnode) { // initial render // 第一个参数为真实的node节点 vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ) } else { // updates // 如果需要diff的prevVnode存在,那么首先对prevVnode和vnode进行diff,并将需要的更新的dom操作已patch的形式打到prevVnode上,并完成真实dom的更新工作 vm.$el = vm.__patch__(prevVnode, vnode) }复制代码
可以看到,该方法调用了一个核心方法__patch__,这可以说是整个Virtual DOM最核心的方法,主要完成了新的虚拟DOM节点和旧的虚拟DOM节点的diff过程,经过patch过程之后生成真实的DOM节点并完成视图的更新工作。
patch
接下来我们看一下vm.__patch__方法到底发生了什么,定义在src/core/vdom/patch.js中:
return function patch (oldVnode, vnode, hydrating, removeOnly) { 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, removeOnly) } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== 'production') { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + ', or missing . Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } // 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) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }复制代码
通过源码我们可以发现,当oldVnode(旧的节点)与vnode(新的节点)在sameVnode的时候才会进行patchVnode,sameVnode这个方法决定是否要对oldvnode和vnode进行diff和patch的过程。也就是新旧VNode节点判定为同一节点的时候才会进行patchVnode这个过程,否则就是创建新的DOM,移除旧的DOM。下面介绍一下sameVnode方法:
sameVnode
sameVnode定义在src/core/vdom/patch.js中:
function sameVnode (a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) )}function sameInputType (a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)}复制代码
通过代码可以看出,只有当新旧两个VNode的tag、key、isComment都相同,与此同时定义或未定义data的时候,且如果标签为input则type必须相同。这时候这新旧两个VNode则算sameVnode,接着进行进行patchVnode操作。
diff算法
Vue在2.x版本的vdom算法是基于snabbdom算法所做的修改实现的。
如图所示,diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种非常高效的算法。接下来看一下diff算法最重要的环节updateChildren源码的实现。updateChildren
updateChildren源码的定义在src/core/vdom/patch.js中:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly is a special flag used only by// to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }复制代码
这一块的源码解析可以参考。