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