Redux 并不慢,只是你使用姿势不对 —— 一份优化指南
- 原文地址:Redux 并不慢,只是你使用姿勢不對 —— 一份優(yōu)化指南
- 原文作者:Julian Krispel
- 譯文出自:掘金翻譯計劃
- 本文永久鏈接:github.com/xitu/gold-m…
- 譯者:reid3290
- 校對者:sunui,xekri
如何優(yōu)化使用了 Redux 的 React 應(yīng)用不是那么顯而易見的,但其實又是非常簡單直接的。本文即是一份帶有若干示例的簡短指南。
在優(yōu)化使用了 Redux 的 React 應(yīng)用的時候,我經(jīng)常聽人說 Redux 很慢。其實在 99% 的情況下,性能低下都和不必要的渲染有關(guān)(這一論斷也適用于其他框架),因為 DOM 更新的代價是昂貴的。通過本文,你將學(xué)會如何在使用 Redux 的 React 應(yīng)用中避免不必要的渲染。
一般來講,要在 Redux store 更新的時候同步更新 React 組件,需要用到 React 和 Redux 的官方綁定庫中的 connect 高階組件。
connect 是一個將你的組件進(jìn)行包裹的函數(shù),它返回一個高階組件,該高階組件會監(jiān)聽 Redux store,當(dāng)有狀態(tài)更新時就重新渲染自身及其后代組件。
React 和 Redux 的官方綁定庫 —— react-redux 快速入門
connect 高階組件實際上已經(jīng)被優(yōu)化過了。為了理解如何更好地使用它,必須先理解它是如何工作的。
實際上,Redux 和 react-redux 都是非常小的庫,因此其源碼也并非高深莫測。我鼓勵人們通讀源碼,或者至少讀一部分。如果你想更進(jìn)一步的話,可以自己實現(xiàn)一個,這能讓你深入理解為什么它要作如此設(shè)計。
閑言少敘,讓我們稍微深入地研究一下 react-redux 的工作機(jī)制。前面已經(jīng)提過,react-redux 的核心是 connect 高階組件,其函數(shù)簽名如下:
return function connect(mapStateToProps,mapDispatchToProps,mergeProps,{pure = true,areStatesEqual = strictEqual,areOwnPropsEqual = shallowEqual,areStatePropsEqual = shallowEqual,areMergedPropsEqual = shallowEqual,...extraOptions} = {} ) { ... }復(fù)制代碼順便說一下 —— 只有 mapStateToProps 這一個參數(shù)是必須的,而且大多數(shù)情況下只會用到前兩個參數(shù)。此處我引用這個函數(shù)簽名是為了闡明 react-redux 的工作機(jī)制。
所有傳給 connect 函數(shù)的參數(shù)都用于生成一個對象,該對象則會作為屬性傳給被包裹的組件。mapStateToProps 用于將 Redux store 的狀態(tài)映射成一個對象,mapDispatchToProps 用于產(chǎn)生一個包含函數(shù)的對象 —— 這些函數(shù)一般都是動作生成器(action creators)。mergeProps 則接收 3 個參數(shù):stateProps、dispatchProps 和 ownProps,前兩個分別是 mapStateToProps 和 mapDispatchToProps 的返回結(jié)果,最后一個則是繼承自組件本身的屬性。默認(rèn)情況下,mergeProps 會將上述參數(shù)簡單地合并到一個對象中;但是你也可以傳遞一個函數(shù)給 mergeProps,connect 則會使用這個函數(shù)為被包裹的組件生成屬性。
connect 函數(shù)的第四個參數(shù)是一個屬性可選的對象,具體包含 5 個可選屬性:一個布爾值 pure 以及其他四個用于決定組件是否需要重新渲染的函數(shù)(應(yīng)當(dāng)返回布爾值)。pure 默認(rèn)為 true,如果設(shè)為 false,connect 高階組件則會跳過所有的優(yōu)化選項,而且那四個函數(shù)也就不起任何作用了。我個人認(rèn)為不太可能有這類應(yīng)用場景,但是如果你想關(guān)閉優(yōu)化功能的話可以將其設(shè)為 false。
mergeProps 返回的對象會和上一個屬性對象作比較,如果 connect 高階組件認(rèn)為屬性對象所有改變的話就會重新渲染組件。為了理解 react-redux 是如何判斷屬性是否有變化的,請參考 shallowEqual 函數(shù)。如果該函數(shù)返回 true,則組件不會渲染;反之,組件將會重新渲染。shallowEqual 負(fù)責(zé)進(jìn)行屬性對象的比較,下文是其部分代碼,基本表明了其工作原理:
for (let i = 0; i < keysA.length; i++) {if (!hasOwn.call(objB, keysA[i]) ||!is(objA[keysA[i]], objB[keysA[i]])) {return false} }復(fù)制代碼概括來講,這段代碼做了這些工作:
遍歷對象 A 中的所有屬性,檢查對象 B 中是否存在同名屬性。然后檢查 A 和 B 同名屬性的屬性值是否相等。如果這些檢查有一個返回 false,則對象 A 和 B 便被認(rèn)為是不等的,組件也就會重新渲染。
這引出一條黃金法則:
只給組件傳遞其渲染所必須的數(shù)據(jù)
這可能有點難以理解,所以讓我們結(jié)合一些例子來細(xì)細(xì)分析一下。
將和 Redux 有連接的組件拆分開來
我見過很多人這樣做:用一個容器組件監(jiān)聽一大堆狀態(tài),然后通過屬性傳遞下去。
const BigComponent = ({ a, b, c, d }) => (<div><CompA a={a} /><CompB b={b} /><CompC c={c} /></div> );const ConnectedBigComponent = connect(({ a, b, c }) => ({ a, b, c }) );復(fù)制代碼現(xiàn)在,一旦 a、b 或 c 中的任何一個發(fā)生改變,BigComponent 以及 CompA、CompB 和 CompC 都會重新渲染。
其實應(yīng)該將組件拆分開來,而無需過分擔(dān)心使用了太多的 connect:
const ConnectedA = connect(CompA, ({ a }) => ({ a })); const ConnectedB = connect(CompB, ({ b }) => ({ b })); const ConnectedC = connect(CompC, ({ c }) => ({ c }));const BigComponent = () => (<div><ConnectedA a={a} /><ConnectedB b={b} /><ConnectedC c={c} /></div> );復(fù)制代碼如此一來,CompA 只有在 a 發(fā)生改變后才會重新渲染,CompB 只有在 b 發(fā)生改變后才會重新渲染,CompC 也是類似的。如果 a、b、c 更新很頻繁的話,那每次更新我們僅僅只是重新渲染一個組件而不是一下渲染三個。就這三個組件來講區(qū)別可能不會很明顯,但要是組件再多一些就比較明顯了。
轉(zhuǎn)變組件狀態(tài),使之盡可能地小
這里有一個人為構(gòu)造(稍有改動)的例子:
你有一個很大的列表,比如說有 300 多個列表項:
<List>{this.props.items.map(({ content, itemId }) => (<ListItemonClick={selectItem}content={content}itemId={itemId}key={itemId}/>))} </List>復(fù)制代碼點擊一個列表項便會觸發(fā)一個動作,同時更新 store 中的值 selectedItem。每一個列表項都通過 Redux 獲取 selectedItem 的值:
const ListItem = connect(({ selectedItem }) => ({ selectedItem }) )(SimpleListItem);復(fù)制代碼這里我們只給組件傳遞了其所必須的狀態(tài),這是對的。但是,當(dāng) selectedItem 發(fā)生變化時,所有 ListItem 都會重新渲染,因為我們從 selectedItem 返回的對象發(fā)生了變化,之前是 { selectedItem: 123 } 而現(xiàn)在是 { selectedItem: 120 }。
記住一點,我們使用了 selectedItem 的值來檢查當(dāng)前列表項是否被選中了。但是實際上組件只需要知道它有沒有被選中即可, 本質(zhì)上就是個 Boolean。布爾值用在這里簡直完美,因為它僅僅有 true 和 false 兩種狀態(tài)。如果我們返回一個布爾值而不是 selectedItem,那當(dāng)那個布爾值發(fā)生改變時只有兩個組件會被重新渲染,這正是我們期望的結(jié)果。mapStateToProps 實際上會將組件的 props 作為第二個參數(shù),我們可以利用這一點來確定當(dāng)前組件是否是被選中的那一項。代碼如下:
const ListItem = connect(({ selectedItem }, { itemId }) => ({ isSelected: selectedItem === itemId }) )(SimpleListItem);復(fù)制代碼如此一來,無論 selectedItem 如何變化,只有兩個組件會被重新渲染 —— 當(dāng)前選中的 ListItem 和那個被取消選擇的 ListItem。
保持?jǐn)?shù)據(jù)扁平
Redux 文檔 中作為最佳實踐提到了這點。保持 store 扁平有很多好處。但就本文而言,嵌套會造成一個問題,因為我們希望狀態(tài)更新粒度盡量小以使應(yīng)用運行盡量快。比如說我們有這樣一種深淺套的狀態(tài):
{articles: [{comments: [{users: [{}]}]}],... }復(fù)制代碼為了優(yōu)化 Article、Comment 和 User 組件,它們都需要訂閱 articles,而后在層層嵌套的屬性中找到所需要的狀態(tài)。其實如果將狀態(tài)展開成這樣會更加合理:
{articles: [{...}],comments: [{articleId: ..,userId: ...,...}],users: [{...}] }復(fù)制代碼之后用自己的映射函數(shù)獲取評論和用戶信息即可。更多關(guān)于狀態(tài)扁平化的內(nèi)容可以參閱 Redux 文檔。
福利:兩個選擇 Redux 狀態(tài)的庫
這一部分完全是可選的。一般來講上述那些建議足夠你編寫出高效的 react 和 Redux 應(yīng)用了。但還有兩個可以大大簡化狀態(tài)選擇的庫:
Reselect 是為 Redux 應(yīng)用編寫 selectors 所必不可少的工具。根據(jù)其官方文檔:
- Selectors 可以計算衍生數(shù)據(jù),可以讓 Redux 做到存儲盡可能少的狀態(tài)。
- Selectors 是高效的,只有在某個參數(shù)發(fā)生變化時才被重新計算。
- Selectors 是可組合的。它們可以用作其他 selectors 的輸入。
對于界面復(fù)雜、狀態(tài)繁多、更新頻繁的應(yīng)用,reselect 可以大大提高應(yīng)用運行效率。
Ramda 是一個由許多高階函數(shù)組成、功能強大的函數(shù)庫。 換句話說,就是許多用于創(chuàng)建函數(shù)的函數(shù)。由于我們的映射函數(shù)也不過只是函數(shù)而已,所以我們可以利用 Ramda 方便地創(chuàng)建 selectors。Ramda 可以完成所有 selectors 可以完成的工作,而且還不止于此。Ramda cookbook 中介紹了一些 Ramda 的應(yīng)用示例。
掘金翻譯計劃 是一個翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū),文章來源為 掘金 上的英文分享文章。內(nèi)容覆蓋 Android、iOS、React、前端、后端、產(chǎn)品、設(shè)計 等領(lǐng)域,想要查看更多優(yōu)質(zhì)譯文請持續(xù)關(guān)注 掘金翻譯計劃。
總結(jié)
以上是生活随笔為你收集整理的Redux 并不慢,只是你使用姿势不对 —— 一份优化指南的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: K-Dominant Character
- 下一篇: [leetcode][JAVA]面试题第