之前面试被面试官问到了解Vue的virtual dom吗?然后接着聊下去自然聊到了diff算法,但答得不好,所以随后我也立刻复习了一番。
Vue 2.0更新了virtual dom 的概念,而数据更改后的前后对比算法就是diff算法,这篇就是主要讲解diff算法是如何对比数据更改后的差异化的。
diff的比较方式
其实Vue与react的diff算法大同小异,所以这里我借用一张图来描述是如何对比的。
从图看出,diff是一层一层同层对比,不会跨层对比。
diff的流程
从网上找到了一个图,自己就懒得重新画了,大致过程就是这样。
这个图把整个diff的流程都过了一遍,接下来我们具体分析是如何进行比较的。
diff的具体分析
这里会对vue的diff源码进行解析,大家可以同步查阅vue的源码(在src/core/vdom/patch.js)
patch
patch是整个diff的入口,会先从根节点对
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// some code
return vnode
}
复制代码
而此处一个基础方法会经常被用来判断新旧节点是否一致 sameVode
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
复制代码
我们回忆刚刚的流程图,如果节点一致,然后我们就继续检查它们的子节点。如果节点不一致,我们直接把旧节点替换成新节点。
patchNode
当我们确定两个节点值得比较时,我们会进入到patchNode
方法。
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
复制代码
这个函数做了以下事情:
- 判断
Vnode
和oldVnode
是否相同,如果是,那么直接return; - 如果他们都有文本节点并且不相等,那么将更新为
Vnode
的文本节点。 - 如果
oldVnode
有子节点而Vnode
没有,则删除el的子节点 - 如果
oldVnode
没有子节点而Vnode
有,则将Vnode
的子节点真实化之后添加到el - 如果两者都有子节点,则执行
updateChildren
函数比较子节点,而这个函数也是diff逻辑最多的一步
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
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
//isUndef 是判断值是否等于undefined或者null
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 {
//看节点是否有key 没有则新建一个 有的话可以复用
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 {
// 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(oldCh, oldStartIdx, oldEndIdx)
}
}
复制代码
接下来进入解析步骤,我们可以看到中间有一大段 if else
判断,我们只要弄清楚这一块逻辑,那么整个diff过程也就差不多理解完毕了。
除开一开始判断旧节点是否为空之外,主要有4个对比过程
从头得知,新旧节点分别都2个指针,分别指向各自的头部与尾部。那么接下来就开始操作了。
- 当新旧节点的头部值得对比,进入
patchNode
方法,同时各自的头部指针+1; - 当新旧节点的尾部值得对比,进入
patchNode
方法,同时各自的尾部指针-1; - 当
oldStartVnode
,newEndVnode
值得对比,说明oldStartVnode
已经跑到了后面,那么就将oldStartVnode.el
移到oldEndVnode.el
的后边。oldStartIdx+1,newEndIdx-1; - 当
oldEndVnode
,newStartVnode
值得对比,说明oldEndVnode
已经跑到了前面,那么就将oldEndVnode.el
移到oldStartVnode.el
的前边。oldEndIdx-1,newStartIdx+1; - 当以上4种对比都不成立时,通过
newStartVnode.key
看是否能在oldVnode中
找到,如果没有则新建节点,如果有则对比新旧节点中相同key的Node,newStartIdx+1。
当循环结束时,这时候会有两种情况。
oldStartIdx > oldEndIdx
,可以认为oldVnode
对比完毕,当然也有可能 newVnode也刚好对比完,一样归为此类。此时newStartIdx和newEndIdx之间的vnode是新增的,调用addVnodes,把他们全部插进before的后边。newStartIdx > newEndIdx
,可以认为newVnode
先遍历完,oldVnode
还有节点。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除。
总结:以上就是diff的解析过程,其实过程并不复杂,只需要理清楚思路,知道它是如何将新旧节点做对比即可。同时给节点加上key,能够有效复用。
参考链接: