關於React Hooks和Immutable性能優化的實踐,我寫了一本掘金小冊

最近,個人第一本小冊《React Hooks 與 Immutable 數據流實戰》在掘金成功上線。各位期待的粉絲朋友久等了,兩個月以前的計劃一直拖到了如今,也常常在 GitHub 的 issue 區也能感覺到你們焦急的心情,實在很是抱歉,不過萬幸的是,它終於成功地問世了。前端

上線了不到 5 天,沒有任何推文介紹的狀況下,銷量已經超過 400,這個是我萬萬沒想到的,不過這也側面反映了各位掘友對個人信任。在後臺大概看了一下 ID 名單,其中不乏熟悉的面孔,但更多的是幾乎沒什麼印象甚至徹底陌生的 ID,確實,回頭看看在掘金這些日子的成長,寫做思考掙扎的過程是極其痛苦的,但正是由於你偶然看到了文章,不經意點了贊、給了一些反饋,才讓我有足夠的鬥志和毅力堅持下去。可能咱們從未謀面,甚至互相連微信都沒有,但就恰恰在一個叫"掘金"的地方,我收到了來自一個陌生人的承認,這種感受從未有過,也是一直激勵我不斷堅持的動力。各位不管是期待已久仍是偶爾打開這篇文章,請讓我很是真誠地說上一聲: 很是感謝!vue

回到小冊自己,目前已經有很多的小夥伴加入了學習。儘管如此,我想我仍然有必要正式地介紹一下這本小冊,由於我以爲這是做爲小冊做者的責任所在。算法

緣起

小冊自己的性質算是一個項目教程,那爲何我要去作這樣一個項目?redux

其實說來也挺可笑的,我僅僅只是想作一個精緻的項目罷了。記得慕課網的名師七月曾經說過一句話: 技術這東西其實很純粹,最後無非兩點:一是打工賺錢,二是作本身想作的事情。而我後來所作的事情,剛好印證了後者。不少時候把事情作成,作成 60 分,是相對輕鬆且常人所能及的,可是要作到 90 分甚至更高,每每須要異常的刻苦,甚至須要恰當的機遇和天賦。這也是爲何相似題材的項目網上一大堆,我仍然堅持要作這個項目的緣由。我想要靠本身獨立作完成一個項目,它必須足夠的精緻,同時不是爲了應付任何人。數組

接着,我試着去整合以前一段時間學到的知識,打算用 React 來搭配Immutable(不可變)數據,而且用上 React 界熾手可熱的hooks來做爲整個項目的基礎技術棧。微信

爲何要用 hooks ?

我想說,React Hooks現在能夠說是前端界"當紅小生", 因其API簡潔性、邏輯複用性等特性逐漸被開發者所應用,vue3.0也是採用相似的Function Based的模式,所以學習React Hooks也是將來的大趨勢。在這裏我也不想再重複都xxx年了,再不學xxx就要被淘汰了之類販賣焦慮的話,其實並無什麼技術是必需要學的,若是它足夠好,我願意將它分享給各位,讓更多的人享受到其帶來的便利和效率上的提高。對於hooks而言,做爲一個深度使用過的玩家,我以爲我是很是樂意給你們來分享的。而經過一個具體的項目來實踐、應用hooks特性,我以爲比干啃文檔要強太多,而且在實踐的過程當中會遇到一些坑,經過坑驅動來學習,能夠加深咱們對於hooks原理的理解。數據結構

爲何用 Immutable 數據?

這就比較複雜了。我想我首先得介紹一下 React 的渲染機制——Reconciliation 過程 (不少人翻譯成 "一致化處理過程",我的以爲不太貼切,直譯爲 "協調" 反而更好,且看下面分解)。閉包

渲染機制

如上圖所示,React 採用的是虛擬 DOM (即 VDOM ),每次屬性 (props) 和狀態 (state) 發生變化的時候,render 函數返回不一樣的元素樹,React 會檢測當前返回的元素樹和上次渲染的元素樹以前的差別,而後針對差別的地方進行更新操做,最後渲染爲真實 DOM,這就是整個 Reconciliation 過程,其核心就是進行新舊 DOM 樹對比的 diff 算法。框架

爲了得到更優秀的性能,首當其衝的工做即是 減小 diff 的過程,那麼在保證應該更新的節點可以獲得更新的前提下,這個 diff 的過程如何來避免呢?函數

答案是利用 shouldComponentUpdate 這個聲明周期函數。這個函數作了什麼事情呢?

默認的 shouldComponentUpdate 會在 props 和 state 發生變化時返回 true, 表示組件會從新渲染,從而調用 render 函數,進行新舊 DOM 樹的 diff 比對。可是咱們能夠在這個生命週期函數裏面作一些判斷,而後返回一個布爾值,而且返回 true 表示即將更新當前組件,false 則不更新當前組件。換句話說,咱們能夠經過 shouldComponentUpdate 控制是否發生 VDOM 樹的 diff 過程。

