從新學習 React (一) 生命週期,Fiber 調度和更新機制

前幾天面試問道 react 的相關知識,對我打擊比較大,感受對 react 認識很是膚淺,因此在這裏從新梳理一下,想一想以前沒有仔細思考過的東西。javascript

另外有說的不對的地方還請幫我指正一下,先謝謝各位啦。java

目錄索引:react

什麼是生命週期和調度?

React 有一套合理的運行機制去控制程序在指定的時刻該作什麼事,當一個生命週期鉤子被觸發後,緊接着會有下一個鉤子,直到整個生命週期結束。面試

生命週期

生命週期表明着每一個執行階段,好比組件初始化,更新完成,立刻要卸載等等,React 會在指定的時機執行相關的生命週期鉤子,使咱們能夠有機在程序運行中會插入本身的邏輯。算法

調度

咱們寫代碼的時候每每會有不少組件以及他們的子組件,各自調用不一樣的生命週期,這時就要解決誰先誰後的問題,在 react v16 以前是採用了遞歸調用的方式一個一個執行,而在如今 v16 的版本中則採用了與之徹底不一樣的處理(調度)方式,名叫 Fiber,這個東西 facebook 作了有兩年時間,實現很是複雜。redux

具體 Fiber 它是一個什麼東西呢?不要着急,咱們先從最基本的生命週期鉤子看起。性能優化

React 生命週期詳解

首先看一下 React V16.4 後的生命週期概況(圖片來源異步

  • 從橫向看,react 分爲三個階段:
    • 建立時
      • constructor() - 類構造器初始化
      • static getDerivedStateFromProps() - 組件初始化時主動觸發
      • render() - 遞歸生成虛擬 DOM
      • componentDidMount() - 完成首次 DOM 渲染
    • 更新時
      • static getDerivedStateFromProps() - 每次 render() 以前執行
      • shouldComponentUpdate() - 校驗是否須要執行更新操做
      • render() - 遞歸生成虛擬 DOM
      • getSnapshotBeforeUpdate() - 在渲染真實 DOM 以前
      • componentDidUpdate() - 完成 DOM 渲染
    • 卸載時
      • componentWillUnmount() - 組件銷燬以前被直接調用

一些乾貨

  • 有三種方式能夠觸發 React 更新,props 發生改變,調用 setState() 和調用 forceUpdate()
  • static getDerivedStateFromProps() 這個鉤子會在每一個更新操做以前(即便props沒有改變)執行一次,使用時應該保持謹慎。
  • componentDidMount()componentDidUpdate() 執行的時機是差很少的,都在 render 以後,只不過前者只在首次渲染後執行,後者首次渲染不會執行
  • getSnapshotBeforeUpdate() 執行時能夠得到只讀的新 DOM 樹,此函數的返回值爲 componentDidUpdate(prevProps, prevState, snapshot) 的第三個參數

嘗試理解 Fiber

關於 Fiber,強烈建議聽一下知乎上程墨Morgan的 live 《深刻理解React v16 新功能》,這裏潛水員的例子和圖片也是引用於此 live。async

背景

咱們知道 React 是經過遞歸的方式來渲染組件的,在 V16 版本以前的版本里,當一個狀態發生變動時,react 會從當前組件開始,依次遞歸調用全部的子組件生命週期鉤子,並且這個過程是同步執行的且沒法中斷的,一旦有很深很深的組件嵌套,就會形成嚴重的頁面卡頓,影響用戶體驗。函數

React 在V16版本以前的版本里引入了 Fiber 這樣一個東西,它的英文涵義爲纖維,在計算機領域它排在在進程和線程的後面,雖然 React 的 Fiber 和計算機調度裏的概念不同,可是能夠方便對比理解,咱們大概能夠想象到 Fiber 多是一個比線程還短的時間片斷。

Fiber 到底作了什麼事

Fiber 把當前須要執行的任務分紅一個個微任務,安排優先級,而後依次處理,每過一段時間(很是短,毫秒級)就會暫停當前的任務,查看有沒有優先級較高的任務,而後暫停(也可能會徹底放棄)掉以前的執行結果,跳出到下一個微任務。同時 Fiber 還作了一些優化,能夠保持住以前運行的結果以到達複用目的。

舉個潛水員的例子

咱們能夠把調度當成一個潛水員在海底尋寶,v16 以前是經過組件遞歸的方式進行尋寶,從父組件開始一層一層深刻到最裏面的子組件,也就是以下圖所示。

而替換成了 Fiber 後,海底變成的狹縫(簡單理解爲遞歸變成了遍歷),潛水員會每隔一小段時間浮出水面,看看有沒有其餘尋寶任務。注意此時沒有尋到寶藏的話,那麼以前潛水的時間就浪費了。就這樣潛水員會一直下潛和冒泡,具體以下圖所示。

引入 Fiber 後帶來的三個階段

從生命週期那張圖片縱向來看,Fiber 將整個生命週期分紅了三個階段:

  • render 階段
    • 因爲 Fiber 會時不時跳出任務,而後從新執行,會致使該階段的生命週期調用屢次的現象,因此 React V16 以前 componentWillMount()componentWillUpdate()componentWillReceiveProps() 的三個生命週期鉤子被加上了 UNSAFE 標記
    • 這個階段效率不必定會比以前同步遞歸來的快,由於會有任務跳出重作的性能損耗,可是從宏觀上看,它不斷執行了最高優先級(影響用戶使用體驗)的任務,因此用戶使用起來會比之前更加的流暢
    • 這個階段的生命週期鉤子可能會重複調用,建議只寫無反作用的代碼
  • pre-commit 階段
    • 該階段 DOM 已經造成,但仍是隻讀狀態
    • 這個階段組件狀態不會再改變
  • commit 階段
    • 此時的 DOM 能夠進行操做
    • 這個階段組件已經完成更新,能夠寫一些有反作用的代碼和添加其它更新操做。

簡而言之:以 render() 爲界,以前執行的生命週期都有可能會打斷並屢次調用,以後的生命週期是不可被打斷的且只會調用一次。因此儘可能把反作用的代碼放在只會執行一次的 commit 階段。

其它生命週期鉤子

除了上面經常使用的鉤子,React 還提供了以下鉤子:

  • static getDerivedStateFromError() 在 render 階段執行,經過返回 state 更新組件狀態
  • componentDidCatch() 在 commit 階段執行,能夠放一些有反作用的代碼

更新機制

理解了生命週期和三個執行階段,就能夠比較容易理解組件狀態的更新機制了。

setState()

這個方法可讓咱們更新組件的 state 狀態。第一個參數能夠是對象,也能夠是 updater 函數,若是是函數,則會接受當前的 state 和 props 做爲參數。第二個參數爲函數,是在 commit 階段後執行,準確的說是在 componentDidUpdate() 後執行。

setState() 的更新過程是異步的(除非綁定在 DOM 事件中或寫在 setTimeout 裏),並且會在最後合併全部的更新,以下:

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)
複製代碼

