vue核心原理(Diff算法、虛擬dom)

核心原理&源碼

Diff 算法

這裏參考大佬文章:https://mp.weixin.qq.com/s/oAlVmZ4Hbt2VhOwFEkNEhw

diff 算法的進化

關於 diff 算法的最經典的就是 Matt Esch 的 virtual-dom,以及 snabbdom(被整合進 vue 2.0中)。

圖片

最開始出現的是 virtual-dom 這個庫,是大家好奇 React 爲什麼這麼快而搞鼓出來的。它的實現是非常學院風格,通過深度優先搜索與 in-order tree 來實現高效的 diff 。
然後是 cito.js 的橫空出世,它對今後所有虛擬 DOM 的算法都有重大影響。它採用兩端同時進行比較的算法,將 diff 速度拉高到幾個層次。
緊隨其後的是 kivi.js,在 cito.js 的基出提出兩項優化方案,使用 key 實現移動追蹤以及及基於 key 的最長自增子序列算法應用(算法複雜度 爲O(n^2))。
但這樣的 diff 算法太過複雜了,於是後來者 snabbdom 將 kivi.js 進行簡化,去掉編輯長度矩離算法,調整兩端比較算法。速度略有損失,但可讀性大大提高。再之後,就是著名的vue2.0 把sanbbdom整個庫整合掉了。

下面我們就來講講這幾個虛擬 DOM 庫 diff 算法的具體實現:

virtual-dom

virtual-dom 作爲虛擬 DOM 開天闢地的作品,採用了對 DOM 樹進行了深度優先的遍歷的方法。

圖片

體現到代碼上:(可以看成僞代碼)

<script>
    function diff(oldTree, newTree) {
        let index = 0; // 當前節點的標誌(樹形層數)
        let patches = [] // 用來記錄每個節點差異的對象
        dfsWalk(oldTree, newTree, patches, index); // 進行深度優先遍歷
        return patches;
    }

    // 對兩棵樹進行深度優先遍歷
    function dfsWalk(oldNode, newNode, patches, index) {
        if (newNode === oldNode) {
            return
        }

        const patch = { type: 'update', vNode: newNode }

        const oldChildren = oldNode.children;
        const newChildren = newNode.children;
        const oldLen = oldChildren.length;
        const newLen = newChildren.length;
        const len = oldLen > newLen ? oldLen : newLen // 取長的
        // 找到對應的子節點進行比較
        for (let i = 0; i < len; i++) {
            const oldChild = oldChildren[i];
            const newChild = newChildren[i];
            index++;
            // 相同節點進行比對
            dfsWalk(oldChild, newChild, patches, index)
            if (isArray(oldChild.children)) {
                index += oldChild.children.length
            }
        }

        if (patch) {
            patches[index] = patch
        }
    }
</script>

VDOM 節點的對比

上面代碼只是對 VDOM 進行了簡單的深度優先遍歷,在遍歷中,還需要對每個 VDOM 進行一些對比,具體分爲以下幾種情況:

  1. 舊節點不存在,插入新節點;新節點不存在,刪除舊節點
  2. 新舊節點如果都是 VNode,且新舊節點 tag 相同
    • 對比新舊節點的屬性
    • 對比新舊節點的子節點差異,通過 key 值進行重排序,key 值相同節點繼續向下遍歷
  3. 新舊節點如果都是 VText,判斷兩者文本是否發生變化
  4. 其他情況直接用新節點替代舊節點

詳細代碼加詳細註釋

<script>
    function diff(oldTree, newTree) {
        let index = 0; // 當前節點的標誌(樹形層數)
        let patches = [] // 用來記錄每個節點差異的對象
        dfsWalk(oldTree, newTree, patches, index); // 進行深度優先遍歷
        return patches;
    }

    import { isVNode, isVText, isArray } from './utils/type.js'

    // 對兩棵樹進行深度優先遍歷
    function dfsWalk(oldNode, newNode, patches, index) {
        if (newNode === oldNode) {
            return
        }

        let patch = patches[index];

        if (!oldNode) {
            // 舊節點不存在,直接插入
            // appendPatch 是用來存節點之間差異的
            patch = appendPatch(patch, {
                type: PATCH.INSERT,
                vNode: newNode,
            })
        } else if (!newNode) {
            // 新節點不存在,刪除舊節點
            patch = appendPatch(patch, {
                type: PATCH.REMOVE,
                vNode: null
            })
        } else if (isVNode(newNode)) { // 新節點是 VNode,就相當於前面寫的Element
            if (isVNode(oldNode)) { // 舊節點也是 VNode,就要比較這兩個節點的 tagName是否一致
                // 新舊節點 tagName 一致,並且 key 也一致。
                if (newNode.tagName === oldNode.tagName && newNode.key === oldNode.key) {
                    // 新老節點屬性的對比, diffProps方法就是對新舊節點自身屬性的對比
                    // 屬性如果有差異,propsPatch的長度 > 0,且差異存在 propsPatch中
                    const propsPatch = diffProps(newNode.props, oldNode.props)
                    if (propsPatch && propsPatch.length > 0) {
                        patch = appendPatch(patch, {
                            type: PATCH.PROPS, // props這個表示是節點的屬性差異
                            patches: propsPatch // 這裏存的是差異的內容
                        })
                    }
                    // 新老節點子節點的對比
                    // diffChildren 方法是專門來對比子節點的。
                    patch = diffChildren(oldNode, newNode, patches, patch, index)
                } 
            } else {
                // 舊節點不是 VNode, 新節點替換舊節點
                patch = appendPatch(patch, {
                    type: PATCH.REPLACE,
                    vNode: newNode
                })
            }
        } else if (isVText(newNode)) { // 既然新節點不是 VNode,就判斷新節點是否是文本節點
            // 舊節點不是文本節點
            if (!isText(oldNode)) {
                // 將舊節點替換成文本節點
                patch = appendPatch(patch, {
                    type: PATCH.VTEXT,
                    vNode: newNode,
                })
            } else if (newNode.text !== oldNode.text) { // 判斷兩者內容是否相等
                // 替換文本
                patch = appendPatch(patch, {
                    type: PATCH.VTEXT,
                    vNode: newNode,
                })
            } 

        }

        if (patch) {
            // 將補丁放入對應位置
            patches[index] = patch
        }

    }
