Redux 并不慢,只是你使用姿勢不對 —— 一份優化指南

  • 原文地址:Redux 并不慢,只是你使用姿勢不對 —— 一份優化指南
  • 原文作者:Julian Krispel
  • 譯文出自:掘金翻譯計劃
  • 本文永久鏈接:github.com/xitu/gold-m…
  • 譯者:reid3290
  • 校對者:sunui,xekri

如何優化使用了 Redux 的 React 應用不是那么顯而易見的,但其實又是非常簡單直接的。本文即是一份帶有若干示例的簡短指南。

在優化使用了 Redux 的 React 應用的時候,我經常聽人說 Redux 很慢。其實在 99% 的情況下,性能低下都和不必要的渲染有關(這一論斷也適用于其他框架),因為 DOM 更新的代價是昂貴的。通過本文,你將學會如何在使用 Redux 的 React 應用中避免不必要的渲染。

一般來講,要在 Redux store 更新的時候同步更新 React 組件,需要用到 React 和 Redux 的官方綁定庫中的 connect 高階組件。
connect 是一個將你的組件進行包裹的函數,它返回一個高階組件,該高階組件會監聽 Redux store,當有狀態更新時就重新渲染自身及其后代組件。

React 和 Redux 的官方綁定庫 —— react-redux 快速入門

connect 高階組件實際上已經被優化過了。為了理解如何更好地使用它,必須先理解它是如何工作的。

實際上,Redux 和 react-redux 都是非常小的庫,因此其源碼也并非高深莫測。我鼓勵人們通讀源碼,或者至少讀一部分。如果你想更進一步的話,可以自己實現一個,這能讓你深入理解為什么它要作如此設計。

閑言少敘,讓我們稍微深入地研究一下 react-redux 的工作機制。前面已經提過,react-redux 的核心是 connect 高階組件,其函數簽名如下:

return function connect(mapStateToProps,mapDispatchToProps,mergeProps,{pure = true,areStatesEqual = strictEqual,areOwnPropsEqual = shallowEqual,areStatePropsEqual = shallowEqual,areMergedPropsEqual = shallowEqual,...extraOptions} = {}
) {
...
}復制代碼

順便說一下 —— 只有 mapStateToProps 這一個參數是必須的,而且大多數情況下只會用到前兩個參數。此處我引用這個函數簽名是為了闡明 react-redux 的工作機制。

所有傳給 connect 函數的參數都用于生成一個對象,該對象則會作為屬性傳給被包裹的組件。mapStateToProps 用于將 Redux store 的狀態映射成一個對象,mapDispatchToProps 用于產生一個包含函數的對象 —— 這些函數一般都是動作生成器(action creators)。mergeProps 則接收 3 個參數:statePropsdispatchPropsownProps,前兩個分別是 mapStateToPropsmapDispatchToProps 的返回結果,最后一個則是繼承自組件本身的屬性。默認情況下,mergeProps 會將上述參數簡單地合并到一個對象中;但是你也可以傳遞一個函數給 mergePropsconnect 則會使用這個函數為被包裹的組件生成屬性。

connect 函數的第四個參數是一個屬性可選的對象,具體包含 5 個可選屬性:一個布爾值 pure 以及其他四個用于決定組件是否需要重新渲染的函數(應當返回布爾值)。pure 默認為 true,如果設為 false,connect 高階組件則會跳過所有的優化選項,而且那四個函數也就不起任何作用了。我個人認為不太可能有這類應用場景,但是如果你想關閉優化功能的話可以將其設為 false。

mergeProps 返回的對象會和上一個屬性對象作比較,如果 connect 高階組件認為屬性對象所有改變的話就會重新渲染組件。為了理解 react-redux 是如何判斷屬性是否有變化的,請參考 shallowEqual 函數。如果該函數返回 true,則組件不會渲染;反之,組件將會重新渲染。shallowEqual 負責進行屬性對象的比較,下文是其部分代碼,基本表明了其工作原理:

for (let i = 0; i < keysA.length; i++) {if (!hasOwn.call(objB, keysA[i]) ||!is(objA[keysA[i]], objB[keysA[i]])) {return false}
}復制代碼

概括來講,這段代碼做了這些工作:

遍歷對象 A 中的所有屬性,檢查對象 B 中是否存在同名屬性。然后檢查 A 和 B 同名屬性的屬性值是否相等。如果這些檢查有一個返回 false,則對象 A 和 B 便被認為是不等的,組件也就會重新渲染。

