React diff算法

https://zhuanlan.zhihu.com/p/...node

React diff 會幫助咱們計算出 Virtual DOM 中真正變化的部分,並只針對該部分進行實際 DOM 操做,而非從新渲染整個頁面,從而保證了每次操做更新後頁面的高效渲染,所以 Virtual DOM 與 diff 是保證 React 性能口碑的幕後推手。算法

計算一棵樹形結構轉換成另外一棵樹形結構的最少操做,是一個複雜且值得研究的問題。傳統 diff 算法經過循環遞歸對節點進行依次對比,效率低下,算法複雜度達到 O(n3),其中 n 是樹中節點的總數。app

diff 策略

  • Web UI 中 DOM 節點跨層級的移動操做特別少,能夠忽略不計。
  • 擁有相同類的兩個組件將會生成類似的樹形結構,擁有不一樣類的兩個組件將會生成不一樣的樹形結構。
  • 對於同一層級的一組子節點,它們能夠經過惟一 id 進行區分。

tree diff 經過分層求異的策略

對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。既然 DOM 節點跨層級的移動操做少到能夠忽略不計,針對這一現象,React 經過 updateDepth 對 Virtual DOM 樹進行層級控制,只會對相同顏色方框內的 DOM 節點進行比較,即同一個父節點下的全部子節點。當發現節點已經不存在,則該節點及其子節點會被徹底刪除掉,不會用於進一步的比較。這樣只須要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。dom

image

因爲 React 只會簡單的考慮同層級節點的位置變換,而對於不一樣層級的節點,只有建立和刪除操做。當出現節點跨層級移動時,並不會出現想象中的移動操做,而是以 A 爲根節點的樹被整個從新建立,這是一種影響 React 性能的操做,所以 React 官方建議不要進行 DOM 節點跨層級的操做。性能

component diff 經過相同類生成類似樹形結構,不一樣類生成不一樣樹形結構的策略

React 是基於組件構建應用的,對於組件間的比較所採起的策略也是簡潔高效。優化

  • 若是是同一類型的組件,按照原策略繼續比較 virtual DOM tree。
  • 若是不是,則將該組件判斷爲 dirty component,從而替換整個組件下的全部子節點。
  • 對於同一類型的組件,有可能其 Virtual DOM 沒有任何變化,若是可以確切的知道這點那能夠節省大量的 diff 運算時間,所以 React 容許用戶經過 shouldComponentUpdate() 來判斷該組件是否須要進行 diff。

image

以下圖,當 component D 改變爲 component G 時,即便這兩個 component 結構類似,一旦 React 判斷 D 和 G 是不一樣類型的組件,就不會比較兩者的結構,而是直接刪除 component D,從新建立 component G 以及其子節點。spa

element diff 經過設置惟一 key的策略

當節點處於同一層級時,React diff 提供了三種節點操做,分別爲:INSERT_MARKUP(插入)、MOVE_EXISTING(移動)和 REMOVE_NODE(刪除)。code

  • INSERT_MARKUP,新的 component 類型不在老集合裏, 便是全新的節點,須要對新節點執行插入操做。
  • MOVE_EXISTING,在老集合有新 component 類型,且 element 是可更新的類型,generateComponentChildren 已調用 receiveComponent,這種狀況下 prevChild=nextChild,就須要作移動操做,能夠複用之前的 DOM 節點。
  • REMOVE_NODE,老 component 類型,在新集合裏也有,但對應的 element 不一樣則不能直接複用和更新,須要執行刪除操做,或者老 component 不在新集合裏的,也須要執行刪除操做。

容許開發者對同一層級的同組子節點,添加惟一 key 進行區分,雖然只是小小的改動,性能上卻發生了翻天覆地的變化!component

image

先對新集合的節點進行循環遍歷,for (name in nextChildren),經過惟一 key 能夠判斷新老集合中是否存在相同的節點,if (prevChild === nextChild),若是存在相同節點,則進行移動操做,但在移動前須要將當前節點在老集合中的位置與 lastIndex 進行比較,if (child._mountIndex < lastIndex),則進行節點移動操做,不然不執行該操做。==這是一種順序優化手段,lastIndex 一直在更新,表示訪問過的節點在老集合中最右的位置(即最大的位置)==,若是新集合中當前訪問的節點比 lastIndex 大,說明當前訪問節點在老集合中就比上一個節點位置靠後,則該節點不會影響其餘節點的位置,所以不用添加到差別隊列中,即不執行移動操做,只有當訪問的節點比 lastIndex 小時,才須要進行移動操做。blog

image

總結

  • React 經過制定大膽的 diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
  • React 經過分層求異的策略,對 tree diff 進行算法優化;
  • React 經過相同類生成類似樹形結構,不一樣類生成不一樣樹形結構的策略,對 component diff 進行算法優化;
  • React 經過設置惟一 key的策略,對 element diff 進行算法優化;
  • 建議,在開發組件時,保持穩定的 DOM 結構會有助於性能的提高;
  • 建議,在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。

Diff Algorithm

  • Level by Level
  • List
  • Components

Event Delegation

Rendering

  • Batching
  • Sub-tree Rendering
  • Selective Sub-tree Rendering

diff策略

oldVdom不存在時,將newVdom生成的dom添加到父元素
newVdom不存在時,將newVdom對應index的真實dom刪除
oldVdom, newVdom 的根節點不一致時,直接將oldVdom替換爲newVdom
若上述都不知足,則說明兩個vdom的根節點是一致的, 而後遞歸調用 diff & patch 方法

function h(type, props, ...children) {
    return {type, props, children};
}

function createElement(node) {
    if (typeof node === 'string') {
        return document.createTextNode(node);
    }
    const $el = document.createElement(node.type);
    node.children
        .map(createElement)
        .forEach($el.appendChild.bind($el));
    return $el;
}

function changed(node1, node2) {
    return typeof node1 !== typeof node2 ||
        typeof node1 === 'string' && node1 !== node2 ||
        node1.type !== node2.type
}

function updateElement($parent, newNode, oldNode, index = 0) {
    console.log(Array.from(arguments))
    // console.log(newNode)
    // console.log(newNode)

    if (!oldNode) {
        $parent.appendChild(
            createElement(newNode)
        );
    } else if (!newNode) {
        $parent.removeChild(
            $parent.childNodes[index]
        );
    } else if (changed(newNode, oldNode)) {
        console.log('if go changed')
        console.log(newNode, oldNode)
        $parent.replaceChild(
            createElement(newNode),
            $parent.childNodes[index]
        );
    } else if (newNode.type) {
        console.log('test if go last if')
        const newLength = newNode.children.length;
        const oldLength = oldNode.children.length;
        for (let i = 0; i < newLength || i < oldLength; i++) {
            updateElement(
                $parent.childNodes[index],
                newNode.children[i],
                oldNode.children[i],
                i
            );
        }
    }
}

// ---------------------------------------------------------------------

// let a = (
//     <ul>
//         <li>item 1</li>
//         <li>item 2</li>
//     </ul>
// );
//
// let b = (
//     <ul>
//         <li>item 1</li>
//         <li>hello!</li>
//     </ul>
// );

let a = h('ul', {}, h('li', {}, 'item1'), h('li', {}, 'item2'))
let b = h('ul', {}, h('li', {}, 'item1'), h('li', {}, 'hello!'))

const $root = document.getElementById('root');
const $reload = document.getElementById('reload');

updateElement($root, a);
$reload.addEventListener('click', () => {
    updateElement($root, b, a);
});



// 4. vdom diffs && patch
//index Virtual DOM對應處於真實DOM中的第幾個子節點
function btsPatch(parentDomNode, oldVdom, newVdom, index=0) {
    if(!oldVdom) parentDomNode.appendChild(applyVDom(newVdom));
    if(!newVdom) { 
        if(parentDomNode.childNodes)
            parentDomNode.removeChild(parentDomNode.childNodes[index]);
    }
    if(typeof oldVdom != typeof newVdom ||
      (typeof oldVdom == 'string' && oldVdom != newVdom) ||
      (typeof oldVdom == 'object' && oldVdom.name != newVdom.name)
    ) {
        if(parentDomNode.childNodes && parentDomNode.childNodes[index])
            parentDomNode.removeChild(parentDomNode.childNodes[index]);
        parentDomNode.appendChild(applyVDom(newVdom));
    } else {
        if( typeof oldVdom == 'object' ) {
            let count = Math.max(oldVdom.children.length, newVdom.children.length);
            if(count > 0) {
                for(let i=0; i < count; i++) {
                    btsPatch(parentDomNode.childNodes[index], oldVdom.children[i], newVdom.children[i], i);
                }
            }
        }
    }
    return // done bts or same string or no children
}
相關文章
相關標籤/搜索