通过前文的介绍,我们知道需要将VNode
转换成真实的node
节点,需要通过patch
函数来实现:
vm.$el = vm.__patch__(prevVnode, vnode)
而__patch__
是在platforms/runtime/index.js
中定义的:
Vue.prototype.__patch__ = inBrowser ? patch : noop
这里主要是为了判断当前环境是否是在浏览器环境中,也就是是否存在Window
对象。这里也是为了做跨平台的处理,如果是在server render
环境,那么patch
就是一个空操作。
我们接着去找render
的实现:
export function createPatchFunction (backend) {
...
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// 如果vnode不存在但oldVnode存在,则表示要移除旧的node
// 那么就调用invokeDestroyHook(oldVnode)来进行销毁
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 如果oldVnode不存在,vnode存在,则创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
// nodeType 节点的类型,详细:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
const isRealElement = isDef(oldVnode.nodeType)
// 如果oldVnode、vnode都存在
// 如果oldVnode与Vnode是同一节点是就调用patchVnode处理去比较两个节点的差异
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// 如果存在真实的节点,存在data-server-rendered属性
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
// 需要用hydrate函数将虚拟DOM和真实DOM进行映射
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
...
}
// 如果不是server-rendered 或者hydration失败
// 创建一个空VNode,代替oldVnode
oldVnode = emptyNodeAt(oldVnode)
}
// 将oldVnode设置为对应的虚拟dom,找到oldVnode.elm的父节点
// 根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm的位置
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 递归更新父级占位节点元素,
if (isDef(vnode.parent)) {
...
}
// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 返回节点
return vnode.elm
}
}
这里通过createPatchFunction
函数,来创建返回一个patch
函数。path
接收6个参数:
- oldVnode: 旧的虚拟节点或旧的真实dom节点
- vnode: 新的虚拟节点
- hydrating: 是否要跟真是dom混合
- removeOnly: 特殊flag,用于组件
- parentElm:父节点
- refElm: 新节点将插入到refElm之前
具体解析看代码注释~抛开调用生命周期钩子和销毁就节点不谈,我们发现代码中的关键在于
sameVnode
、createElm
和patchVnode
方法。
判断2个节点,是否是同一个节点
/**
* 节点 key 必须相同
* tag、注释、data是否存在、input类型是否相同
* 如果isAsyncPlaceholder是true,则需要asyncFactory属性相同
*/
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 createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested // for transition enter check
// 用于创建组件,在调用了组件初始化钩子之后,初始化组件,并且重新激活组件。
// 在重新激活组件中使用 insert 方法操作 DOM
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 错误检测,主要用于判断是否正确注册了component,这个错误还是比较常见
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
inPre++
}
if (
!inPre &&
!vnode.ns &&
!(
config.ignoredElements.length &&
config.ignoredElements.some(ignore => {
return isRegExp(ignore)
? ignore.test(tag)
: ignore === tag
})
) &&
config.isUnknownElement(tag)
) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// nodeOps 封装的操作dom的合集
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode) // 用于为 scoped CSS 设置作用域 ID 属性
// weex处理
if (__WEEX__) {
...
} else {
// 用于创建子节点,如果子节点是数组,则遍历执行 createElm 方法.
// 如果子节点的 text 属性有数据,则使用 nodeOps.appendChild(...) 在真实 DOM 中插入文本内容。
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// insert 用于将元素插入真实 DOM 中
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)
}
}
通过以上的注释,我们可以知道:createElm
方法的最终目的就是创建真实的 DOM
对象
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新老 vnode 相等
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// 异步占位
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 复用新老节点被标记为static,新老节点key相同,新 vnode 是克隆所得;新 vnode 有 v-once 的属性
// 如果新节点没有被克隆,这意味着渲染函数已经被hot-reload-api重置,我们需要做一个适当的重新渲染。
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
// 执行 data.hook.prepatch 钩子
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 遍历调用 cbs.update 钩子函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 执行 data.hook.update 钩子
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 旧 vnode 的 text 选项为 undefined
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新老节点的 children 不同,执行 updateChildren 方法。
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 旧 vnode children 不存在 执行 addVnodes 方法
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 新 vnode children 不存在 执行 removeVnodes 方法
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果新旧 vnode 都是 undefined,且老节点存在 text,清空文本
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新老节点文本不同,更新文本内容
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// 执行 data.hook.postpatch 钩子,至此 patch 完成
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
addVnodes
和removeVnodes
都比较好理解,一个是增加一个节点元素,一个是删除节点元素。主要来看一下updateChildren
方法:
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 <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
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)
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
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)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
这里说起来可能会比较复杂,下面开始用图来解释一下整体的流程:
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] // 新列表终点值
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
...
}
进行循环遍历,遍历条件为 oldStartIdx <= oldEndIdx
和 newStartIdx <= newEndIdx
,在遍历过程中,oldStartIdx
和 newStartIdx
递增,oldEndIdx
和 newEndIdx
递减。当条件不符合跳出遍历循环
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
如果oldStartVnode
不存在,oldCh
起始点向后移动。如果oldEndVnode
不存在,oldCh
终止点向前移动。
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
如果oldStartVnode
和 newStartVnode
是sameVnode,则patchVnode
,同时彼此向后移动一位
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
如果oldEndVnode
和 newEndVnode
是sameVnode,则patchVnode
,同时彼此向前移动一位
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]
}
如果oldStartVnode
和 newEndVnode
是 sameVnode,则先 patchVnode
,然后把oldStartVnode
移到oldCh
最后的位置即可,然后oldStartIdx
向后移动一位,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]
}
如果oldEndVnode
和 newStartVnode
是 sameVnode,则先 patchVnode
,然后把oldEndVnode
移到oldCh
最前的位置即可,然后newStartIdx
向后移动一位,oldEndIdx
向前移动一位
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)
}
如果以上条件都不匹配,则查找oldVnode
中与vnode
具有相同key
的节点,并将查找的结果赋值给elmToMove
。如果找不到相同key的节点,则表示是新创建的节点
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)
}
newStartVnode = newCh[++newStartIdx]
若为同一类型就调用patchVnode
,就将对应下标处的oldVnode
设置为undefined
,把vnodeToMove
插入到oldCh
之前,newStartIdx
继续向后移动。如果两个 vnode 不相似,视为新元素,执行 createElm
创建。
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}
11. 如果老 vnode 数组的开始索引小于结束索引,说明老 node 数组长度大于新 vnode 数组,执行 removeVnodes 方法从 DOM 中移除老 vnode 数组中多余的 vnode。
else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
到这里,patch
的主要功能也基本讲完了,我们发现,在本篇中,大量出现了一个key
字段。经过上面的调研,其实我们已经知道Vue的diff
算法中其核心是基于两个简单的假设:
- 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构
- 同一层级的一组节点,他们可以通过唯一的id进行区分 基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n),当页面的数据发生变化时,Diff算法只会比较同一层级的节点:
如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则。 比如一下这个情况:
我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的:
即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率? 所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
所以一句话,key的作用主要是为了高效的更新虚拟DOM。另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。