之因此設計成這樣,是爲了不在一次生命週期中出現屢次的重渲染,影響頁面性能。

forceUpdate()

若是咱們想強制刷新一個組件,能夠直接調用該方法,調用時會直接執行 render() 這個函數而跳過 shouldComponentUpdate()

舉個極端例子

function wait() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
      console.log("wait");
    }, 0);
  });
}

//......省略組件建立
async componentDidMount() {
  await wait();
  this.setState({
    name: "new name"
  });
  console.log("componentDidMount");
}

componentDidUpdate() {
  console.log("componentDidUpdate");
}

render() {
  console.log(this.state);
  return null
}
//......省略組件建立

// 輸出結果以下
// wait
// {name: "new name"}
// componentDidUpdate
// componentDidMount

// 注意 componentDidUpdate 的輸出位置,通常狀況下
// componentDidUpdate 都是在componentDidMount 後面
// 執行的,可是這裏由於setState 寫在了 await 後面
// 因此狀況相反。
複製代碼

結語

瞭解 react 生命週期和更新機制確實有利於編寫代碼,特別是當代碼量愈來愈大時,錯用的 setState 或生命週期鉤子均可能埋下愈來愈多的雷,直到有一天沒法維護。。。

個人我的建議以下:

  • 把反作用代碼統統放在 commit 階段,由於這個階段不會影響頁面渲染性能
  • 儘量不要使用 forceUpdate() 方法,借用 Evan You 的一句話,若是你發現你本身須要在 Vue 中作一次強制更新,99.9% 的狀況,是你在某個地方作錯了事
  • 只要調用了 setState() 就會進行 render(),不管 state 是否改變
  • 知道 setState() 更新的何時是同步的,何時是異步的,參見上文
  • 不要把 getDerivedStateFromProps() 當成是 UNSAFE_componentWillReceiveProps() 的替代品,由於 getDerivedStateFromProps() 會在每次 render() 以前執行,即便 props 沒有改變
相關文章
相關標籤/搜索