前因是因为面试了几家公司都提到了diff算法相关的知识
诸如:怎么比较两颗dom树的不同
其实在vue和react怎么实现虚拟DOM和真实DOM同步的diff算法可以体现
先大概有个概念,diff算法的整体策略是深度优先遍历,同级比较
直接开始看源码吧,下面是我在GitHub上找到的相关源码
diff算法的实现,patchNode和update Children这两个函数是比较关键的函数
本文仅限于过一遍diff算法的原理实现,有些细节没能照顾到,见谅哈~
patch
function patch (oldVnode, vnode) {
//利用sameVnode判断新旧接节点是否值得比较
if (sameVnode(oldVnode, vnode)) {
//新旧节点一样,值得比较
patchVnode(oldVnode, vnode)
} else {
//获取就节点挂载的对象
const oEl = oldVnode.el
//获取节点的父子节点,parentEle是真实的dom
let parentEle = api.parentNode(oEl)
//创建新节点真实的dom
createEle(vnode)
if (parentEle !== null) {
//给真实dom的父节点插入新的dom(vnode),同时移除旧的dom
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
return vnode //重点:返回的一个真实dom
}
var oldVnode = patch (oldVnode, vnode)
patch的过程其实是将虚拟dom和真实的dom对比后的结果反映为真实的dom,实现更新
过程
- 通过sameNode判断两个节点的是否需要比较
- 若值得比较,则调用patchNode进行比较;若不值得则获取真实的父子节点,同时创建新的节点的真实dom,并把新的节点的真实dom插入到为真实dom的父子节点parentEle
以下是怎么判断两个节点是否一致的sameVnode方法
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)
)
)
)
}
只有节点上所有的key,标签tag,数据data完全一致,才值得比较节点情况
patchNode
对源码有删改,只留下了关键的部分
function patchVnode(
oldVnode, //旧节点
vnode, //新节点
insertedVnodeQueue, //增加新节点的队列
) {
//比较新旧节点 节点引用相同 直接返回
if (oldVnode === vnode) {
return
}
//diff算法 新旧节点的孩子
const oldCh = oldVnode.children
const ch = vnode.children
//找到真实dom => el
const elm = vnode.elm = oldVnode.elm
//新旧节点有文本节点
if (isUndef(vnode.text)) {
//新旧节点都有子节点
if (isDef(oldCh) && isDef(ch)) {
//子节点不相同,调用updateChildren方法
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
//新节点有子节点,旧节点没有
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
//旧节点的文本节点要清空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
//旧节点有子节点,新节点没有
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
//新旧节点的文本节点不一样,给elm添加新的文本节点替换
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
}
过程总结一下:
- 获取真实的dom ,为el
oldVnode === vnode新旧节点的引用相同,直接返回- 新旧节点都有文本节点,不过不相等,则将el的文本节点设置为新节点的文本节点
- 新节点有子节点,旧节点没有,则将新节点的子节点真实化后添加到el
- 旧节点有子节点,新节点没有,则删除el的子节点
- 当两者都有子节点,则调用updateChildren方法比较子节点
updateChildren
//updateChildren方法以下是方法的关于四种比较
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
//新尾和旧尾比较
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
//旧头和新尾比较
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
//把旧头插入到旧尾的位置 oldEndVnode.el
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
//新头和旧尾比较
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
//把旧尾插入到旧头的位置 oldStartVnode.el
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
这个方法是人为设计出了四种比较方法,新节点的每一个子节点都首先用着四种方法比较,如果两个节点符合比较资格,则继续调用patchNode进行更深层的比较,循环进行到最后一个子节点
具体是怎么实现的,结合图片来理解
下面是自己对源码实现具体比较的过程的一些解读,updateChildren是diff算法实现高效性的操作,情况考虑的很多,也是有些复杂,不知道自己的理解是否有错误的点,欢迎批评指正,感谢!
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, 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) {
//...
//四种比较
//新头和旧头比较
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
//新尾和旧尾比较
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
//旧头和新尾比较
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
//把旧头插入到旧尾的位置 oldEndVnode.el
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
//新头和旧尾比较
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
//把旧尾插入到旧头的位置 oldStartVnode.el
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 不符合四种比较的情况,使用key时作为index的比较
if (oldKeyToIdx === undefined) {
//首先根据旧节点的所有子节点生成index
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
//新节点的key和旧节点的key没有匹配的,则直接插入到oldStartVnode的位置
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
//有匹配,进行移动的操作,idxInOld是指移动到具体的索引值
elmToMove = oldCh[idxInOld]
//要移动的节点和新节点的css选择器不一致,则直接插入到oldStart的位置
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
//节点一致,再次进行比较子节点
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
//遍历结束的位置,当startindex大于endindex,说明遍历完了
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
源码是在github的vuejs/src/core/patch.js扒下来的
参考: