react 多節(jié)點 diff 簡易實現(xiàn)

Diff 的目的

react 是一個數(shù)據(jù)驅動的框架,通過將數(shù)據(jù)與 UI 關聯(lián)起來達到數(shù)據(jù)更新時同時更新 UI 更新的目的代承。對于 react web app 來說安岂,數(shù)據(jù)的變動最終會轉化為 dom 的變化褂始。當然 react 并不會對 dom 進行直接比較景埃,而是對比變化前的 fiber。對 fiber 的 diff 最終會反映到 dom 上粱哼。

先假設在 fiber 變化時不使用 diff 算法季二,即一旦 fiber 改變則刪除變化前的所有 fiber 并插入變化后的 fiber 。這種方法雖然簡便揭措,但存在性能問題胯舷,因為 dom 的刪除和創(chuàng)建都需要耗費時間。例如蜂筹,fiber 從 a, b, c 變?yōu)?a, c, b需纳。只需要將 b 插入到 c 之后即可芦倒,無需創(chuàng)建任何 fiber 艺挪。因此,需要一種方法來標記元素的變更兵扬,這就是 diff 算法麻裳。

如果變化后都存在多個元素,則屬于多節(jié)點的 diff器钟。多節(jié)點的 fiber diff 對于每一個 fiber 實際只存在兩種情況:

  1. Placement: 需要移動某個 fiber 的位置或插入一個新 fiber
  2. Deletion: 需要刪除某個 fiber

為什么移動或新增 dom 都屬于同一種情況津坑,因為 react 實際上最終會調用 Node.insertBefore() 來進行 placement 操作,其定義如下:

Node.insertBefore() 方法在參考節(jié)點之前插入一個擁有指定父節(jié)點的子節(jié)點傲霸。 如果給定的子節(jié)點是對文檔中現(xiàn)有節(jié)點的引用 疆瑰,insertBefore() 會將其從當前位置移動到新位置 (在將節(jié)點附加到其他節(jié)點之前眉反,不需要從其父節(jié)點刪除該節(jié)點)。

因此 react 并不關心該 fiber 是移動(已經(jīng)存在)還是新增(不存在需要創(chuàng)建)穆役。例如 fiber 從 a, b, c, d 變?yōu)?a, c, b,d寸五,那么 react 會將 b 這個 fiber 標記為 Placement。其余 fiber 不變耿币。在最終進行 dom 變化時調用 parent.insertBefore(d, b)梳杏。因此 diff 的目的并不是要嚴格的找出 fiber 從哪個位置移動到哪個位置,只需要得出哪些需要刪除淹接,哪些需要 Placement 即可十性。

算法

假設存在 now 以及 before 兩個 fiber 集合。為了簡化場景塑悼,認為 now 中的 fiber 在 before 中都存在劲适。這時候問題可以轉換為 如何移動 before 中的元素將其轉換為 now。react處理辦法為 右移 before 中的部分 fiber 將其轉換為 now厢蒜。例如减响,before 以及 after 中 key 的順序為:

before: a, b, c
now: a, c, b

那么標記 b 為 Placement 即可。對于這個任務郭怪,我們將 上一個位置不變的元素在 now 中的位置記為 lastKeepIndex支示,當遍歷 now 數(shù)組中的每個 fiber 時,如果該 fiber 在 before 數(shù)組中存在鄙才,且 beforeIndex < lastKeepIndex 颂鸿。則說明當前所遍歷到得 fiber 在:

  • 變動前: 位于 lastKeepIndex 所標記的 fiber 的左邊。
  • 變動后: 位于 lastKeepIndex 所標記的右邊攒庵,因為當前 fiber 為遍歷到的位置為最靠右邊的 fiber嘴纺。

這就意味這這個 fiber 是需要移動的。如果不滿足這個條件浓冒,則需要該 fiber 相對 lastKeepIndex 所標記的 fiber 位置沒有變動栽渴,無需改變。
當然稳懒,實際上不可能 now 中的 fiber 在 before 中都能找到闲擦。但這種同樣直接標記為 Placement 即可。同時在 before 中卻不在 now 中的需要元素標記為 Deletion场梆。為了方便這里我們定義 4 種類型的 Diff:

type Key = string | number

interface FiberNode {
    'key': Key
}

// Move 與 Insert 實際上都為 Placement墅冷,這里為了方便說明將其分為兩種
type Diff = 'Move'|'Insert'|'Deletion'|'None'

整個 diff 的邏輯為:

function diff(before: FiberNode[], now: FiberNode[]): [Key, Diff][]{
    // 將 fiber 轉換為 [key, index] 方式供后續(xù)使用
    const beforeMap = new Map<Key, number>(before.map((n, i) => [n.key, i]));
    let lastKeepIndex = -1;
    const addOrmove = now.map(n => {
        const beforeIndex = beforeMap.get(n.key)
        let mutation: Diff = null;
        // 如果這個 fiber 在 before 中不存在,則直接標記為 Insert
        if(beforeIndex === undefined){ 
            mutation = 'Insert'
        }else{
            // 刪除可以復用的 fiber或油,遍歷完成后剩下的則為需要標記刪除的 fiber
            beforeMap.delete(n.key); 
            // 滿足條件寞忿,表明該 fiber 需要移動
            if(beforeIndex < lastKeepIndex){
                mutation = 'Move';
            }else{
                // 否則不需要移動,該 beforeIndex 作為新的 lastKeepIndex 
                lastKeepIndex = beforeIndex;
                mutation = 'None'
            }
        }
        return [n.key, mutation] as [Key, Diff]
    })
    // 標記需要刪除的 fiber
    const deletion = Array.from(beforeMap.entries()).map(n => [n[0], 'Deletion'] as [Key, Diff])
    return [...addOrmove, ...deletion]
}
const before: FiberNode[] = [
    {key: 'a'},
    {key: 'b'},
    {key: 'c'},
    {key: 'd'},
    {key: 'x'},
]

