React 的調和算法Diffing 算法策略詳解

算法策略

React的調和算法,主要發生在render階段,調和算法並不是一個特定的算法函數,而是指在調和過程中,為提高構建workInProcess樹的性能,以及Dom樹更新的性能,而采用的一種策略,又稱diffing算法。 在React 的官網上描述“Diffing” 算法時,提到瞭“diffing two trees”,但是在源碼實現時,並不是創建好兩棵樹後,再從上往下的diffing這兩棵樹。這個diffing發生在搭建子節點時, 實際是新生成的ReactElement 與 current樹上fibe節點的diffing。 為瞭將diffing算法的時間復雜度控制在O(n)(樹diff的時間復雜度涉及到樹的編輯距離,可以看這裡), 采用瞭如下策略:

隻比較同層級的節點,(貌似這一點沒有在官網中提到)

對於單節點比較,如果當前節點type 和 key 不相同,不再比較其下子節點,直接刪掉該節點及其下整棵子樹,根據ReactElement重新生成節點樹。因為React認為不同類型的組件生成的樹形結構不一樣,不必復用。

如果子節點是數組,可根據唯一的key值定位節點進行比較,這樣即使子節點順序發生變化,也可以根據key值進行復用。

值得註意的是,所有節點的diffing都會比較key,key 默認值為null。若是沒有設置,則null是恒等於null的,認為key是相同的。

單節點diffing

單個節點進行diffing時,會diffing兩組屬性:fibe.elementType vs ReactElement.type , fibe.key vs ReactElement.key, 如果這兩組屬性都相等,數據結構的物理空間會有如下復用邏輯(詳見源碼reconcileSingleElement函數):

  1. 如果current fibe 存在 alternate 節點,(這意味著這個fibe節點之前參與過調和),則復用該alternate節點的物理空間;否則需要clone current fibe節點,占用新的物理空間
  2. 對應的instance 或者 Dom 會被復用
  3. current fibe 下的child樹也會被直接掛載過來(下一步遞歸子節點時,會被使用)

如果兩組屬性有一個不相等:

  • fibe 根據ReactElement重新創建
  • 對應的instance和Dom 也會重建
  • child 樹全部刪除。

如果生成的ReactElement 是單節點,但是對應的current樹上是多節點時,會從逐一查找有沒有匹配的,找到匹配的,其他的都刪除;找不到,全部刪除。例如Couter組件有如下邏輯:當點擊次數大於10時,隱藏點擊按鈕。當到達第10次時,span節點會被復用。

class Counter extends React.Component{
    state={
        count:0
    }
    addCount = ()=>{
        const count = this.state.count+1;
        this.setState({count})
    }
    componentDidUpdate(){
        console.log("updated")
    }
    render(){
        return this.state.count < 10 ? [
            <button onClick={this.addCount}>點擊</button>
            <span>點擊次數:{this.state.count}</span>
        ]:<span>點擊次數:{this.state.count}</span>;
    }
}

數組節點diffing

數組節點進行diffing時,流程比較復雜:(源碼見reconcileChildrenArray函數)

中間有兩次重要的遍歷,第一次按index遍歷,新舊節點依次比較,遇到key值不匹配的立即中斷遍歷。 第二次遍歷,對剩下的舊節點建立 “key – 節點”的Map表,遍歷剩下的新節點,按key值從表中查找舊節點進行比較。以下是三種case下,新舊節點匹配比較的情況。

key值的使用要求

從單節點和數組節點的diffing上看,key值主要是為瞭減少新建。為瞭保證diffing時新建舊節點能匹配上,key值使用時有如下註意:

  1. 得穩定,如果key值每次都變化(比如使用瞭隨機數),diffing時,新舊節全部匹配不上,將會引起大量的新建;
  2. 必須得唯一,如果key值不唯一,在建立“key – 節點”的Map表時,會遺漏和錯亂,導致頁面更新錯誤。

對於單節點,diffing時key值也會用到,不要認為其沒用隨便亂設置。 也有一些不規范的用法,對單節點使用key值來實現組件的銷毀和重建,但這種用法是不符合React的設計理念的。

例如,有一個日志組件,內部拉取日志數據,對外部沒有數據依賴。但是在需求迭代時,在同頁面增加瞭審批操作,審批後,後端會新增一條日志數據,這時候會希望前端頁面實時的展示新增的日志數據,會需要觸發日志組件重新拉取數據。如果不使用向上提升數據的方式來解決問題,可以給該組件指定一個隨機的key,當審批操作完成時,修改key值,則組件就會重新構建。但這種方式的開銷較高,整個組件樹都會銷毀重建。可以采用其他解決方案代替,例如可以給該組件指定ref,當審批完成後,通過ref調用該組件的刷新函數,重新獲取數據,更新組件。

對於數組節點,key值主要是在子節點位置發生大的錯位時,會起到關鍵作用。 對於普通的末尾新增,和末尾刪除,第一次index遍歷就可以匹配完,不會進入第二次key map的遍歷。 因為key值對“穩定”和“唯一”性這兩個要求,一般前端會需要後端對list數據提供一個唯一標識,對於聚合接口,會給後端增加額外工作,這種情況,可以先瞭解數據的變化特性,再決定是否真的需要設置key。

對於上面的日志組件,日志是一個列表,新增的日志,都是插在第一行,如果不設置key,基本上所有的子節點都要更新。如果設置key,就需要後端服務為每一條日志增加“永遠”惟一的標識符,例如id。曾經經歷過一次因為key值的唯一性變化,導致數據更新時頁面展示錯誤的案例。

到此這篇關於React 的調和算法(Diffing 算法)的文章就介紹到這瞭,更多相關React Diffing 算法內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: