使用 Immer 的更好的減速器
已發表: 2022-03-10作為一個 React 開發者,你應該已經熟悉了state 不應該直接改變的原則。 您可能想知道這意味著什麼(我們大多數人在剛開始時都感到困惑)。
本教程將對此進行公正處理:您將了解什麼是不可變狀態以及對它的需求。 您還將學習如何使用 Immer 處理不可變狀態以及使用它的好處。 您可以在此 Github 存儲庫中找到本文中的代碼。
JavaScript 中的不變性及其重要性
Immer.js 是一個小型 JavaScript 庫,由 Michel Weststrate 編寫,其使命是讓您“以更方便的方式處理不可變狀態”。
但在深入研究 Immer 之前,讓我們快速回顧一下 JavaScript 中的不變性以及它在 React 應用程序中的重要性。
最新的 ECMAScript(又名 JavaScript)標准定義了九種內置數據類型。 在這九種類型中,有六種被稱為primitive
值/類型。 這六個原語是undefined
、 number
、 string
、 boolean
、 bigint
和symbol
。 使用 JavaScript 的typeof
運算符進行簡單檢查將揭示這些數據類型的類型。
console.log(typeof 5) // number console.log(typeof 'name') // string console.log(typeof (1 < 2)) // boolean console.log(typeof undefined) // undefined console.log(typeof Symbol('js')) // symbol console.log(typeof BigInt(900719925474)) // bigint
primitive
是一個不是對象且沒有方法的值。 對於我們目前的討論來說,最重要的是一個原語的值一旦被創建就不能改變。 因此,原語被稱為immutable
的。
其餘三種類型是null
、 object
和function
。 我們還可以使用typeof
運算符檢查它們的類型。
console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function
這些類型是mutable
的。 這意味著它們的值可以在創建後隨時更改。
您可能想知道為什麼我在上面有數組[0, 1]
。 好吧,在 JavaScriptland 中,數組只是一種特殊類型的對象。 如果您還想知道null
以及它與undefined
有何不同。 undefined
只是意味著我們沒有為變量設置值,而null
是對象的特例。 如果您知道某物應該是一個對象,但該對像不存在,您只需返回null
。
為了用一個簡單的例子來說明,試著在你的瀏覽器控制台中運行下面的代碼。
console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]
String.prototype.match
應該返回一個數組,它是一個對object
類型。 當它找不到這樣的對象時,它返回null
。 返回undefined
在這裡也沒有意義。
夠了。 讓我們回到討論不變性。
根據 MDN 文檔:
“除了對象之外的所有類型都定義了不可變的值(即不能更改的值)。”
該語句包含函數,因為它們是一種特殊類型的 JavaScript 對象。 請參閱此處的函數定義。
讓我們快速了解一下可變和不可變數據類型在實踐中的含義。 嘗試在瀏覽器控制台中運行以下代碼。
let a = 5; let b = a console.log(`a: ${a}; b: ${b}`) // a: 5; b: 5 b = 7 console.log(`a: ${a}; b: ${b}`) // a: 5; b: 7
我們的結果表明,即使b
是從a
“派生”的,更改b
的值也不會影響a
的值。 這是因為當 JavaScript 引擎執行語句b = a
時,它會創建一個新的、單獨的內存位置,將5
放入其中,並將b
指向該位置。
對象呢? 考慮下面的代碼。
let c = { name: 'some name'} let d = c; console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"some name"}; d: {"name":"some name"} d.name = 'new name' console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"new name"}; d: {"name":"new name"}
我們可以看到,通過變量d
更改 name 屬性也會在c
中更改它。 這是因為當 JavaScript 引擎執行語句c = { name: 'some name
'
}
時,JavaScript 引擎會在內存中創建一個空間,將對象放入其中,並將c
指向它。 然後,當它執行語句d = c
時,JavaScript 引擎只是將d
指向同一個位置。 它不會創建新的內存位置。 因此,對d
中的項目的任何更改都隱含地是對c
中的項目的操作。 不費力氣,我們就能明白為什麼這是個麻煩事。
想像一下,您正在開發一個 React 應用程序,並且您想在某個地方通過讀取變量c
來將用戶名顯示為some name
。 但是在其他地方,您通過操作對象d
在代碼中引入了錯誤。 這將導致用戶名顯示為new name
。 如果c
和d
是原語,我們就不會有這個問題。 但是對於典型的 React 應用程序必須維護的狀態種類來說,原語太簡單了。
這就是為什麼在應用程序中保持不可變狀態很重要的主要原因。 我鼓勵您通過閱讀 Immutable.js README 中的這個簡短部分來檢查其他一些注意事項:不變性的案例。
了解了為什麼我們需要在 React 應用程序中保持不變性之後,現在讓我們看看 Immer 如何通過其produce
函數解決這個問題。
Immer的produce
功能
Immer 的核心 API 非常小,您將使用的主要功能是produce
功能。 produce
簡單地接受一個初始狀態和一個定義狀態應該如何變化的回調。 回調本身接收到它進行所有預期更新的狀態的草稿(相同,但仍然是副本)副本。 最後,它produce
一個新的、不可變的狀態,並應用了所有的更改。
這種狀態更新的一般模式是:
// produce signature produce(state, callback) => nextState
讓我們看看這在實踐中是如何工作的。
import produce from 'immer' const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], } // to add a new package const newPackage = { name: 'immer', installed: false } const nextState = produce(initState, draft => { draft.packages.push(newPackage) })
在上面的代碼中,我們簡單地傳遞了起始狀態和一個回調來指定我們希望突變如何發生。 就這麼簡單。 我們不需要觸及該州的任何其他部分。 它使initState
保持不變,並在結構上共享我們在起始狀態和新狀態之間未觸及的那些狀態部分。 在我們的州,其中一個這樣的部分是pets
數組。 nextState
produce
一個不可變的狀態樹,其中包含我們所做的更改以及我們未修改的部分。

