【Vue面试进阶系列】Vue diff算法 源码解析

之前面试被面试官问到了解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)
        }
    }
}
复制代码

这个函数做了以下事情:

  • 判断VnodeoldVnode是否相同,如果是,那么直接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个指针,分别指向各自的头部与尾部。那么接下来就开始操作了。

  1. 当新旧节点的头部值得对比,进入patchNode方法,同时各自的头部指针+1
  2. 当新旧节点的尾部值得对比,进入patchNode方法,同时各自的尾部指针-1
  3. oldStartVnodenewEndVnode值得对比,说明oldStartVnode已经跑到了后面,那么就将oldStartVnode.el移到oldEndVnode.el的后边。oldStartIdx+1,newEndIdx-1
  4. oldEndVnodenewStartVnode值得对比,说明oldEndVnode已经跑到了前面,那么就将oldEndVnode.el移到oldStartVnode.el的前边。oldEndIdx-1,newStartIdx+1
  5. 当以上4种对比都不成立时,通过newStartVnode.key 看是否能在oldVnode中找到,如果没有则新建节点,如果有则对比新旧节点中相同key的Node,newStartIdx+1

当循环结束时,这时候会有两种情况。

  1. oldStartIdx > oldEndIdx,可以认为oldVnode对比完毕,当然也有可能 newVnode也刚好对比完,一样归为此类。此时newStartIdx和newEndIdx之间的vnode是新增的,调用addVnodes,把他们全部插进before的后边
  2. newStartIdx > newEndIdx,可以认为newVnode先遍历完,oldVnode还有节点。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除

总结:以上就是diff的解析过程,其实过程并不复杂,只需要理清楚思路,知道它是如何将新旧节点做对比即可。同时给节点加上key,能够有效复用。

参考链接:

github.com/aooy/blog/i…

转载于:https://juejin.im/post/5ccef5c76fb9a031fd635095

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值