今天來分享給大家PLC程序解密?每個plc的生產廠家都說自己的plc無法解密 ,但是最終都還難逃破解的厄運。經過幾天的努力功夫不負有心人,終于可以直讀密碼了react-redux 這個庫想必熟悉 react 的人都不陌生,用一句話描述它就是:它作為『redux 這個框架無關的數據流管理庫』和『react 這個視圖庫』的橋梁,使得 react 中能更新 redux 的 store,并能監聽 store 的變化并通知 react 的相關組件更新,從而能讓 react 將狀態放在外部管理(有利于 model 集中管理,能利用 redux 單項數據流架構,數據流易預測易維護,也極大的方便了任意層級組件間通信等等好處)。
react-redux 版本來自截止 2022.02.28 時的最新版本 v8.0.0-beta.2(有點悲催的是,讀源碼的時候還是 7 版本,沒想到剛讀完git pull
一下就升到 8 了,所以把 8 又看了一遍)
react-redux 8
相比于 7 版本包括但不限于這些改變:
- 全部用 typescript 重構
- 原來的 Subscription class 被 createSubscription 重構,用閉包函數代替 class 的好處,講到那部分代碼的時候會提到。
-
使用 React18 的useSyncExternalStore代替原來自己實現的訂閱更新(內部是
useReducer
),useSyncExternalStore
以及它的前身useMutableSource解決了 concurrent 模式下的tearing
問題,也讓庫本身的代碼更簡潔,useSyncExternalStore
相比于前輩useMutableSource
不用關心selector
(這里說的是useSyncExternalStore
的 selector,不是 react-redux)的 immutable 心智負擔。
?
下面的部分和源碼解析沒有直接關系,但讀了也能有所收獲,也能明白為什么要寫這篇文章。想直接看源碼解析部分的可以跳轉到React-Redux 源碼解析部分
正文前的吹水階段 1:既然是『再讀』,那『首讀』呢?
不知道大家平時在逛技術論壇的時候,有沒有看見過類似這樣的評論:redux 性能不好,mobx 更香……
喜歡刨根問底的人(比如我)看到了不禁想問更多問題:
- 究竟是 redux 性能不好還是 react-redux 性能不好?
- 具體不好在哪里?
- 能不能避免?
這些問題你問了,可能得到的也是三言兩語,不夠深入。與此同時還有一個問題, react-redux 是如何關聯起 redux 和 react 的?這個問題倒是有不少源碼解析的文章,我曾經看過一篇很詳細的,不過很可惜是老版本的,還在用 class component,所以當時的我決定自己去看源碼。當時屬于是粗讀,讀完之后的簡單總結就是 Provider 中有 Subscription 實例,connect 這個高階組件中也有 Subscription 實例,并且有負責自身更新的 hooks: useReducer,useReducer 的 dispatch 會被注冊進 Subscription 的 listeners,listeners 中有一個方法 notify 會遍歷調用每個 listener,notify 會被注冊給 redux 的 subscribe,從而 redux 的 state 更新后會通知給所有 connect 組件,當然每個 connect 都有檢查自己是否需要更新的方法 checkForUpdates 來避免不必要的更新,具體細節就不說了。
總之,當時我只粗讀了整體邏輯,但是可以解答我上面的問題了:
-
react-redux 確實有可能性能不好。而至于 redux,每次 dispatch 都會讓 state 去每個 reducer 走一遍,并且為了保證數據 immutable 也會有額外的創建復制開銷。不過
mutable
陣營的庫如果頻繁修改對象也會導致 V8 的對象內存結構由順序結構變成字典結構,查詢速度降低,以及內聯緩存變得高度超態,這點上 immutable 算拉回一點差距。不過為了一個清晰可靠的數據流架構,這種級別的開銷在大部分場景算是值得,甚至忽略不計。 - react-redux 性能具體不好在哪里?因為每個 connect 不管需不需要更新都會被通知一次,開發者定義的 selector 都會被調用一遍甚至多遍,如果 selector 邏輯昂貴,還是會比較消耗性能的。
- 那么 react-redux 一定會性能不好嗎?不一定,根據上面的分析,如果你的 selector 邏輯簡單(或者將復雜派生計算都放在 redux 的 reducer 里,但是這樣可能不利于構建一個合理的 model),connect 用的不多,那么性能并不會被 mobx 這樣的細粒度更新拉開太多。也就是說 selector 里業務計算不復雜、使用全局狀態管理的組件不多的情況下,完全不會有可感知的性能問題。那如果 selector 里面的業務計算復雜怎么辦呢?能不能完全避免呢?當然可以,你可以用 reselect 這個庫,它會緩存 selector 的結果,只有原始數據變化時才會重新計算派生數據。
這就是我的『首讀』,我帶著目的和問題去讀源碼,現在問題已經解決了,按理說一切都結束了,那么『再讀』是因何而起的呢?
正文前的吹水階段 2:為什么要『再讀』?
前段時間我關注了一個 github 上的 React 狀態管理庫zustand
。
zustand 是一個非常時髦的基于 hooks 的狀態管理庫,基于簡化的 flux 架構,也是 2021 年 Star 增長最快的 React 狀態管理庫。可以說是 redux + react-redux 的有力競爭者。
它的 github 開頭是這樣介紹的
大意是:它是一個小巧、快速、可擴展的、使用簡化的 flux 架構的狀態管理解決方案。有基于 hooks 的 api,使用起來十分舒適、人性化。
不要因為它很可愛而忽視它(貌似作者把它比喻成小熊了,封面圖也是一個可愛的小熊)。它有很多的爪子,花了大量的時間去處理常見的陷阱,比如可怕的子代僵尸問題(zombie child problem),react 并發模式(react concurrency),以及使用 portals 時多個 render 之間的 context 丟失問題(context loss)。它可能是 React 領域中唯一一個能夠正確處理所有這些問題的狀態管理器。
里面講到一個東西:zombie child problem。當我點進 zombie child problem 時,是 react-redux 的官方文檔,讓我們一起來看看這個問題是什么以及 react-redux 是如何解決的。想看原文可以直接點鏈接。
"Stale Props" and "Zombie Children"(過期 Props 和僵尸子節點問題)
自 v7.1.0 版本發布以后,react-redux 就可以使用 hooks api 了,官方也推薦使用 hooks 作為組件中的默認使用方法。但是有一些邊緣情況可能會發生,這篇文檔就是讓我們意識到這些事的。
react-redux 實現中最難的地方之一就是:如果你的 mapStateToProps 是(state, ownProps)這樣使用的,它將會每次被傳入『最新的』props。一直到版本 4 都一直有邊緣場景下的重復的 bug 被報告,比如:有一個列表 item 的數據被刪除了,mapStateToProps 里面就報錯了。
從版本 5 開始,react-redux 試圖保證
ownProps
的一致性。在版本 7 里面,每個connect()
內部都有一個自定義的 Subscription 類,從而當 connect 里面又有 connect,它能形成一個嵌套的結構。這確保了樹中更低層的 connect 組件只會在離它最近的祖先 connect 組件更新后才會接受到來自 store 的更新。然而,這個實現依賴于每個connect()
實例里面覆寫了內部 React Context 的一部分(subscription 那部分),用它自身的 Subscription 實例用于嵌套。然后用這個新的 React Context ( \ ) 渲染子節點。\>如果用 hooks,沒有辦法渲染一個 context.Provider,這就代表它不能讓 subscriptions 有嵌套的結構。因為這一點,"stale props" 和 "zombie child" 問題可能在『用 hooks 代替 connect』 的應用里重新發生。
具體來說,"stale props" 會出現在這種場景:
- selector 函數會根據這個組件的 props 計算出數據
- 父組件會重新 render,并傳給這個組件新的 props
- 但是這個組件會在 props 更新之前就執行 selector(譯者注:因為子組件的來自 store 的更新是在 useLayoutEffect/useEffect 中注冊的,所以子組件先于父組件注冊,redux 觸發訂閱會先觸發子組件的更新方法)
這種舊的 props 和最新 store state 算出來的結果,很有可能是錯誤的,甚至會引起報錯。
"Zombie child"具體是指在以下場景:
- 多個嵌套的 connect 組件 mounted,子組件比父組件更早的注冊到 store 上
- 一個 action dispatch 了在 store 里刪除數據的行為,比如一個 todo list 中的 item
- 父組件在渲染的時候就會少一個 item 子組件
- 但是,因為子組件是先被訂閱的,它的 subscription 先于父組件。當它計算一個基于 store 和 props 計算的值時,部分數據可能已經不存在了,如果計算邏輯不注意的話就會報錯。
useSelector()
試圖這樣解決這個問題:它會捕獲所有來自 store 更新導致的 selector 計算中的報錯,當錯誤發生時,組件會強制更新,這時 selector 會再次執行。這個需要 selector 是個純函數并且你沒有邏輯依賴 selector 拋出錯誤。如果你更喜歡自己處理,這里有一個可能有用的事項能幫助你在使用
useSelector()
時避免這些問題
- 不要在 selector 的計算中依賴 props
- 如果在:你必須要依賴 props 計算并且 props 將來可能發生變化、依賴的 store 數據可能會被刪除,這兩種情況下時,你要防備性的寫 selector。不要直接像
state.todos[props.id].name
這樣讀取值,而是先讀取state.todos[props.id]
,驗證它是否存在再讀取todo.name
因為connect
向 context provider 增加了必要的Subscription
,它會延遲執行子 subscriptions 直到這個 connected 組件 re-rendered。組件樹中如果有 connected 組件在使用useSelector
的組件的上層,也可以避免這個問題,因為父 connect 有和 hooks 組件同樣的 store 更新(譯者注:父 connect 組件更新后才會更新子 hooks 組件,同時 connect 組件的更新會帶動子節點更新,被刪除的節點在此次父組件的更新中已經卸載了:因為上文中說state.todos[props.id].name
,說明 hooks 組件是上層通過 ids 遍歷出來的。于是后續來自 store 的子 hooks 組件更新不會有被刪除的)
以上的解釋可能讓大家明白了 "Stale Props" 和 "Zombie Children" 問題是如何產生的以及 react-redux 大概是怎么解決的,就是通過子代 connect 的更新被嵌套收集到父級 connect,每次 redux 更新并不是遍歷更新所有 connect,而是父級先更新,然后子代由父級更新后才觸發更新。但是似乎 hooks 的出現讓它并不能完美解決問題了,而且具體這些設計的細節也沒有說到。這部分的疑惑和缺失就是我準備再讀 react-redux 源碼的原因。
React-Redux 源碼解析
react-redux 版本來自截止 2022.02.28 時的最新版本 v8.0.0-beta.2
閱讀源碼期間在 fork 的 react-redux 項目中寫下了一些中文注釋,作為一個新項目放在了react-redux-with-comment倉庫,閱讀文章需要對照源碼的可以看一下,版本是 8.0.0-beta.2
在講具體細節之前我想先說一下總體的抽象設計,讓大家心中帶著設計藍圖去讀其中的細節,否則只看細節很難讓它們之間串聯起來明白它們是如何共同協作完成整個功能的。
React-Redux 的 Provider 和 connect 都提供了自己的貫穿子樹的 context,它們的所有的子節點都可以拿到它們,并會將自己的更新方法交給它們。最終形成了根 <-- 父 <-- 子這樣的收集順序。根收集的更新方法會由 redux 觸發,父收集的更新方法在父更新后再更新,于是保證了父節點被 redux 更新后子節點才更新的順序。
審核編輯:符乾江
評論
查看更多