和麪試官聊聊Diff___React

最近看了 vue-design 項目,寫的很棒,文中 diff 的思路全是來自該項目,這裏只是作一個學習的記錄。做者(【掘金地址】hcysunyang)已是 vue3 的 contributor 了。值得學習的一位 前端人。再次感謝👏👏👏html

我是一名前端的小學生。行文中對某些設計原理理解有誤十分歡迎你們討論指正😁😁😁,謝謝啦!固然有好的建議也謝謝提出來
在這裏插入圖片描述
(玩笑)前端

當前前端框架都有 diff算法,做用主要是處理比對虛擬Dom(Vnode),最大化複用舊節點,最後渲染爲真實 Dom,最大化下降節點建立、刪除的的開銷。vue

老樣子,原本寫一篇文章的。東西越寫越多就裂開了🤣
在這裏插入圖片描述node

【第一篇】和麪試官聊聊Diff___React(本文)
【第二篇】和麪試官聊聊Diff___vue2
【第三篇】和麪試官聊聊Diff___Vue3react

好了,不說廢話了。咱們開始吧。git


前言

vue 、react中都是組件構成,每一個組件又是標籤元素構成。github

我主要技術棧是Vue。稍微說一下vue
vue編譯會涉及到幾個過程 【參考剖析 Vue.js 內部運行機制,推薦看看】面試

parse(解析) => optimize(優化) => generate(節點生成)算法

這個diff算法是處在optimize(優化)階段的一個操做。編程

另外,各個框架的節點比對都是同級比對,即同一層級的相應子節點比對。
【圖】

本文注重的是patch過程,具體的細節和邊界就沒有考慮。

==另 外 注 意==

  • 三篇文章 diff 的講解,爲了方便展現 節點複用, 用了 children 保存內容,實際上這是不合理的,由於children不一樣還會遞歸補丁(patch)
  • diff也不是vue optimize的所有,只是其中一部分,例如compile時肯定節點類型,不一樣類型 不一樣的 mount/patch 處理方式等等。

React_diff

基本思路

先說思路,
如今好比說由若干個新老節點(preNodes / nextNodes )。

  • 先將新節點(nextNodes)與老節點(preNodes)一一比對,
  • 遇到相同的節點(本文假設key相同即相同),根據索引相對大小判斷節點是否須要移動
  • 遇到新節點,就掛載到 nextNodes 中上一個節點的前面
  • 最後將移動後的節點(newNodes)與新節點( nextNodes )比對去除多餘節點

最後的結果就是由 nextNodespreNodes 產生的 節點樹(newNodes)。

好比有以下新舊節點:

// 舊節點
const preNodes = [
  {key: "k-1", children: "<span>old1</span>"},
  {key: "k-2", children: "<span>old2</span>"},
  {key: "k-3", children: "<span>old3</span>"},
  {key: "k-4", children: "<span>old4</span>"},
  {key: "k-5", children: "<span>old5</span>"},
  {key: "k-6", children: "<span>old6</span>"},
]
//新節點,想要最後呈現的節點
const nextNodes = [
  {key: "k-11", children: "<span>11</span>"},
  {key: "k-0", children: "<span>0</span>"},
  {key: "k-5", children: "<span>5</span>"},
  {key: "k-13", children: "<span>13</span>"},
  {key: "k-1", children: "<span>1</span>"},
  {key: "k-7", children: "<span>7</span>"},
  {key: "k-16", children: "<span>16</span>"},
  {key: "k-3", children: "<span>3</span>"},
  {key: "k-15", children: "<span>15</span>"},
  {key: "k-17", children: "<span>7</span>"},
  {key: "k-4", children: "<span>4</span>"},
  {key: "k-6", children: "<span>6</span>"}
]

如上,在 preNodes 裏若是有老節點能夠複用,便用老節點替代他。指望的結果應該是
在這裏插入圖片描述

能夠看到老節點都獲得了複用~

下面就具體講解獲得最後新節點的過程。