關鍵的知識點已經作好了鋪墊。如今咱們以 React 官方的一個圖爲例,完整地分析一下 Reconciliation 的流程:

SCU 即 shouldComponentUpdate 的簡寫,圖中的紅色節點表示 shouldComponentUpdate 函數返回 true ,須要調用 render 方法,進行新舊 VDOM 樹的 diff 過程,綠色節點表示此函數返回 false ,不須要進行 DOM 樹的更新。

從 C1 開始,C1 爲紅色節點,shouldComponentUpdate 返回 true,須要進行進一步的新舊 VDOM 樹的比對,假設如今兩棵樹上的 C1節點類型相同,則遞歸進入下一層節點的比較,首先進入 C2,綠色節點,表示 SCU 返回 false,不須要對 C2 的 VDOM 節點進行比對,同時 C2 下面全部的後代節點 都不須要比對。

如今進入 C3,C3 爲紅色節點,表示 SCU 爲 true,須要在該節點上進行比對,假設兩棵樹的 C3 節點類型相同,則繼續進入到下一層的比對中。其 r 中 C6 爲紅色節點,進行相應的 diff 操做,C七、C8 都爲綠色節點,都不須要更新。

固然可能你會有疑問,上面都是在 diff 的時候假設節點類型相同,那若是節點類型不相同的時候會怎樣呢?這裏 React 的作法很是簡單粗暴,直接將 原 VDOM 樹上該節點以及該節點下全部的後代節點 所有刪除,而後替換爲新 VDOM 樹上同一位置的節點,固然這個節點的後代節點也全都跟着過來了。這屬於 diff 算法的實現細節,咱們在文末的彩蛋中會對於 diff 更全面和細緻的拆解:)

所以咱們能夠發現,若是可以合理利用 shouldComponentUpdate,從而能避免沒必要要的 Reconciliation 過程,使得應用性能能夠更加優秀。

通常 shouldComponentUpdate 會比較 props 和 state 中的屬性是否發生改變 (淺比較) 來斷定是否返回 true,從而觸發 Reconciliation 過程。典型的應用就是 React 中推出的 PureComponent 這個 API,會在 props 或者 state 改變時對二者的數據進行淺層比較。

可是這個項目全面擁抱函數式組件,再也不用類組件了,所以 shouldComponentUpdate 就不能再用了。用了函數組件後,是否是就沒有了淺比較的方案了呢?並非。React 爲函數組件提供了一個 memo 方法,它和 PureComponent 在數據比對上惟一的區別就在於 只進行了 props 的淺比較,由於函數組件是沒有 state 的。並且它的用法很簡單,直接將函數傳入 memo 中導出便可。形如:

function Home () {
    //xxx
} 
export default memo (Home);
複製代碼

這也就解釋了爲何咱們須要用在每一個組件導出時都要加 memo 包裹。

如今就有了一系列的優化方案了。

優化方案一:PureComponent (memo) 進行淺層比較

上一節我埋下了一個伏筆,就是 PureComponent 或者 memo 將會進行新舊數據的淺層比對。你可能會比較好奇,淺層比較是怎麼比較的呢?口說無憑,我以爲讓你們直觀地感覺一下比較重要,因此我暫且扒出 PureComponent 淺比較部分的核心源碼讓你們體會一下,你們不用緊張,其實邏輯很是簡單。

function shallowEqual (objA: mixed, objB: mixed): boolean {
  // 下面的 is 至關於 === 的功能,只是對 + 0 和 - 0,以及 NaN 和 NaN 的狀況進行了特殊處理
  // 第一關:基礎數據類型直接比較出結果
  if (is (objA, objB)) {
    return true;
  }
  // 第二關:只要有一個不是對象數據類型就返回 false
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // 第三關:在這裏已經能夠保證兩個都是對象數據類型,比較二者的屬性數量
  const keysA = Object.keys (objA);
  const keysB = Object.keys (objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // 第四關:比較二者的屬性是否相等,值是否相等
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call (objB, keysA [i]) ||
      !is (objA [keysA [i]], objB [keysA [i]])
    ) {
      return false;
    }
  }

  return true;
}
複製代碼

從我寫的註釋能夠看出,在這裏開啓了四道關卡,但終究仍是淺層比較。在下面的狀況會判斷失靈。

state: {a: ["1"]} -> state: {a: ["1", "2"]}
複製代碼

其實 a 數組已經改變了,可是淺層比較會表示沒有改變,由於數組的引用沒有變。看到沒有?一旦屬性的值爲引用類型的時候淺比較就失靈了。

這就是這種方式最大的弊端,因爲 JS 引用賦值的緣由,這種方式僅僅適用於無狀態組件或者狀態數據很是簡單的組件,對於大量的應用型組件,它是無能爲力的。

優化方案二:shouldComponentUpdate 中進行深層比對

爲了解決方案一帶來的問題,咱們如今不作淺層比對了,咱們把 props 中全部的屬性和值進行遞歸比對。