const after: FiberNode[] = [
    {key: 'f'},
    {key: 'c'},
    {key: 'b'},
    {key: 'e'},
    {key: 'a'},
    {key: 'd'}
]
console.info(diff(before, after))
[ [ 'f', 'Insert' ],
  [ 'c', 'None' ],
  [ 'b', 'Move' ],
  [ 'e', 'Insert' ],
  [ 'a', 'Move' ],
  [ 'd', 'None' ],
  [ 'x', 'Deletion' ] ]

如何應用到真實 dom

在得到 diff 的結果后顶岸,react 通過兩個 dom 操作函數(shù)來將 diff 應用到真實的 dom:

  1. Node.insertBefore()
  2. Node.appendChild()

第一個函數(shù)對應于變化后需要進行 Placement 有兄弟節(jié)點的情況腔彰,例如 fiber 從 a,b,c,d 變化為 a,c,b,d叫编。此時 b 被標記為 Placement。react 會找到變化后它的第一個不需要變動的兄弟節(jié)點即為 d霹抛,并調用 parent.insertBefore(d, b) 宵溅。完成后真實的 dom 就從 a,b,c,d 變成 a,c,b,d。

第二個函數(shù)對應于變化后需要進行 Placement 不存在兄弟節(jié)點的情況上炎,例如 fiber 從 a,b,c 變化為 a,c,b 此時 b 被標記為 Placement恃逻,但其不存在兄弟節(jié)點。react 會調用 parent.appendChild(b) 藕施。完成后真實的 dom 就從 a,b,c 變成 a,c,b寇损。

當然,真實的情況比這要更復雜裳食。因此插入 dom 必定要先找到 fiber 樹中真正的 dom 節(jié)點矛市。而 fiber 樹實際上是用戶自定義組件 fiber 以及真實 dom fiber 組合在一起的,如何找到真實的兄弟 dom 節(jié)點對應的 fiber 也是一個比較復雜的任務诲祸。

一點擴展

react 通過 diff 算法來進行性能優(yōu)化浊吏,減少 dom 的創(chuàng)建和刪除。那么 react 采用的優(yōu)化是否為 最優(yōu)化呢救氯?答案是:否找田。例如存在這樣一個特殊的例子:

before: 1, 2, 3, ..., 999
now: 999, 1, 2, 3, ..., 998

由于 react diff 算法的局限,這里需要將 1 從 998 移動到 999 之后着憨,但實際上我們一眼就能看出最簡單的方法是將 999 移動到 1 之前墩衙。這也就是最近很多框架開始使用最長上升子序列來優(yōu)化 diff 算法的原因。那么問題來了甲抖,你知道為什么這里 react 需要移動 998 個元素漆改,或者說為什么最長上升子序列可以解決整個問題嗎?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市准谚,隨后出現(xiàn)的幾起案子挫剑,更是在濱河造成了極大的恐慌,老刑警劉巖柱衔,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件樊破,死亡現(xiàn)場離奇詭異,居然都是意外死亡秀存,警方通過查閱死者的電腦和手機捶码,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來或链,“玉大人,你說我怎么就攤上這事档押“难危” “怎么了祈纯?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長叼耙。 經(jīng)常有香客問我腕窥,道長,這世上最難降的妖魔是什么筛婉? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任簇爆,我火速辦了婚禮,結果婚禮上爽撒,老公的妹妹穿的比我還像新娘入蛆。我一直安慰自己,他們只是感情好硕勿,可當我...
    茶點故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布哨毁。 她就那樣靜靜地躺著,像睡著了一般源武。 火紅的嫁衣襯著肌膚如雪扼褪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天粱栖,我揣著相機與錄音话浇,去河邊找鬼。 笑死闹究,一個胖子當著我的面吹牛凳枝,可吹牛的內容都是我干的。 我是一名探鬼主播跋核,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼岖瑰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了砂代?” 一聲冷哼從身側響起蹋订,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刻伊,沒想到半個月后露戒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡捶箱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年智什,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丁屎。...
    茶點故事閱讀 38,697評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡荠锭,死狀恐怖,靈堂內的尸體忽然破棺而出晨川,到底是詐尸還是另有隱情证九,我是刑警寧澤删豺,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站愧怜,受9級特大地震影響呀页,放射性物質發(fā)生泄漏。R本人自食惡果不足惜拥坛,卻給世界環(huán)境...
    茶點故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一蓬蝶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猜惋,春花似錦丸氛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至梨撞,卻和暖如春雹洗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卧波。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工时肿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人港粱。 一個月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓螃成,卻偏偏與公主長得像,于是被迫代替她去往敵國和親查坪。 傳聞我的和親對象是個殘疾皇子寸宏,可洞房花燭夜當晚...
    茶點故事閱讀 43,587評論 2 350

推薦閱讀更多精彩內容