React、Redux 和 Immutable.js:高效 Web 應用程序的組成部分
已發表: 2022-03-11React、Redux 和 Immutable.js 是目前最流行的 JavaScript 庫之一,並且正迅速成為開發人員在前端開發方面的首選。 在我參與過的幾個 React 和 Redux 項目中,我意識到很多剛開始使用 React 的開發人員並不完全理解 React 以及如何編寫高效的代碼來充分利用它的潛力。
在這個 Immutable.js 教程中,我們將使用 React 和 Redux 構建一個簡單的應用程序,並確定 React 的一些最常見的誤用以及避免它們的方法。
數據引用問題
React 是關於性能的。 它是從頭開始構建的,具有極高的性能,僅重新渲染 DOM 的最小部分以滿足新的數據更改。 任何 React 應用程序都應該主要由小的簡單(或無狀態函數)組件組成。 它們很容易推理,並且大多數都可以讓shouldComponentUpdate函數返回false 。
shouldComponentUpdate(nextProps, nextState) { return false; }性能方面,最重要的組件生命週期函數是shouldComponentUpdate ,如果可能,它應該總是返回false 。 這確保了該組件永遠不會重新渲染(初始渲染除外),從而有效地使 React 應用程序感覺非常快。
如果不是這種情況,我們的目標是對舊 props/state 與新 props/state 進行廉價的相等性檢查,如果數據未更改,則跳過重新渲染。
讓我們退後一步,回顧一下 JavaScript 如何對不同的數據類型執行相等檢查。
對boolean 、 string和integer等原始數據類型的相等性檢查非常簡單,因為它們總是通過它們的實際值進行比較:
1 === 1 'string' === 'string' true === true另一方面,對象、數組和函數等複雜類型的相等性檢查則完全不同。 如果兩個對象具有相同的引用(指向內存中的相同對象),則它們是相同的。
const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // false儘管 obj1 和 obj2 看起來相同,但它們的引用是不同的。 由於它們不同,在shouldComponentUpdate函數中天真地比較它們會導致我們的組件不必要地重新渲染。
需要注意的重要一點是,來自 Redux reducer 的數據,如果設置不正確,將始終使用不同的引用提供服務,這將導致組件每次重新渲染。
這是我們尋求避免組件重新渲染的核心問題。
處理引用
讓我們舉一個例子,其中我們有深度嵌套的對象,我們想將它與以前的版本進行比較。 我們可以遞歸循環遍歷嵌套的對象 props 並比較每一個,但顯然這將非常昂貴並且是不可能的。
這讓我們只有一個解決方案,那就是檢查參考資料,但很快就會出現新問題:
- 如果沒有任何變化,則保留參考
- 如果任何嵌套對象/數組道具值發生更改,則更改引用
如果我們想以一種漂亮、乾淨和性能優化的方式來做這件事,這不是一件容易的事。 Facebook 很久以前就意識到了這個問題,並調用了 Immutable.js 來解決這個問題。
import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference 沒有一個 Immutable.js 函數對給定數據執行直接突變。 相反,數據在內部被克隆、變異,如果有任何更改,則返回新的引用。 否則返回初始引用。 必須明確設置新引用,例如obj1 = obj1.set(...); .
React、Redux 和 Immutable.js 示例
展示這些庫的強大功能的最佳方式是構建一個簡單的應用程序。 還有什麼比待辦事項應用程序更簡單的呢?
為簡潔起見,在本文中,我們將僅介紹對這些概念至關重要的應用程序部分。 應用程序代碼的完整源代碼可以在 GitHub 上找到。
當應用程序啟動時,您會注意到對console.log的調用方便地放置在關鍵區域,以清楚地顯示 DOM 重新渲染的數量,這是最小的。
與任何其他待辦事項應用程序一樣,我們希望顯示待辦事項列表。 當用戶單擊待辦事項時,我們會將其標記為已完成。 此外,我們需要在頂部有一個小的輸入字段來添加新的待辦事項,並在底部的 3 個過濾器上允許用戶在以下之間切換:
- 全部
- 完全的
- 積極的
Redux 減速器
Redux 應用程序中的所有數據都存在於單個 store 對像中,我們可以將 reducer 視為一種將 store 拆分為更易於推理的小塊的便捷方式。 由於 reducer 也是一個函數,它也可以拆分成更小的部分。
我們的減速器將由 2 個小部件組成:
- 待辦事項列表
- 主動過濾器
// reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });與 Redux 連接
現在我們已經使用 Immutable.js 數據設置了一個 Redux reducer,讓我們將它與 React 組件連接以傳遞數據。
// components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);在一個完美的世界裡,connect 應該只在頂級路由組件上執行,提取 mapStateToProps 中的數據,其餘的是基本的 React 將 props 傳遞給孩子。 在大型應用程序中,跟踪所有連接往往會變得很困難,因此我們希望將它們保持在最低限度。
需要注意的是 state.todos 是一個從 Redux combineReducers函數返回的常規 JavaScript 對象(todos 是 reducer 的名稱),但 state.todos.todoList 是一個不可變列表,它保持在這樣一個非常重要的直到它通過shouldComponentUpdate檢查。
避免組件重新渲染
在我們深入挖掘之前,重要的是要了解必須向組件提供什麼類型的數據:
- 任何類型的原始類型
- 對象/數組僅以不可變形式
擁有這些類型的數據可以讓我們對 React 組件中的 props 進行淺顯的比較。
下一個示例展示瞭如何以最簡單的方式區分 props:
$ npm install react-pure-render import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }函數shallowEqual將僅檢查 1 級深度的道具/狀態差異。 它的運行速度非常快,並且與我們的不可變數據完美協同。 必須在每個組件中編寫這個shouldComponentUpdate會很不方便,但幸運的是有一個簡單的解決方案。

將 shouldComponentUpdate提取到一個特殊的單獨組件中:
// components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }然後只需擴展需要此 shouldComponentUpdate 邏輯的任何組件:
// components/Todo.js export default class Todo extends PureComponent { // Component code }這是在大多數情況下避免組件重新渲染的一種非常乾淨和有效的方法,以後如果應用程序變得更複雜並且突然需要自定義解決方案,它可以輕鬆更改。
在將函數作為道具傳遞時使用PureComponent時會出現一個小問題。 由於帶有 ES6類的 React 不會自動將this綁定到我們必須手動執行的函數。 我們可以通過執行以下操作之一來實現這一點:
- 使用 ES6 箭頭函數綁定:
<Component onClick={() => this.handleClick()} /> - 使用綁定:
<Component onClick={this.handleClick.bind(this)} />
這兩種方法都會導致Component重新渲染,因為每次都向onClick傳遞了不同的引用。
為了解決這個問題,我們可以在構造方法中預先綁定函數,如下所示:
constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }如果你發現自己大部分時間都預先綁定了多個函數,我們可以導出和重用小的輔助函數:
// utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }如果沒有一個解決方案適合您,您始終可以手動編寫shouldComponentUpdate條件。
處理組件內的不可變數據
使用當前的不可變數據設置,避免了重新渲染,並且我們在組件的 props 中留下了不可變數據。 使用這種不可變數據的方法有很多,但最常見的錯誤是使用不可變的 toJS函數立即將數據轉換為純 JS。
使用toJS將不可變數據深度轉換為普通 JS 否定了避免重新渲染的整個目的,因為正如預期的那樣,它非常慢,因此應該避免。 那麼我們如何處理不可變數據呢?
它需要按原樣使用,這就是為什麼 Immutable API 提供了各種各樣的函數, map和get在 React 組件中最常用。 來自 Redux Reducer 的todoList數據結構是一個不可變形式的對像數組,每個對象代表一個單獨的 todo 項:
[{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]Immutable.js API 與常規 JavaScript 非常相似,因此我們可以像使用任何其他對像數組一樣使用 todoList。 在大多數情況下,地圖功能證明是最好的。
在 map 回調中,我們得到todo ,它是一個仍然處於不可變形式的對象,我們可以安全地將它傳遞給Todo組件。
// components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }如果您計劃對不可變數據執行多個鍊式迭代,例如:
myMap.filter(somePred).sort(someComp)…那麼首先使用toSeq將其轉換為Seq並在迭代後將其轉換回所需的形式非常重要,例如:
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()由於 Immutable.js 從不直接改變給定數據,它總是需要製作另一個副本,像這樣執行多次迭代可能非常昂貴。 Seq 是惰性的不可變數據序列,這意味著它將執行盡可能少的操作來完成其任務,同時跳過創建中間副本。 Seq 就是為這種方式而構建的。
在Todo組件內部使用get或getIn來獲取道具。
夠簡單吧?
好吧,我意識到,很多時候它會變得非常難以理解,因為它有大量的get() ,尤其是getIn() 。 所以我決定在性能和可讀性之間找到一個最佳點,經過一些簡單的實驗後,我發現 Immutable.js的 toObject和toArray函數工作得很好。
這些函數將(1 級深度)Immutable.js 對象/數組淺轉換為純 JavaScript 對象/數組。 如果我們有任何數據深深嵌套在裡面,它們將保持不可變的形式,準備好傳遞給
它比get()慢了可以忽略不計,但看起來更乾淨:
// components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }讓我們看看這一切在行動
如果您還沒有從 GitHub 克隆代碼,現在是這樣做的好時機:
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutable啟動服務器很簡單(確保安裝了 Node.js 和 NPM):
npm install npm start 在 Web 瀏覽器中導航到 http://localhost:3000。 打開開發者控制台,在添加一些待辦事項時觀察日誌,將它們標記為完成並更改過濾器:
- 添加 5 個待辦事項
- 將過濾器從“全部”更改為“活動”,然後返回“全部”
- 無需重新渲染,只需過濾器更改
- 將 2 個待辦事項標記為已完成
- 重新渲染了兩個 todo,但一次只渲染一個
- 將過濾器從“全部”更改為“活動”,然後返回“全部”
- 僅安裝/卸載了 2 個已完成的待辦事項
- 活動的沒有重新渲染
- 從列表中間刪除單個待辦事項
- 只有移除的待辦事項受到影響,其他沒有重新渲染
包起來
如果使用得當,React、Redux 和 Immutable.js 的協同作用可以為大型 Web 應用程序中經常遇到的許多性能問題提供一些優雅的解決方案。
Immutable.js 允許我們檢測 JavaScript 對象/數組的變化,而無需求助於深度相等檢查的低效率,這反過來又允許 React 在不需要時避免昂貴的重新渲染操作。 這意味著 Immutable.js 的性能在大多數情況下往往都很好。
我希望你喜歡這篇文章,並發現它對在你未來的項目中構建 React 創新解決方案很有用。