</script>

屬性的對比

<script>
	function diffProps(newProps, oldProps) {
        const patches = [];
        // 將新舊屬性都淺拷貝進 props
        const props = Object.assign({}, newProps, oldProps)

        // 將props對象的鍵轉換成數組
        Object.keys(props).forEach(key => {
            // 如果新屬性裏有這個鍵,就能獲取到這個鍵的屬性值
            const newVal = newProps[key];
            // 舊屬性也一樣
            const oldVal = newProps[key];
            // 新屬性這個鍵不存在
            if (!newVal) {
                // 那就直接用舊的
                patches.push({
                    type: PATCH.REMOVE_PROP,
                    key,
                    value: oldVal,
                })
            }
            // 舊的不存在或者新的不等於舊的
            if (oldVal === undefined || newVal !== oldVal) {
                patches.push({
                    type: PATCH.SET_PROP,
                    key,
                    value: newVal,
                })
            }
        })
    }
</script>

子節點的對比

這一部分可以說是 diff 算法中,變動最多的部分,因爲前面的部分,各個庫對比的方向基本一致,而關於子節點的對比,各個倉庫都在前者基礎上不斷得進行改進。

首先需要明白,爲什麼需要改進子節點的對比方式。如果我們直接按照深度優先遍歷的方式,一個個去對比子節點,子節點的順序發生改變,那麼就會導致 diff 算法認爲所有子節點都需要進行 replace,重新將所有子節點的虛擬 DOM 轉換成真實 DOM,這種操作是十分消耗性能的。

圖片

但是,如果我們能夠找到新舊虛擬 DOM 對應的位置,然後進行移動,那麼就能夠儘量減少 DOM 的操作。

圖片

virtual-dom 在一開始就進行了這方面的嘗試,對子節點添加 key 值,通過 key 值的對比,來判斷子節點是否進行了移動。通過 key 值對比子節點是否移動的模式,被各個庫沿用,這也就是爲什麼主流的視圖庫中,子節點如果缺失 key 值,會有 warning 的原因。

圖片

具體是怎麼對比的,我們先看代碼:

<script>
	function diffChildren(oldNode, newNode, patches, patch, index) {
        const oldChildren = oldNode.children;
        // 新節點按照舊節點的順序重新排序
        const sortedSet = sortChildren(oldChildren, newNode.children)
        // 拿到新節點的子節點
        const newChildren = sortedSet.children;
        const oldLen = oldChildren.length;
        const newLen = newChildren.length;

        const len = oldLen > newLen ? oldLen : newLen
        for (let i = 0; i < len; i++) {
            let leftNode = oldChildren[i];
            let rightNode = newChildren[i];
            index++;

            if (!leftNode) {
                if (rightNode) {
                    // 舊節點不存在,新節點存在,進行插入操作
                    patch = appendPatch(patch, {
                        type: PATCH.INSERT,
                        vNode: rightNode,
                    })
                }
            } else {
                // 相同節點進行比對
                dfsWalk(leftNode, rightNode, patches, index)
            }
            if (isVNode(leftNode) && isArray(leftNode.children)) {
                index += leftNode.children.length
            }

        }

        if (sortedSet.moves) {
            // 最後進行重新排序
            patch = appendPatch(patch, {
                type: PATCH.ORDER,
                moves: sortedSet.moves,
            })
        }

        return patch
    }
</script>

這裏首先需要對新的子節點進行重排序,先進行相同節點的 diff ,最後把子節點按照新的子節點順序重新排列。

圖片

這裏有個較複雜的部分,就是對子節點的重新排序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章