有了這些簡單但有用的知識,讓我們看看produce
如何幫助我們簡化 React reducer。
使用 Immer 編寫減速器
假設我們有下面定義的狀態對象
const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };
我們想添加一個新對象,然後在後續步驟中,將其installed
的密鑰設置為true
const newPackage = { name: 'immer', installed: false };
如果我們使用 JavaScript 對象和數組擴展語法以通常的方式執行此操作,我們的狀態縮減器可能如下所示。
const updateReducer = (state = initState, action) => { switch (action.type) { case 'ADD_PACKAGE': return { ...state, packages: [...state.packages, action.package], }; case 'UPDATE_INSTALLED': return { ...state, packages: state.packages.map(pack => pack.name === action.name ? { ...pack, installed: action.installed } : pack ), }; default: return state; } };
我們可以看到,這對於這個相對簡單的狀態對象來說是不必要的冗長並且容易出錯。 我們還必須觸及國家的每一個部分,這是不必要的。 讓我們看看如何使用 Immer 來簡化它。
const updateReducerWithProduce = (state = initState, action) => produce(state, draft => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'UPDATE_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
並且通過幾行代碼,我們大大簡化了我們的 reducer。 此外,如果我們陷入默認情況,Immer 只會返回草稿狀態,而我們不需要做任何事情。 請注意如何減少樣板代碼和消除狀態傳播。 使用 Immer,我們只關心我們想要更新的狀態部分。 如果我們找不到這樣的項目,例如在 `UPDATE_INSTALLED` 操作中,我們只需繼續前進,而無需觸及任何其他內容。 `produce` 函數也適用於柯里化。 將回調作為第一個參數傳遞給 `produce` 旨在用於柯里化。 咖哩“produce”的簽名是//curried produce signature produce(callback) => (state) => nextState
讓我們看看如何用咖哩產品更新我們之前的狀態。 我們的咖哩產品看起來像這樣: const curriedProduce = produce((draft, action) => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'SET_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
咖哩產物函數接受一個函數作為其第一個參數並返回一個咖哩產物,它現在只需要一個狀態來產生下一個狀態。 該函數的第一個參數是草稿狀態(將派生自調用此咖哩產品時要傳遞的狀態)。 然後跟隨我們希望傳遞給函數的每個參數數量。
為了使用這個函數,我們現在需要做的就是傳入我們想要從中產生下一個狀態的狀態和像這樣的動作對象。
// add a new package to the starting state const nextState = curriedProduce(initState, { type: 'ADD_PACKAGE', package: newPackage, }); // update an item in the recently produced state const nextState2 = curriedProduce(nextState, { type: 'SET_INSTALLED', name: 'immer', installed: true, });
請注意,在 React 應用程序中使用useReducer
鉤子時,我們不需要像我在上面所做的那樣顯式傳遞狀態,因為它會處理這個問題。
你可能想知道,Immer 會像現在 React 中的所有東西一樣得到一個hook
嗎? 好吧,你有好消息陪伴。 Immer 有兩個處理狀態的鉤子: useImmer
和useImmerReducer
鉤子。 讓我們看看它們是如何工作的。
使用useImmer
和useImmerReducer
Hooks
對useImmer
鉤子的最佳描述來自 use-immer README 本身。
useImmer(initialState)
與useState
非常相似。 函數返回一個元組,元組的第一個值是當前狀態,第二個是updater函數,它接受一個immer producer函數,在這個函數中可以自由地對draft
進行變異,直到producer結束並進行更改不可變並成為下一個狀態。
要使用這些鉤子,除了主要的 Immer 庫之外,您還必須單獨安裝它們。
yarn add immer use-immer
在代碼方面, useImmer
鉤子如下所示
import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)
就這麼簡單。 你可以說它是 React 的 useState 但有點類固醇。 使用更新功能非常簡單。 它接收草稿狀態,您可以像下面那樣隨意修改它。
// make changes to data updateData(draft => { // modify the draft as much as you want. })
Immer 的創建者提供了一個代碼沙盒示例,您可以嘗試看看它是如何工作的。
如果你使用過 React 的useReducer
鉤子,那麼useImmerReducer
也同樣易於使用。 它有一個相似的簽名。 讓我們看看用代碼術語來說是什麼樣子的。
import React from "react"; import { useImmerReducer } from "use-immer"; const initState = {} const reducer = (draft, action) => { switch(action.type) { default: break; } } const [data, dataDispatch] = useImmerReducer(reducer, initState);
我們可以看到 reducer 收到了一個draft
狀態,我們可以隨意修改它。 這裡還有一個代碼框示例供您試驗。
這就是使用 Immer hooks 的簡單程度。 但是,如果您仍然想知道為什麼要在項目中使用 Immer,這裡總結了我發現的使用 Immer 的一些最重要的原因。
為什麼你應該使用 Immer
如果您已經編寫了任何時間的狀態管理邏輯,您將很快體會到 Immer 提供的簡單性。 但這並不是 Immer 提供的唯一好處。
當你使用 Immer 時,你最終會編寫更少的樣板代碼,正如我們在相對簡單的 reducer 中看到的那樣。 這也使得深度更新相對容易。
使用 Immutable.js 等庫,您必須學習新的 API 才能獲得不變性的好處。 但是使用 Immer,您可以使用普通的 JavaScript Objects
、 Arrays
、 Sets
和Maps
來實現相同的目標。 沒有什麼新東西要學。
Immer 還默認提供結構共享。 這僅僅意味著當您對狀態對象進行更改時,Immer 會自動在新狀態和先前狀態之間共享狀態中未更改的部分。
使用 Immer,您還可以自動凍結對象,這意味著您無法更改produced
的狀態。 例如,當我開始使用 Immer 時,我嘗試將sort
方法應用於 Immer 的生產函數返回的對像數組。 它拋出了一個錯誤,告訴我無法對數組進行任何更改。 在應用sort
之前,我必須應用數組切片方法。 再一次,生成的nextState
是不可變的狀態樹。
Immer 也是強類型的,gzip 壓縮後非常小,只有 3KB。
結論
在管理狀態更新方面,使用 Immer 對我來說是輕而易舉的事。 這是一個非常輕量級的庫,可以讓你繼續使用你所學過的關於 JavaScript 的所有東西,而無需嘗試學習全新的東西。 我鼓勵您將它安裝在您的項目中並立即開始使用它。 您可以在現有項目中添加使用它並逐步更新您的減速器。
我還鼓勵您閱讀 Michael Weststrate 的 Immer 介紹性博客文章。 我覺得特別有趣的部分是“Immer 是如何工作的?” 這部分解釋了 Immer 如何利用代理等語言功能和寫時復制等概念。
我還建議您閱讀這篇博文:JavaScript 中的不變性:一種對比觀點,作者 Steven de Salas 介紹了他對追求不變性的優點的看法。
我希望通過您在這篇文章中學到的東西,您可以立即開始使用 Immer。
相關資源
use-immer
, GitHub- 伊默,GitHub
-
function
, MDN 網絡文檔, Mozilla -
proxy
,MDN 網絡文檔,Mozilla - 對象(計算機科學),維基百科
- “JS 中的不變性”,Orji Chidi Matthew,GitHub
- “ECMAScript 數據類型和值”,Ecma International
- JavaScript 的不可變集合,Immutable.js,GitHub
- “不變性的案例”,Immutable.js,GitHub