按照正常來講,像 React-Redux 這一類較為活躍的社群類庫,在 React 有較大的更新出現的時候一般都會及時跟進的。而這一次 React 的 Hooks 釋出,有將近兩個月的 beta 期,以及到截止本篇文章釋出已經 Hooks 正式版也已經將近一個月來,React-Redux 到現在都沒有正式釋出一個類似
useRedux
這樣的 Hooks API,那麼這是為什麼呢?我們來分析一下原因。
去年年底,出於興趣,研究了一波 redux 和 react-redux 的原始碼,除了在原理上的理解之外,讓我較為好奇的一點就是:React-Redux到目前為止都沒有對 Hooks 進行支援。從使用角度上來講,出現一個類似:
function ConnectedComponent() {
const store = useReduxStore()
const state = useRedux(function mapState())
}
複製程式碼
這樣的程式碼是非常可以理解的,而且也是非常符合 Hooks 的使用習慣的,事實上社群上也出現來很多非官方的 reudx Hooks 的類庫:
說明了總體上社群對於 Hooks 的接受度是很高的,大家應該都在期待官方能給出一個真正的 Hooks API。那麼為什麼 React-Redux 到現在都沒有釋出正式的 Hooks API 呢?
在翻閱 React-Redux 的 issues 列表的時候,我發現了這個 issue。作者非常完整得為我們介紹了 React-Redux 從最初得 idea 到現在 v6 版本得成長曆程。那麼 v6 版本相比 v5 版本有哪些大的變化呢?
- 使用
createContext
來傳遞 state - 只有
Provider
訂閱了 store 的變化 - 不再對被
connect
的元件傳遞 store 物件
v6 版本更新這些內容的主要原因如下:
- 老的 context API 即將被刪除,並且如果和新的 context API 一起使用會有問題
- React 即將推出
Concurrent Mode
非同步渲染,如果使用老的方式可能會導致不同的子樹獲取的狀態不同,使用新的 context API,React 會確保整棵樹拿到的是相同的狀態 createContext
預設帶有top-down資料流,不再需要 React-Redux 自己實現
以上是 v6 版本的變化和其原因,但是到目前為止我們好像並沒有看到任何提及 Hooks 的地方。別急,接下去就是正題了。
在升級到 v6 的過程中,React-Redux 團隊發現 v6 版本的整體效能是比不上 v5 的。這個效能下降的主要原因不是 React-Redux 的實現程式碼有什麼問題,其主要問題是來自createContent
的實現方式,以及 React-Redux 選擇了只有在Provider
中訂閱 store 變化。
注意
React-Redux 選擇使用
createContext
和只有在Provider
中訂閱都是沒有任何問題的,也是 React 官方推薦的使用方法,從面向未來的眼光來看,這是勢必的升級。所以同學們在後面分析問題的時候不要問,為什麼不換個實現方式啥的。
那麼所謂的效能問題具體是怎麼來的呢?**主要原因是createContext
在 value 變化的時候他是如何通知子樹的。**我們先來看一組效能測試對比圖:
這個測試用例來自react-redux-benchmarks,大家有興趣可以自己去跑一下。
從圖中我們可以看出來,v6 主要效能降低的點是來自於Scripting
,也就是執行 JavaScript 指令碼的時間,從資料上來看,是 v5 版本的兩倍多。雖然在Rendering
和Painting
階段要好很多,但是因為Scripting
的佔比最大,所以總體上講是略微有些下降的。
**其根本原因是createContext
的實現方式中,我們更改了Provider
的 value,那麼在這次更新週期中,React 會遍歷Provider
的所有子節點,並對監聽了這一個 context 的節點進行標記,讓後續渲染中知道這個節點是需要更新的,即便他的 props 和 state 根本沒有變化。**關於為什麼 React 要這麼去實現的原因不是一句話能講完的,他涉及到 React 16 之後 Fiber 判斷一個節點是否有更新的方法,後面我會單獨寫一篇文章來講解,現在大家只需要知道他就是這麼實現的就可以了。
因為上訴的原因,我們可以想象在一個節點非常多的 React 應用中,一個類似 React-Redux 這樣放置在最頂層的Provier
資料變化之後,他的總體計算量肯定是非常大的。
相對的,在 v5 中因為使用老的 context API,為了避免一些這個 API 帶來的問題,所以 React-Redux 團隊選擇在connect
返回的WrapperComponent
HOC 中進行 store 資料變化的監聽,也就是說 Store 變化之後之後被connect
的元件可能出現 props 上的變化,而沒有任何需要遍歷子樹的需要。
以上就是 v6 版本在效能上不升反降的原因。這也是 React 新的 context API 不是很適合用在變化頻繁的資料上原因。我們可以想象如果我們像以前一樣把一個表單的所有項資料都快取在 redux store 裡面,每次輸入都要更新 store,可能帶來的對整體效能的影響。關於這一塊,React 也有一個issue在討論是否以及如何設計一個方案來解決這個效能上的問題。這個討論非常熱鬧,大家有興趣可以關注一下。
那麼到現在為止我們還是沒有講到 Hooks 相關的任何內容,是不是有點偏題了?不,因為我們已經知道了大部分的原因,那就是新的 context API 存在的效能問題。而這個問題,反應到 Hooks 上面,則會更大程度地體現出來。
如果我們要封裝一個類似useRedux
這樣的 Hook,那麼我們肯定需要用到useContext
來獲取Provider
提供的 state,畢竟Provider
是唯一訂閱來 store 變化的。而使用了useContext
,就代表我們這個元件是依賴於這個 context 的,也就是說一旦 state 變化,這個元件就會被標記為需要更新。
而按照我們一直以來的使用 React-Reudx 的情況,我們都會提供mapState
來對映元件真正需要監聽的資料,因為 store 是整個應用的,不太會存在某一個元件需要整個應用所有的資料的情況。這種情況下,在 v5 版本中,甚至是在 v6 版本中使用connect
的情況,都會在 HOC 中進行mapState
的執行進行資料對映,然後通過shallowEqual
判斷是否有依賴的 state 變化,如果沒有其實是不需要更新真正的元件的。
但是在使用useContext
的情況,即便我們給useRedux
提供來mapState
,但是他的執行依然要等到這個元件真正開始執行更新的時候。也就是說我們無法讓 React 在更新這個元件之前就判斷他是否可以不被更新,那麼 React 提供的優化就沒啥用了。
而同時一旦我們的元件開始執行,即便我們發現useRedux
返回的 map 之後的 state 其實跟上一次是一樣的,我們也無法告訴 React 這個元件其實是不需要更新的來終止這次更新。所以,這是一個無法在**類庫層面進行的優化。**要優化我們只有通過使用者自己使用useMemo
這樣的 API,那麼對於開發經驗不是那麼多的同學,很可能會導致這個元件會被頻繁進行無用更新,而導致效能浪費。
那麼以上就是為什麼 React-Redux 以及很多常用類庫還沒有更新 Hooks API 的原因來,目前來說這個效能問題較為無解,React 官方也在考慮是否要出一些新的 context 相關的 API 來專門優化更新頻率較高的情況,我們也只能拭目以待了。
目前來說,如果你不清楚createContent
的這些問題,建議不要把經常需要更新的內容放在 context 裡面(除非沒有別的方法)。
以上,就是我對於為什麼 Hooks 現在呼聲這麼高,但是社群支援卻沒有這麼快跟進的原因分析,如果有任何問題,可以直接回復郵件,或者在我的AMA(Ask Me Anything)中給我提問,我都會瀏覽,並且進行解答。
另外在這裡提出的一些問題,也會在後續進行更詳細的解析:
createContext
為什麼在更新的時候要遍歷所有子樹節點- React 中判斷一個節點是否可以跳過更新的判斷條件
我是Jocky,如果對於我分析的React內容感興趣,可以訂閱我,我會保持對React生態更新及時跟進,以及對React及其生態的內容進行深度解析。