圖例講解

  • i 是nextNodes的索引, j 是preNodes的索引,每一個nextNode都要與全部preNode節點做比對。
  • preNodes的上一個索引節點橙色標記,虛線標記當前遍歷的節點,綠色爲新增節點,j 標記的是相等的節點(若是有)

初始狀態,新生成節點(newNodes)基於老節點
在這裏插入圖片描述
i=0, lastIndex=0 (默認值),k-11preNodes 未找到, 爲新節點。插入至 0
在這裏插入圖片描述
i=1, lastIndex=0(默認值),k-0preNodes 未找到, 爲新節點。插入至 i -1 節點後面
在這裏插入圖片描述

i=2, lastIndex=4(更新後),k-5 在preNodes找到索引(j=4 > lastIndex,index更新)插入(刪除+新增),複用節點
在這裏插入圖片描述
i=3, lastIndex=4,k-13 在preNodes未找到, 爲新節點。插入至 i -1節點 後
在這裏插入圖片描述
i=4, lastIndex=4,k-1preNodes 找到索引(j=0 < lastIndex)插入(刪除+新增)至 i - 1
在這裏插入圖片描述

i=5, lastIndex=0,k-7preNodes 未找到,爲新節點。插入至 i-1 節點後
在這裏插入圖片描述

i=6, lastIndex=0,k-16preNodes 未找到,爲新節點。插入至 i -1 節點後
在這裏插入圖片描述

i=7, lastIndex=4,k-3preNodes 找到索引(j=2 < lastIndex)插入(刪除+新增)至 i - 1
在這裏插入圖片描述

i=8, lastIndex=0,k-5preNodes 未找到,爲新節點。插入至 i -1 節點後
在這裏插入圖片描述

i=9, lastIndex=0,k-17preNodes 未找到,爲新節點。插入至 i-1 節點 後
在這裏插入圖片描述i=10, lastIndex=4,k-4 在preNodes找到索引(j=3 < lastIndex)插入(刪除+新增)至 i - 1
在這裏插入圖片描述

i=11, lastIndex=6(更新後),k-6preNodes 找到索引(j=5 > lastIndex,index更新)插入(刪除+新增),複用節點
在這裏插入圖片描述
遍歷完成,清除多餘節點。
在這裏插入圖片描述
最終結果
在這裏插入圖片描述
建議仔細理解。

代碼實現

本節是代碼的具體實現,很少作講解,若是有任何疑慮建議精度圖例講解。或者留言交流,十分歡迎~~~

第一版

// React_diff()
function React_diff(){
  console.log(nextNodes.map(item => item.key));
  const newNodes = JSON.parse(JSON.stringify(preNodes));
  let lastIndex = 0;
  for(let i=0; i< nextNodes.length; i++){
    const nextNode = nextNodes[i];
    let find = false;
    for(let j=0; j< preNodes.length; j++){
      const preNode = preNodes[j];
      if(preNode.key === nextNode.key){
        find = true;
        if(j < lastIndex){ // 須要移動
          /* 
            insertBefore 效果時遇到一樣的刪除原來的再添加,
            這裏由於是數組模擬,因此須要先添加再刪除 ,
            數組處理有點不一樣,插入和刪除索引 都是從老節點找的。
            [1,2,3].splice(0,0,4) => [4,1,2,3]
          */
          const index = i > 0 ? i-1 : 0;
          const insertPos = newNodes.findIndex(node => node.key === nextNodes[index].key);
          const deleteIndex = newNodes.findIndex(node => node.key === preNode.key);

          //添加因爲 splice 是在某索引前面加。因此insertPos+1
          newNodes.splice(insertPos+1, 0, preNode)
          //刪除
          newNodes.splice(deleteIndex, 1);
        }else {
          lastIndex = j;
        }
      }
    }
    if(!find) {// 插入新節點
      const index = i > 0 ? i-1 : 0;
      const insertPos = newNodes.findIndex(node => node.key === nextNodes[index].key);
      newNodes.splice(insertPos + 1, 0, nextNode);
    }
  }
  
  for(let i = newNodes.length - 1; i>=0; i--){
    let find = false;
    for(let j =0; j<nextNodes.length; j++){
      if(nextNodes[j].key === newNodes[i].key){
        find = true;
        continue;
      }
    }
    if(!find){
      newNodes.splice(i, 1);
    }
  }
  console.log('react diff: ', newNodes);
}

優化版

優化點能夠利用key與index的作個對應關係,少一層遍歷.

function React_diff(){
  const newNodes = JSON.parse(JSON.stringify(preNodes));
  let lastIndex = 0;

  //產生nextNodes 的keyInIndexmap
  const prekeyInIndex = {};
  for(let i =0; i< preNodes.length; i++){
    prekeyInIndex[preNodes[i].key] = i;
  }

  for(let i=0; i< nextNodes.length; i++){
    const nextNode = nextNodes[i];
    let find = false;
    const j = prekeyInIndex[nextNode.key];
    if(typeof j !== 'undefined') {
      find = true;
      const preNode = preNodes[j];
      console.log(j, lastIndex);
      if(j < lastIndex){ // 須要移動
        /* 
          insertBefore 效果時遇到一樣的刪除原來的再添加,
          這裏由於是數組模擬,因此須要先添加再刪除 ,
          數組處理有點不一樣,插入和刪除索引 都是從老節點找的。
          [1,2,3].splice(0,0,4) => [4,1,2,3]
        */
        const index = i > 0 ? i-1 : 0;
        const insertPos = newNodes.findIndex(node => node.key === nextNodes[index].key);
        const deleteIndex = newNodes.findIndex(node => node.key === preNode.key);

        //添加因爲 splice 是在某索引前面加。因此insertPos+1
        newNodes.splice(insertPos+1, 0, preNode)
        //刪除
        newNodes.splice(deleteIndex, 1);
      }else {
        lastIndex = j;
      }
    }
    if(!find) {// 插入新節點
      const index = i > 0 ? i-1 : 0;
      const insertPos = newNodes.findIndex(node => node.key === nextNodes[index].key);
      newNodes.splice(insertPos + 1, 0, nextNode);
    }
  }
  
  //產生nextNodes 的keyInIndexmap
  const nextkeyInIndex = {};
  for(let i =0; i< nextNodes.length; i++){
    nextkeyInIndex[nextNodes[i].key] = i;
  }
  for(let i = newNodes.length - 1; i>=0; i--){
    const idx = nextkeyInIndex[newNodes[i].key];
    if(typeof idx === 'undefined'){
      newNodes.splice(i, 1);
    }
  }
  console.log('react diff: ', newNodes);
}

總結

本文中例子只是爲了更好理解 diff 思路, patch 過程與真實狀況還有些差別(下面爲與vue patch的一些差別。可作參考)

  • 重複節點問題。新老節點有重複節點時,本文diff函數沒處理這種狀況。
  • 僅是用數組模擬了Vnode,真實的Vnode 不止 key和children,還有更多的參數
  • 比對相同節點時,僅比對了 key, 真實其實還涉及到 class(類名) 、attrs(屬性值)、孩子節點(遞歸)等屬性比對;另外上面的children也要比對若不一樣也要遞歸遍歷
  • 插入、刪除、添加節點我用的數組。其實應該用 insertbeforedeleteadd。這些方法均是單獨封裝不能採用相對應的 Dom Api,由於 vue 不止用在瀏覽器環境
  • ...

Vue@3.2 已經出來了,React@18也快了,哎,框架學不完。仍是多看看不變的東西吧(js, 設計模式, 數據結構,算法...)

哎哎哎,,同志,看完怎麼不點贊,別看別人就說你呢,你幾個意思?
在這裏插入圖片描述


參考

站在別人肩膀能看的更遠。

【推薦】vue-design
【掘金小冊】剖析Vue.js內部運行機制
【CSDN】React、Vue2.x、Vue3.0的diff算法


以上。
在這裏插入圖片描述

相關文章
相關標籤/搜索