Vue 源码学习之 diff 算法

Vue 的 diff 算法学习

diff 算法是一种同层的树节点进行比较的高效算法,有两个特点:

  • 只比较同层级的节点,不会跨层级比较;
  • diff 比较过程中,循环从两边向中间靠拢;

原理分析

当数据发生变化时,set会调用 dep.notify通知所有的 watcher,订阅者会调用 patch给真实 DOM 打补丁,更新相应的视图;

patch 函数:oldVnode 和 vnode 分别代表旧节点和新节点,主要做了四个判断:

  • 没有新节点,则直接调用 destory 销毁节点;
  • 没有旧节点,代表新建,直接调用 createElem 创建节点;
  • 旧节点和新节点自身一致,进行 sameVnode 判断,一样时调用 patchVnode;
  • 旧节点和新节点自身不一致,创建新节点,删除旧节点;
function patch(oldVnode, vnode, hydrating, removeOnly) {
    
    
    // 判断有没有新节点,没有则直接调用 destory
    if (isUndef(vnode)) {
    
    
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {
    
    
        // 没有旧节点,则直接创建节点
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue) 
    } else {
    
    
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
    
    
            // 旧节点和新节点一致,调用 patchVnode
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        } else {
    
    
            // 旧节点和新节点不一致,根据新节点创建新的 DOM 节点
            if (isRealElement) {
    
    
                if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    
    
                    oldVnode.removeAttribute(SSR_ATTR)
                    hydrating = true
                }
                if (isTrue(hydrating)) {
    
    
                    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
    
    
                        invokeInsertHook(vnode, insertedVnodeQueue, true)
                        return oldVnode
                    }
                }
                oldVnode = emptyNodeAt(oldVnode)
            }
            return vnode.elm
        }
    }
}

patchVnode 函数主要做几个判断

  • 新旧节点一致,不操作;
  • 新节点是否是文本节点,是则直接更新旧节点的文本内容为新节点的文本内容;
  • 新旧节点是否都有子节点,是则进行比较更新子节点;
  • 新节点有子节点,旧节点没有,则直接新节点插入到父节点;
  • 新节点没有子节点,旧节点有子节点,则直接删除旧节点的子节点;
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    
    
    // 新旧节点一致,不操作
    if (oldVnode === vnode) {
    
    
        return
    }
    // 将旧节点的 el 引用到新节点的 el,变化时同步变化
    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
    }
    // 新旧节点都是静态节点且 key 值一致
    // 新节点是克隆节点或 v-once 控制的节点,只需要将旧的 componentInstance 复制给新节点
    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
    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)) {
    
    
        for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
        if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 判断新节点是否是文本节点
    if (isUndef(vnode.text)) {
    
    
        // 新节点不是文本节点,且新旧节点都有子节点
        if (isDef(oldCh) && isDef(ch)) {
    
    
            // 新旧节点的子节点不一致,调用 updateChildren 
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {
    
    
            // 只有新节点有子节点,在旧节点上添加子节点
            // 因为前面 elm 已经引用了旧节点的 elm,所以直接添加
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) {
    
    
            // 旧节点有子节点,新节点没有子节点,则代表删除旧节点的子节点
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        } else if (isDef(oldVnode.text)) {
    
    
            // 旧节点的子节点是文本节点,则直接将子节点置为空字符串
            nodeOps.setTextContent(elm, '')
        }
    } else if (oldVnode.text !== vnode.text) {
    
    
        // 新旧节点都是文本节点,且内容不一致,则直接使用新节点的文本节点
        nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
    
    
        if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
}

updateChildren 函数:5 种场景判断

  • 新旧节点的 start 相同,直接 patchVnode,同时头指针 +1;
  • 新旧节点的 end 相同,直接 patchVnode,同时尾指针 -1;
  • 旧节点的 start 和新节点的 end 相同,直接 patchVnode,同时旧头指针 +1,新尾指针 -1;
  • 旧节点的 end 和新节点的 start 相同,直接 patchVnode,同时旧尾指针 -1,新头指针 +1;
  • 以上四种情况不满足时,则分两种情况:
    • 旧节点中不存在 key 值相同的节点,则 createElm 创建节点;
    • 旧节点中存在 key 值相同的节点,比较 key 相同的节点和新节点的 start,相同则直接 patchVnode;不同则 createElm 新建节点;同时新节点的头指针 +1;
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    
    
    let oldStartIdx = 0 // 旧头指针
    let newStartIdx = 0 // 新头指针
    let oldEndIdx = oldCh.length - 1 // 旧尾指针
    let newEndIdx = newCh.length - 1 // 新尾指针
    let oldStartVnode = oldCh[0] // 旧的头节点
    let oldEndVnode = oldCh[oldEndIdx] // 旧的尾节点
    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]
        } 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)) {
    
    
            // 旧的头节点和新的尾节点相同,旧头指针右移,新尾指针左移
            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)) {
    
    
            // 旧的尾节点和新的头节点相同,旧尾指针右移,新头指针左移
            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)
			
            // 尝试寻找新旧节点 key 相同的节点,这也是是否有设置 key 值的关键点
            idxInOld = isDef(newStartVnode.key)
                ? oldKeyToIdx[newStartVnode.key]
            : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

            if (isUndef(idxInOld)) {
    
    
                // 没有 key 相同的节点,新建节点
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            } else {
    
    
                // 有 key 相同的节点
                vnodeToMove = oldCh[idxInOld]
                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 {
    
    
                    // 不相同,新建一个节点
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                }
            }
            newStartVnode = newCh[++newStartIdx]
        }
 }

总结:

  • 当数据发生变化时,watcher 调用 patch 给真实 DOM 打补丁;
  • 通过 sameVnode 判断是否相同,相同则调用 patchVnode
  • patchVnode 主要操作如下:
    • 将旧节点的 el 引用到新节点的 el,变化时同步变化;
    • 新旧节点都有文本且内容不一致,用新节点的文本替换旧节点的文本;
    • 旧节点有子节点,新节点没有,则删除旧节点的子节点;
    • 新节点有子节点,旧节点没有,则创建节点添加到 el;
    • 新旧节点都有子节点,进行 updateChildren 比较更新;
  • updateChildren 主要操作如下:
    • 设置新旧 vnode 的头尾指针;
    • 比较新旧 vnode 的头尾指针,循环向中靠拢,根据比较情况调用 patchVnode 重复使用 patch 流程,不符合比较情况时,通过查找 key 相同的 vnode 分情况比较;

猜你喜欢

转载自blog.csdn.net/Ljwen_/article/details/124667861
今日推荐