本文基于vue-2.4.4源码进行分析
Vue 2.0开始,引入VirtualDOM
。
使用VirtualDOM
而不使用真实DOM
是出于性能优化的考虑。
真实DOM
使用document.createElement
创建DOM元素,但是这个方法会带来性能上的损失。
举个例子:
let div = document.createElement('div');
let count = 0
for(let k in div) {
count++
}
console.log(count) // 231
执行上面的代码,我们可以看到该方法创建的DOM
元素的属性多达231个,但是我们真正需要的可能只有不到10%。
为了解决这个问题,VirtualDOM
应运而生。它和真实DOM
保持映射关系,每个VNode
节点都存储了对应真实DOM
节点的一些重要参数,当数据发生改变时,在改变真实DOM
节点之前,会先比较相应的VNode的的数据,如果需要改变,才更新真实DOM
。这样就可以通过操作VirtualDOM
来提高直接操作DOM
的效率和性能。
比较VNode
数据这个操作就是我们今天要讨论的patch
,在讨论之前,我们先简单说下VNode
。
VNode
在上篇Vue 2.0 模板编译源码分析中我们得出模板编译的结果是render function
。
render function
的运行结果就是VNode
, 参考src/core/instance/render.js
Vue.prototype._render = function (): VNode {
...
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options
...
vnode = render.call(vm._renderProxy, vm.$createElement)
...
}
Vue 2.0中的VNode(src/core/vdom/vnode.js)定义如下:
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
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 // 对应的真实 DOM 元素
this.ns = undefined
this.context = context
this.functionalContext = 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
}
}
它是真实DOM
的简化版,与真实DOM
一一对映。通过new
实例化的VNode
可以分为:EmptyVNode
(注释节点)、TextVNode
(文本节点)、ElementVNode
(元素节点)、ComponentVNode
(组件节点)、CloneVNode
(克隆节点)等。
patch原理
再拉通一下整个思路,目前我们晓得
render function
生成 VNode
,是在 vm._render
里完成的。
那么vm._render
方法又是在什么时候调用的呢?
debugger
一下代码,可以看到流程如下:
初始化时,通过render function
生成 VNode
的同时进行Watcher
的绑定。当数据发生会变化时,会执行_update
方法,生成一个新的VNode
对象,然后调用__patch__
方法,比较新生成的VNode
和旧的VNode
,最后将差异(变化的节点)更新到真实的DOM
树上。
patch(src/core/vdom/patch.js)所用的diff算法来源于snabbdom,只会在同层级进行比较,不会跨层级比较。图示如下:
下面结合源码进行原理分析:
入参
patch方法接收6个参数:
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
...
}
-
oldVnode
: 旧的VNode
或旧的真实DOM
节点 -
vnode
: 新的VNode
-
hydrating
: 是否要和真实DOM
混合 -
removeOnly
: 特殊的flag,用于<transition-group>
-
parentElm
: 父节点 -
refElm
: 新节点将插入到refElm
之前
流程
-
如果
vnode
不存在,但是oldVnode
存在,说明是需要销毁旧节点,则调用invokeDestroyHook(oldVnode)
来销毁oldVnode
。if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }
-
如果
vnode
存在,但是oldVnode
不存在,说明是需要创建新节点,则调用createElm
来创建新节点。if (isUndef(oldVnode)) { isInitialPatch = true // 用于做延迟插值处理 createElm(vnode, insertedVnodeQueue, parentElm, refElm) }
当
vnode
和oldVnode
都存在时
3.1 如果oldVnode不是真实节点,并且
vnode
和oldVnode
是同一节点时,说明是需要比较新旧节点,则调用patchVnode
进行patch
。-
3.2 如果
oldVnode
是真实节点时- 3.2.1 如果oldVnode是元素节点,且含有
data-server-rendered
属性时,移除该属性,并设置hydrating
为true
。 - 3.2.2 如果
hydrating
为true
时,调用hydrate
方法,将Virtural DOM
与真实DOM
进行映射,然后将oldVnode
设置为对应的Virtual DOM
。
- 3.2.1 如果oldVnode是元素节点,且含有
-
3.3 如果
oldVnode
是真实节点时或vnode
和oldVnode
不是同一节点时,找到oldVnode.elm
的父节点,根据vnode
创建一个真实的DOM
节点,并插入到该父节点中的oldVnode.elm
位置。如果组件根节点被替换,遍历更新父节点element
。然后移除旧节点。{ // 3. 当vnode和oldVnode都存在时 const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // 3.1 如果oldVnode不是真实节点,并且vnode和oldVnode是同一节点时 // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { // 3.2 如果oldVnode是真实节点时 // 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)) { // 3.2.1 如果oldVnode是元素节点,且含有`data-server-rendered`属性时 oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { // 3.2.2 如果hydrating为true时 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 ' + '<p>, or missing <tbody>. 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) } // 3.3 // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) if (isDef(vnode.parent)) { // component root element replaced. // update parent placeholder node element, recursively 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) } 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]() } } } ancestor = ancestor.parent } } if (isDef(parentElm)) { // 移除老节点 removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } }
- 最后返回
vnode.elm
。
原理
由上面的流程我们知道了当vnode
和oldVnode
都存在、oldVnode
不是真实节点,并且vnode
和oldVnode
是同一节点时,才会调用patchVnode
进行patch
。
下面根据patchVnode
源码分析patch的原理:
- 如果
oldVnode
和vnode
完全一致,则可认为没有变化,return
; - 如果
oldVnode
的isAsyncPlaceholder
属性为true
时,跳过检查异步组件,return
; - 如果
oldVnode
跟vnode
都是静态节点,且具有相同的key
,并且当vnode
是克隆节点或是v-once
指令控制的节点时,只需要把oldVnode.elm
和oldVnode.child
都复制到vnode
上,也不用再有其他操作,return
; - 否则,如果vnode不是文本节点时
4.1 如果
vnode
和oldVnode
都有子节点并且两者的子节点不一致时,就调用updateChildren
更新子节点。updateChildren
方法详细的解析可参考解析vue2.0的diff算法,图示说明,很形象。4.2 如果只有
vnode
有子节点,则调用addVnodes
创建子节点;4.3 如果只有
oldVnode
有子节点,则调用removeVnodes
把这些节点都删除;4.4 如果
oldVnode
和vnode
都没有子节点,但是oldVnode
是文本节点时,则把vnode.elm
的文本设置为空字符串;
- 如果
vnode
是文本节点但是vnode.text != oldVnode.text
时只需要更新vnode.elm
的文本内容就可以。
原理流程图如下:
自此,Vue的patch原理就分析完了。