面试中和
VDOM一起考察的经常是Diff算法,以前看别人的面经可能搞得不是很清楚,这次想自己总结一下,看的是Vue源码中关于Diff的部分,在我之前也在我的Vue源码阅读系列中曾经提到过patch函数(四)小菜鸡的Vue源码阅读——Vue是如何编译解析模板的并挂载组件,但是没有深入,这一篇面试准备刚好可以补充到那里去,本篇写作前参考了(完整版)快速掌握虚拟DOM和diff算法【Vue】
0. _update
这个方法是将VDOM渲染成真实DOM的,这里面自然也就涉及到了一个初始化和更新真实DOM两种情况,这里说明一下vue实例上挂载的一些属性
$el: 真实DOM_vnode: 虚拟DOM
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 这个初始化的时候是mountComponent函数中赋值,是根DOM元素
const prevEl = vm.$el
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// 第一次渲染,初始化,这个时候第一个参数是真实DOM
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 不是第一次渲染,更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
// ...
}
1.patch
__patch__方法在web/runtime中被注入,它是由一个函数构造器传入参数返回的,不过这和整个diff核心并不重要,重要的是看patch函数的实现
// platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
// platforms/web/runtime/patch.js
import { createPatchFunction } from 'core/vdom/patch'
export const patch: Function = createPatchFunction({ nodeOps, modules })
createPatchFunction作为一个创建器函数,闭包了很多方法,然后返回一个patch函数
export function createPatchFunction (backend) {
return function patch(){}
}
下面开始介绍主要的patch函数
createElm函数就是通过vnode生成真实DOM,然后挂载到vnode上的elm属性上,且会通过父元素插入的到真实的DOM中去oldVnode参数的类型为VNode|Element,第一次渲染的时候传的就是$el,是根DOM元素- 当前后的
vnode都存在且为同类节点时才符合patch的条件
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 新的vnode未定义说明是需要销毁了
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 旧vnode不存在
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
// 是原来的是虚拟DOM,且是同一个vnode,patch
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 原来是真实DOM
if (isRealElement) {
// ...
// 创建一个空的Vnode,并把这个真实DOM挂载上去
oldVnode = emptyNodeAt(oldVnode)
}
// 获取到当前真实DOM的父元素
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 创建新的DOM,并插入到真实的DOM
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// ...
// remove原来的DOM
}
}
return vnode.elm
}
2.patchVnode
该函数通过判断新旧vnode的ch和text来判断
- 最复杂的新旧都有ch,那么执行
updateChildren来更新,这里也是diff的核心 - 新有ch,旧有ch,那么增加
- 新没有ch,旧有ch,那么移除
- 如果text不一样,更新text
我们仔细发现
if...else的判断和我们预期想的不一样,那是因为vue的vdom生成中有一个算法就是,如果当前有ch,那么必然不会有text,因为text会合并到ch中去,所以多了这个条件,条件语句可以做一些优化,这种情况的出现大多数的博客没有做说明,我刚开始看的时候也很疑惑
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 没变化但就不需要patch了
if (oldVnode === vnode) {
return
}
// 把原来的真实DOM先拿到,因为新的VDOM的elm是undfined,我们需要知道原来的真实DOM渲染在哪
const elm = vnode.elm = oldVnode.elm
// 省略一些hook相关操作
const oldCh = oldVnode.children
const ch = vnode.children
// 根据新老节点的children判断
// 大条件,新节点无text
if (isUndef(vnode.text)) {
// 新老节点都有ch
if (isDef(oldCh) && isDef(ch)) {
// 更新ch
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 新有,旧没有
// 旧节点有text,则清除
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 增加新节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 旧有新没有
// 旧有ch,也就意味着旧没有text
// 移除旧的
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 新旧都没有ch,把文字清除
nodeOps.setTextContent(elm, '')
}
// 新节点有text,必然意味着节点无ch
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
// hook相关
}
3. updateChildren
- 首先定义四个指针,和四个指针指向的节点,接下来的循环中,每一次指针移动的时候,他们指向的节点也会移动
oldStartIdx:oldStartVnodenewStartIdx:newStartVnodeoldEndIdx:oldEndVnodenewEndIdx:newEndVnode
- 对新旧两个children做循环,循环终止的条件是
oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,新旧两端的指针有一个重合了就结束 - 每一次循环依次做五种比较,且如果比较成功就递归patchVnode,然后让指针往中间移动
- 旧头——新头
- 旧尾——新尾
- 旧头——新尾
- 旧尾——新头
- 比较新头指向的节点和旧节点中key是否有相同
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
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, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
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, newCh, newStartIdx)
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, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
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(oldCh, oldStartIdx, oldEndIdx)
}
}