標籤:

React 16+版本中為什麼用更新生命周期函數?

React 16+版本中為什麼用getDerivedStateFromProps 去替換componentWillReceiveProps,說後者不安全,不安全在哪裡了?


謝邀。分為幾點。

  1. 理解 fiber 架構
  2. unsafe 原因
  3. 靜態函數:getDerivedStateFromProps
  4. getSnapshotBeforeUpdate
  5. 減少聲明周期,提高性能

理解 Fiber 架構

跟著 本文 走,你可以實現一個 Fiber 架構的簡化 React 版本。其中你會發現,現在 React 的做法是將 reconciler ( diff + render + 創建 DOM ) + commit( patch ),這兩個操作是分開的。

React 16 在發布之前就已經提到過,某些生命周期可能會被調用多次,這是因為 Fiber 架構下 reconciler 階段會調用多次,等一切結束以後,才調用 Commit ,然而 reconciler 就會執行那些所謂的 wilxxxx ,這明顯是不符合「語意」的,既然 「will「 了,然後又 「will「 一次,這不是脫了褲子放屁?

因此,willxxx 基本都會被刪除或者替換更明確的語意。

Unsafe 的原因

如果你能聯繫上下文,你就明白為什麼 React team 認為 willxxx 是 unsafe 了。在 react 誕生這幾個生命周期函數以來,大家可謂是無所不用其極。輕量一點的,在 willxxx 里設置 state,重口味一點的在 willxxx 里操作真實 DOM ( 用ref ),變態一點的在 willxxx 里引入 jQ 一頓亂搞。

以前么, willxxx 只執行一次,看似問題不是很大,雖然官方並不願意看到大家這麼亂搞,但是是可以接受的。現在因為 willxxx 可能執行多次,這問題就非常嚴重了。

比如,某些個用戶在 willxxx 中操作了真實 DOM ,那麼調用這個函數的時候,頁面會馬上重繪,更奇妙的是,如果這個函數被調用了兩次,那就是兩次重繪,性能誰能受得了?多了怎麼辦?所以,這明顯不是 React team 想看到的。

再說,在 willxxx 中 setState ,其實並沒有真正的運行更新邏輯,而是將 willxxx 中的所有 setState 進行合併,然後在 render 之前,一次打到 state 里,再進行 re-render(這就是為什麼 setState 是一個看似非同步的函數),所以 setState 沒什麼問題,但是操作 DOM 就難受了。

靜態函數:getDerivedStateFromProps

為什麼,為什麼,為什麼,我要在標題前面寫上「靜態函數」這幾個字?

靜態函數的特點就是,他不屬於任何一個實例,因此,他的內部 this 指向並不是組件的本身。這樣的結果直接導致了,用戶不能做以下幾個事情:

  1. 用this.refs....
  2. 用this.上的任何方法

這樣的設計,能夠使得 getDerivedStateFromProps 這個函數強迫變成一個純函數,邏輯也相對簡單,就沒那麼多錯誤了。

getSnapshotBeforeUpdate

我們看一下下面的清晰圖,你就能知道,getSnapshotBeforeUpdate 是讓那些以前比較變態的用戶完成真實 DOM 讀取的一個代替品,因為 getSnapshotBeforeUpdate 已經到了 commit 階段,因此這個函數只會運行一次,所以就和以前的 willxxx 一樣,使用了。

圖片來自:http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

減少聲明周期,提高性能

用戶級別的錯誤會發生在幾個地方:

  • constructor
  • 一切生命周期
  • render 函數
  • 事件回調

componentDidcatch的實現是依賴try catch ,理論上來說,我們只要在這些函數上都 try catch 就能夠捕獲框架級別的大部分錯誤,但是 try catch 性能並不好,constructor、render 函數、事件回調 這三個玩意呢,是不能改的,也不能去掉的,這是根基。

生命周期函數才是額外實現的,那麼優化肯定要從這些個周期進行,通過砍掉亂七八糟的 willxxx 函數,我們就不必 try catch 那麼多了,因此性能得到了一點提升。

總結

  • Fiber 架構下,reconciler 會進行多次,reconciler 過程又會調用多次之前的 willxxx ,造成了語意不明確,因此幹掉
  • 都次調用 willxxx 會導致一些性能安全/數據錯亂等問題,因此 Unsafe
  • 靜態函數 getDerivedStateFromProps ,直接將其函數內的用戶邏輯降低幾個數量級,減少用戶出錯,提高性能,符合語意
  • getSnapshotBeforeUpdate 替換之前 willxxxx,給想讀取 dom 的用戶一些空間,強逼用戶到 mount 階段才能操作 dom
  • 提高性能,減少 try catch 的使用


1)減少生命周期跟提升性能沒有直接關係。如 React 團隊最新博客所言,getDerivedStateFromProps 之前只在 parent props 導致更新時調用,改成在 setState 更新視圖時也調用。調用的頻次比 componentWillReceiveProps 還高。如果兩者寫相同代碼,新版本的性能更低。

React v16.4.0: Pointer Events - React Blog?

reactjs.org圖標

2)getDerivedStateFromProps 的設計目的是隔離組件實例訪問,但不能真正阻止訪問實例,下面代碼可以把實例傳入靜態方法。

state = {
instance: this
}

3)React 修改生命周期是為 async rendering 和 fiber 調度服務的。dan 之前在大會上演示了 suspense 的能力,可以在 render 函數里使用 createFetcher 的方法,非同步請求數據。

它大概的實現機制是,第一次 render 時,throw 一個 promise,在 catch 住之後,promise.then 時 re-render,re-render 時,fetcher 會從 cache 里同步地讀取 promise resolve 的值。此時 render 方法變成了跟以前一樣的同步方法。

這裡不僅存在兩次 render 方法調用,同時也存在兩次 render 方法調用前的生命周期調用,即 will* 開頭的生命周期。這就要求,不僅 render 需要是 pure 或 idempotent(冪等) 的,will* 開頭的生命周期也應該是 pure 或者 idempotent 的。

按照當前 react 社區在 will* 生命周期里通過組件實例所做的操作,async-render 機制肯定會帶來無法預測的副作用。所以修改生命周期,隔離 render 之前的生命周期,提純是必要的。


推薦閱讀:

React 裡面的循環
Redux VS 命令模式
Redux有哪些最佳實踐?
有沒有考慮過ReactDom.render的第一個參數到底是什麼?
如何評價數據流管理架構 Redux?

TAG:React |