這引出一條黃金法則:

只給組件傳遞其渲染所必須的數據

這可能有點難以理解,所以讓我們結合一些例子來細細分析一下。

將和 Redux 有連接的組件拆分開來

我見過很多人這樣做:用一個容器組件監聽一大堆狀態,然后通過屬性傳遞下去。

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 })
);復制代碼

現在,一旦 abc 中的任何一個發生改變,BigComponent 以及 CompACompBCompC 都會重新渲染。

其實應該將組件拆分開來,而無需過分擔心使用了太多的 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>
);復制代碼

如此一來,CompA 只有在 a 發生改變后才會重新渲染,CompB 只有在 b 發生改變后才會重新渲染,CompC 也是類似的。如果 abc 更新很頻繁的話,那每次更新我們僅僅只是重新渲染一個組件而不是一下渲染三個。就這三個組件來講區別可能不會很明顯,但要是組件再多一些就比較明顯了。

轉變組件狀態,使之盡可能地小

這里有一個人為構造(稍有改動)的例子:

你有一個很大的列表,比如說有 300 多個列表項:

<List>{this.props.items.map(({ content, itemId }) => (<ListItemonClick={selectItem}content={content}itemId={itemId}key={itemId}/>))}
</List>復制代碼

點擊一個列表項便會觸發一個動作,同時更新 store 中的值 selectedItem。每一個列表項都通過 Redux 獲取 selectedItem 的值:

const ListItem = connect(({ selectedItem }) => ({ selectedItem })
)(SimpleListItem);復制代碼

這里我們只給組件傳遞了其所必須的狀態,這是對的。但是,當 selectedItem 發生變化時,所有 ListItem 都會重新渲染,因為我們從 selectedItem 返回的對象發生了變化,之前是 { selectedItem: 123 } 而現在是 { selectedItem: 120 }

記住一點,我們使用了 selectedItem 的值來檢查當前列表項是否被選中了。但是實際上組件只需要知道它有沒有被選中即可, 本質上就是個 Boolean。布爾值用在這里簡直完美,因為它僅僅有 truefalse 兩種狀態。如果我們返回一個布爾值而不是 selectedItem,那當那個布爾值發生改變時只有兩個組件會被重新渲染,這正是我們期望的結果。mapStateToProps 實際上會將組件的 props 作為第二個參數,我們可以利用這一點來確定當前組件是否是被選中的那一項。代碼如下:

const ListItem = connect(({ selectedItem }, { itemId }) => ({ isSelected: selectedItem === itemId })
)(SimpleListItem);復制代碼

如此一來,無論 selectedItem 如何變化,只有兩個組件會被重新渲染 —— 當前選中的 ListItem 和那個被取消選擇的 ListItem

保持數據扁平

Redux 文檔 中作為最佳實踐提到了這點。保持 store 扁平有很多好處。但就本文而言,嵌套會造成一個問題,因為我們希望狀態更新粒度盡量小以使應用運行盡量快。比如說我們有這樣一種深淺套的狀態:

{articles: [{comments: [{users: [{}]}]}],...
}復制代碼

為了優化 ArticleCommentUser 組件,它們都需要訂閱 articles,而后在層層嵌套的屬性中找到所需要的狀態。其實如果將狀態展開成這樣會更加合理:

{articles: [{...}],comments: [{articleId: ..,userId: ...,...}],users: [{...}]
}復制代碼

之后用自己的映射函數獲取評論和用戶信息即可。更多關于狀態扁平化的內容可以參閱 Redux 文檔。

福利:兩個選擇 Redux 狀態的庫

這一部分完全是可選的。一般來講上述那些建議足夠你編寫出高效的 react 和 Redux 應用了。但還有兩個可以大大簡化狀態選擇的庫:

Reselect 是為 Redux 應用編寫 selectors 所必不可少的工具。根據其官方文檔:

  • Selectors 可以計算衍生數據,可以讓 Redux 做到存儲盡可能少的狀態。
  • Selectors 是高效的,只有在某個參數發生變化時才被重新計算。
  • Selectors 是可組合的。它們可以用作其他 selectors 的輸入。

對于界面復雜、狀態繁多、更新頻繁的應用,reselect 可以大大提高應用運行效率。

