React 16+版本中為什麼用更新生命周期函數?
React 16+版本中為什麼用getDerivedStateFromProps 去替換componentWillReceiveProps,說後者不安全,不安全在哪裡了?
謝邀。分為幾點。
- 理解 fiber 架構
- unsafe 原因
- 靜態函數:getDerivedStateFromProps
- getSnapshotBeforeUpdate
- 減少聲明周期,提高性能
理解 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 指向並不是組件的本身。這樣的結果直接導致了,用戶不能做以下幾個事情:
- 用this.refs....
- 用this.上的任何方法
這樣的設計,能夠使得 getDerivedStateFromProps 這個函數強迫變成一個純函數,邏輯也相對簡單,就沒那麼多錯誤了。
getSnapshotBeforeUpdate
我們看一下下面的清晰圖,你就能知道,getSnapshotBeforeUpdate 是讓那些以前比較變態的用戶完成真實 DOM 讀取的一個代替品,因為 getSnapshotBeforeUpdate 已經到了 commit 階段,因此這個函數只會運行一次,所以就和以前的 willxxx 一樣,使用了。
減少聲明周期,提高性能
用戶級別的錯誤會發生在幾個地方:
- 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 |