咱們把上面淺層比對的代碼進行一些魔改:

function deepEqual (objA: mixed, objB: mixed): boolean {
  // 下面的 is 至關於 === 的功能,只是對 + 0 和 - 0,以及 NaN 和 NaN 的狀況進行了特殊處理
  // 第一關:保證二者都是基本數據類型。基礎數據類型直接比較出結果。
  // 對象類型咱就不比了
  if (objA == null && objB == null) return true;
  if (typeof objA !== 'object' &&
      typeof objB !== 'object' &&
      is (objA, objB)) {
    return true;
  }
  // 第二關:只要有一個不是對象數據類型就返回 false
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // 第三關:在這裏已經能夠保證兩個都是對象數據類型,比較二者的屬性數量
  const keysA = Object.keys (objA);
  const keysB = Object.keys (objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // 第四關:比較二者的屬性是否相等,值是否相等
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call (objB, keysA [i]) ||
      !is (objA [keysA [i]], objB [keysA [i]])
    ) {
      return false;
    } else {
        if (!deepEqual (objA [keysA [i]], objB [keysA [i]])){
            return false;
        }
    }
  }

  return true;
}
複製代碼

當訪問到對象的屬性值的時候,將屬性值再進行遞歸比對,這樣就達到了深層比對的效果。可是想一想一種極端的狀況,就是在屬性有一萬條的時候,只有最後一個屬性發生了變化,那咱們就不得已將一萬條屬性都遍歷。這是很是浪費性能的。

優化方案 3: immutable 數據結構 + SCU (memo) 淺層比對

回到問題的本質,不管是直接用淺層比對,仍是進行深層比對,咱們最終是想z知道組件的 props (或 state) 數據有無發生改變。

在這樣的條件下,immutable 數據應運而生。

什麼是 immutable 數據?它有什麼優點?

immutable 數據一種利用結構共享造成的持久化數據結構,一旦有部分被修改,那麼將會返回一個全新的對象,而且原來相同的節點會直接共享。

具體點來講,immutable 對象數據內部採用是多叉樹的結構,凡有節點被改變,那麼它和與它相關的全部上級節點都更新。

用一張動圖來模擬一下這個過程:

是吧!只更新了父節點,比直接比對全部的屬性簡直強太多,而且更新後返回了一個全新的引用,即便是淺比對也能感知到數據的改變。

所以,採用 immutable 既可以最大效率地更新數據結構,又可以和現有的 PureComponent (memo) 順利對接,感知到狀態的變化,是提升 React 渲染性能的極佳方案。

不過有一說一,immutable 也有一些被部分開發者吐槽的點,首先是 immutable 對象和 JS 對象要注意轉換,不能混用,這個你們注意適當的時候調用 toJS 或者 fromJS 便可,問題並不大。

其次就是對於 immutable API 的學習成本的爭議。我以爲這個問題見仁見智吧,個人觀點是:若是你目前沉溺在已經運用得很是熟練的技術棧當中,不說深刻學習新技術,連新的 API 都懶得學,我以爲對我的成長來講是一個不太好的徵兆。

學習目標

估計有同窗看完上面的還不過癮,追問道:"學完這個有什麼用啊?"如今就來好好梳理一下,學完這本小冊能夠達到的效果和目標:

  1. 熟練使用React Hooks進行業務開發,理解哪些場景產生閉包陷阱,如何避免掉坑。
  2. 手寫近6000行代碼,封裝13個基礎UI組件、12個業務組件,完全掌握React + Redux的工程化編碼的全流程。
  3. 封裝經常使用的移動端組件,實現常見的需求,如封裝滾動組件實現圖片懶加載實現上拉/下拉刷新的功能、實現防抖功能、實現組件代碼分割(CodeSpliting)等。
  4. 擁有實現前端複雜交互的實際項目經驗,提高本身的內功,好比開發播放器內核就是其中一個很大的挑戰。
  5. 掌握CSS中的諸多技巧,提高本身的CSS能力,不管佈局仍是動畫,都有至關多的實踐和探索,未使用任何UI框架,樣式代碼獨立實現。
  6. 完全理解redux原理,並可以獨立開發redux的中間件。

小冊展望

小冊上線後,我也陸陸續續聽到了各位掘友的反饋,有對文章進行勘誤的,也有對項目代碼提出修改意見的,你們積極的參與讓我不敢有一絲的懈怠。項目的更新和維護仍然在不斷地進行中,後期會根據和你們的溝通結果,對項目的部分細節進行重構,另外也會加上更多的彩蛋,目前的計劃是將hooks源碼解析的系列文章放在小冊中,不斷給這個小冊增值。但願你們可以多多支持,也但願你可以經過這個項目獲得充分的鍛鍊、吸收到足夠的經驗,關於項目更多的細節,這裏就不贅述了,在小冊的第一節已經有足夠具體的介紹了。

最後奉上小冊的地址, 祝學習愉快!

相關文章
相關標籤/搜索