Ramda 是一個由許多高階函數組成、功能強大的函數庫。 換句話說,就是許多用于創建函數的函數。由于我們的映射函數也不過只是函數而已,所以我們可以利用 Ramda 方便地創建 selectors。Ramda 可以完成所有 selectors 可以完成的工作,而且還不止于此。Ramda cookbook 中介紹了一些 Ramda 的應用示例。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、后端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/257967.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/257967.shtml
英文地址,請注明出處:http://en.pswp.cn/news/257967.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

把windows裝到linux下,如何將WSL(Windows Subsystem for Linux 2)安裝到Windows 10?

原標題&#xff1a;如何將WSL(Windows Subsystem for Linux 2)安裝到Windows 10&#xff1f;Windows 10憑借大受歡迎的WSL(Windows Subsystem for Linux)進入Linux領域。由于最近推出了WSL的最新版WSL2&#xff0c;用戶現在可以利用實際的Linux內核從Windows執行Linux任務。現在…

TWRP-recovery中文界面安裝方法[轉]

把下載到的ui.zip放入sdcard1/twrp文件夾。注意&#xff0c;是內置存儲卡中。如沒有上述文件夾&#xff0c;自行建立后通過文件管理器放入&#xff0c;不是卡刷。文件夾應如下所示&#xff1a;sdcard1&#xff08;內置SD&#xff09; &#xff5c; ┕--twrp&#xff08;文件夾…

如何定期備份網站數據

產生這個問題的背景是我在維護兩個個人的網站&#xff0c;因為采用的是虛擬主機&#xff0c;有時候空間續費不及時等&#xff0c;都可能造成數據的丟失&#xff0c;為了保障數據不丟失&#xff0c;因為有必要每15天左右對網站數據進行備份以防止發生不當的事情。 我們希望做的就…

初創團隊可能不適合應屆生小孩

根據最近招聘中接觸到的一些剛畢業小孩的表現&#xff0c;談談這個問題&#xff1a; 1、扛不住&#xff0c;初創團隊一般最好一人撐一快工作&#xff0c;剛畢業經驗比較薄的小孩在這方面一是心理上不敢擔當&#xff0c;二是能力上確實還需要磨煉成長 2、初創團隊的那個環境可能…

vba執行linux命令,從VBA中的shell命令捕獲輸出值?

慕蓋茨4494581根據Andrew Lessard的回答&#xff0c;這是一個運行命令并將輸出作為字符串返回的函數 -Public Function ShellRun(sCmd As String) As StringRun a shell command, returning the output as a stringDim oShell As ObjectSet oShell CreateObject("WScript…

溢出和剪裁,可見性

內容溢出和剪裁 如果一個元素的內容對于元素大小來說過大&#xff0c;就有可能溢出元素本身。對于此情況&#xff0c;有一些解決辦法可選。 溢出 overflow 值 visible(默認):內容在元素框外可見。一般會導致內容超出其自己的元素框&#xff0c;但不會改變框的形狀scroll:溢出部…

C#= 棧模仿堆的操作

//原理&#xff0c;利用兩個棧&#xff0c;互相作用&#xff0c;來模仿堆的效果&#xff0c;先進先出。。 1 using System;2 using System.Collections.Generic;3 using System.Linq;4 using System.Threading.Tasks;5 6 namespace TwoStacksQueue7 {8 public class Progra…

linux計劃任務執行日志,linux中centos制定計劃任務執行命令并且輸出日志

1.寫腳本最簡單的 寫如下代碼#!/bin/shABC1.每個命令之間用;隔開說明&#xff1a;各命令的執行給果&#xff0c;不會影響其它命令的執行。換句話說&#xff0c;各個命令都會執行&#xff0c;但不保證每個命令都執行成功。2.每個命令之間用&&隔開說明&#xff1a;若前面…

Java-大集合拆分為指定大小的小集合

因為Oracle數據的in 最大允許1000 ,超過就會報錯&#xff0c; 所以需要將集合拆分為多個集合進行處理. /*** 拆分集合* param <T>* param resList 要拆分的集合* param count 每個集合的元素個數* return 返回拆分后的各個集合*/public static <T> List<L…

AsyncTask與多任務

問題由來&#xff1a; 之前看到一篇博文&#xff0c;說AsyncTask不適合運行多任務&#xff0c; 多個任務不會異步執行&#xff0c; 當時只是印象里記住了一下也不確定&#xff0c; 今天把代碼看了看&#xff0c; 把原因寫出來。 問題的代碼演示&#xff1a; 1 public class Asy…

iptables簡單應用

可以修改/etc/rc.d/boot.local讓規則重啟后也能生效&#xff0c;如&#xff1a;/sbin/iptables -F/sbin/iptables -A INPUT -i eth0 -p tcp --sport 80 -j ACCEPT/sbin/iptables -A INPUT -i eth0 -p tcp -j DROP/sbin/iptables -A INPUT -i eth0 -p udp -j DROPiptables是一個…

linux中內部命令有哪些,linux內部命令有哪些

linux中常見的內部命令有&#xff1a;1.exit命令&#xff0c;退出當前的shell&#xff1b;2.history命令&#xff0c;顯示歷史執行過的命令&#xff1b;3.cd命令&#xff0c;切換當前工作目錄&#xff1b;4.source命令&#xff0c;重新執行剛修改的初始化文件&#xff1b;5.ech…

使用SALT-API進入集成開發的簡單樣例

測試的時候&#xff0c;可以CURL -K&#xff0c;但真正作集成的時候&#xff0c;卻是不可以的。 必須&#xff0c;不可以讓TOKEN滿天飛吧。 現在進入這個階段了。寫個樣例先&#xff1a; import salt import salt.auth import salt.log import saltapiopts salt.client.LocalC…

POJ 2778

題意&#xff1a;很Uva項鏈題目類似。 區別&#xff1a; 1、字符串很多&#xff0c;用map hash超時&#xff0c;用Trie查找。 2、DFS判斷連通&#xff0c;和并查集判連通&#xff0c;被我寫錯的地方時&#xff0c;查森林的時候&#xff0c;還是要Find_Set。 1 #include <ios…

linux掛載VMFS硬盤,ESX4.1掛載NFS共享存儲(VMkernel)

要使用vmotion,iscsi,nfs功能&#xff0c;必須啟用VMkernel端口&#xff0c;ESX 4.1默認不啟用&#xff0c;ESXi 5.x默認啟用。在 vCenter Server“SZVCENTER01”上調用對象“datastoreSystem-44”的“HostDatastoreSystem.CreateNasDatastore” 失敗。掛載NFS存儲的ESX控制臺命…

Perl學習之四:語句(續)

循環控制&#xff1a;1.last 退出標簽的語句塊2.next 3.redo不推薦&#xff0c;循環次數不可控 4.goto不推薦。***************************************標簽&#xff1a; 先定義一個 labellast|next|redo|goto label&#xff1b; last VS next 相當于C語言中的&#xff1a;las…

2017年8個最流行的Web編程趨勢

互聯網一直在不斷的發展&#xff0c;這意味著開發人員必須及時了解當前的所有變化。人們在新聞、社交、購物到銀行等各大方面都與互聯網有著千絲萬縷的聯系。因此&#xff0c;為了滿足全球數百萬網絡用戶的需求&#xff0c;Web開發需求正在上升。Web編程趨勢是在W開發的過程中不…

linux 分卷壓縮到指定目錄,運用在android下Linux分卷壓縮與分卷解壓的命令

protected static Vector execRootCmd(String paramString) {Vector localVector new Vector();try {Process localProcess Runtime.getRuntime().exec("su ");// 經過Root處理的android系統即有su命令OutputStream localOutputStream localProcess.getOutputStre…

gRPC-rs:從 C 到 Rust

介紹 在上篇文章中&#xff0c;我們講到 TiKV 為了支持 [gRPC]&#xff0c;我們造了個輪子 [gRPC-rs]&#xff0c;這篇文章簡要地介紹一下這個庫。首先我們來聊聊什么是 gRPC。gRPC 是 Google 推出的基于 [HTTP2] 的開源 RPC 框架&#xff0c;希望通過它使得各種微服務之間擁有…

紅帽linux無法進入tty,linux自啟腳本(以及無法進入tty控制臺)

1.建立需開機運行的腳本auto(可以不要后面的.sh后綴)2.放在/etc/init.d/目錄下 (操作系統復制命令&#xff0c;在當前文件夾下復制sudo cp auto /etc/init.d)[可能先要對init.d取得x權限]3.賦予權限&#xff0c;在init.d文件目錄下sudo chmod 775 ./auto4.執行&#xff0c;命…