Skip to main content

不可变的更新模式

¥Immutable Update Patterns

先决概念#不可变数据管理 中列出的文章提供了许多关于如何不可变地执行基本更新操作的好示例,例如更新对象中的字段或将项目添加到数组的末尾。然而,Reducers 通常需要结合使用这些基本操作来执行更复杂的任务。以下是你可能需要执行的一些更常见任务的一些示例。

¥The articles listed in Prerequisite Concepts#Immutable Data Management give a number of good examples for how to perform basic update operations immutably, such as updating a field in an object or adding an item to the end of an array. However, reducers will often need to use those basic operations in combination to perform more complicated tasks. Here are some examples for some of the more common tasks you might have to implement.

更新嵌套对象

¥Updating Nested Objects

更新嵌套数据的关键是必须适当地复制和更新每个级别的嵌套。对于那些学习 Redux 的人来说,这通常是一个困难的概念,并且在尝试更新嵌套对象时经常会出现一些特定的问题。这些会导致意外的直接突变,应该避免。

¥The key to updating nested data is that every level of nesting must be copied and updated appropriately. This is often a difficult concept for those learning Redux, and there are some specific problems that frequently occur when trying to update nested objects. These lead to accidental direct mutation, and should be avoided.

正确的做法:复制所有级别的嵌套数据

¥Correct Approach: Copying All Levels of Nested Data

不幸的是,将不可变更新正确应用到深度嵌套状态的过程很容易变得冗长且难以阅读。以下是更新 state.first.second[someId].fourth 的示例:

¥Unfortunately, the process of correctly applying immutable updates to deeply nested state can easily become verbose and hard to read. Here's what an example of updating state.first.second[someId].fourth might look like:

function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}

显然,每一层嵌套都会使代码变得更难阅读,并且产生更多犯错的机会。这是鼓励你保持状态扁平化并尽可能多地编写 reducer 的几个原因之一。

¥Obviously, each layer of nesting makes this harder to read, and gives more chances to make mistakes. This is one of several reasons why you are encouraged to keep your state flattened, and compose reducers as much as possible.

常见错误 #1:指向相同对象的新变量

¥Common Mistake #1: New variables that point to the same objects

定义新变量不会创建新的实际对象 - 它只创建对同一对象的另一个引用。此错误的一个示例是:

¥Defining a new variable does not create a new actual object - it only creates another reference to the same object. An example of this error would be:

function updateNestedState(state, action) {
let nestedState = state.nestedState
// ERROR: this directly modifies the existing object reference - don't do this!
nestedState.nestedField = action.data

return {
...state,
nestedState
}
}

该函数确实正确返回了顶层状态对象的浅表副本,但由于 nestedState 变量仍然指向现有对象,因此状态直接发生了变化。

¥This function does correctly return a shallow copy of the top-level state object, but because the nestedState variable was still pointing at the existing object, the state was directly mutated.

常见错误 #2:只制作一层的浅表副本

¥Common Mistake #2: Only making a shallow copy of one level

此错误的另一个常见版本如下所示:

¥Another common version of this error looks like this:

function updateNestedState(state, action) {
// Problem: this only does a shallow copy!
let newState = { ...state }

// ERROR: nestedState is still the same object!
newState.nestedState.nestedField = action.data

return newState
}

仅仅对顶层进行浅拷贝是不够的 - nestedState 对象也应该被复制。

¥Doing a shallow copy of the top level is not sufficient - the nestedState object should be copied as well.

插入和删除数组中的项目

¥Inserting and Removing Items in Arrays

通常,Javascript 数组的内容是使用 pushunshiftsplice 等修改函数来修改的。由于我们不想直接在 reducer 中改变状态,因此通常应该避免这些。因此,你可能会看到像这样编写的 "insert" 或 "remove" 行为:

¥Normally, a Javascript array's contents are modified using mutative functions like push, unshift, and splice. Since we don't want to mutate state directly in reducers, those should normally be avoided. Because of that, you might see "insert" or "remove" behavior written like this:

function insertItem(array, action) {
return [
...array.slice(0, action.index),
action.item,
...array.slice(action.index)
]
}

function removeItem(array, action) {
return [...array.slice(0, action.index), ...array.slice(action.index + 1)]
}

但是,请记住,关键是原始内存中的引用没有被修改。只要我们先制作一个副本,我们就可以安全地对副本进行修改。请注意,这对于数组和对象都是如此,但嵌套值仍然必须使用相同的规则进行更新。

¥However, remember that the key is that the original in-memory reference is not modified. As long as we make a copy first, we can safely mutate the copy. Note that this is true for both arrays and objects, but nested values still must be updated using the same rules.

这意味着我们还可以像这样编写插入和删除函数:

¥This means that we could also write the insert and remove functions like this:

function insertItem(array, action) {
let newArray = array.slice()
newArray.splice(action.index, 0, action.item)
return newArray
}

function removeItem(array, action) {
let newArray = array.slice()
newArray.splice(action.index, 1)
return newArray
}

删除功能也可以实现为:

¥The remove function could also be implemented as:

function removeItem(array, action) {
return array.filter((item, index) => index !== action.index)
}

更新数组中的项目

¥Updating an Item in an Array

更新数组中的一项可以通过使用 Array.map 来完成,为我们要更新的项返回一个新值,并为所有其他项返回现有值:

¥Updating one item in an array can be accomplished by using Array.map, returning a new value for the item we want to update, and returning the existing values for all other items:

function updateObjectInArray(array, action) {
return array.map((item, index) => {
if (index !== action.index) {
// This isn't the item we care about - keep it as-is
return item
}

// Otherwise, this is the one we want - return an updated value
return {
...item,
...action.item
}
})
}

不可变的更新实用程序库

¥Immutable Update Utility Libraries

由于编写不可变的更新代码可能会变得乏味,因此有许多实用程序库试图抽象出该过程。这些库的 API 和用法各不相同,但都试图提供一种更短、更简洁的方式来编写这些更新。例如,伊梅尔 将不可变更新变成一个简单的函数和普通的 JavaScript 对象:

¥Because writing immutable update code can become tedious, there are a number of utility libraries that try to abstract out the process. These libraries vary in APIs and usage, but all try to provide a shorter and more succinct way of writing these updates. For example, Immer makes immutable updates a simple function and plain JavaScript objects:

var usersState = [{ name: 'John Doe', address: { city: 'London' } }]
var newState = immer.produce(usersState, draftState => {
draftState[0].name = 'Jon Doe'
draftState[0].address.city = 'Paris'
//nested update similar to mutable way
})

有些(例如 dot-prop-immutable)采用字符串路径作为命令:

¥Some, like dot-prop-immutable, take string paths for commands:

state = dotProp.set(state, `todos.${index}.complete`, true)

其他的,比如 immutability-helper(现已弃用的 React Immutability Helpers 插件的一个分支),使用嵌套值和辅助函数:

¥Others, like immutability-helper (a fork of the now-deprecated React Immutability Helpers addon), use nested values and helper functions:

var collection = [1, 2, { a: [12, 17, 15] }]
var newCollection = update(collection, {
2: { a: { $splice: [[1, 1, 13, 14]] } }
})

它们可以提供一种有用的替代方法来编写手动不可变更新逻辑。

¥They can provide a useful alternative to writing manual immutable update logic.

许多不可变更新实用程序的列表可以在 Redux 插件目录不可变数据#不可变更新实用程序 部分中找到。

¥A list of many immutable update utilities can be found in the Immutable Data#Immutable Update Utilities section of the Redux Addons Catalog.

使用 Redux Toolkit 简化不可变更新

¥Simplifying Immutable Updates with Redux Toolkit

我们的 Redux 工具包 软件包包括一个内部使用 Immer 的 createReducer 实用程序。因此,你可以编写看似 "mutate" 状态的 reducer,但更新实际上是不可变地应用的。

¥Our Redux Toolkit package includes a createReducer utility that uses Immer internally. Because of this, you can write reducers that appear to "mutate" state, but the updates are actually applied immutably.

这允许以更简单的方式编写不可变的更新逻辑。使用 createReducer嵌套数据示例 可能如下所示:

¥This allows immutable update logic to be written in a much simpler way. Here's what the nested data example might look like using createReducer:

import { createReducer } from '@reduxjs/toolkit'

const initialState = {
first: {
second: {
id1: { fourth: 'a' },
id2: { fourth: 'b' }
}
}
}

const reducer = createReducer(initialState, {
UPDATE_ITEM: (state, action) => {
state.first.second[action.someId].fourth = action.someValue
}
})

这显然更短且更容易阅读。然而,只有当你使用 Redux Toolkit 中的 "magic" createReducer 函数(将该 reducer 封装在 Immer 的 produce 功能 中)时,这才可以正常工作。如果在没有 Immer 的情况下使用这个 reducer,它实际上会改变状态!。仅通过查看代码也并不明显看出该函数实际上是安全的并且不可变地更新状态。请确保你完全理解不可变更新的概念。如果你确实使用此功能,则在代码中添加一些注释可能会有所帮助,以解释你的 reducer 正在使用 Redux Toolkit 和 Immer。

¥This is clearly much shorter and easier to read. However, this only works correctly if you are using the "magic" createReducer function from Redux Toolkit that wraps this reducer in Immer's produce function. If this reducer is used without Immer, it will actually mutate the state!. It's also not obvious just by looking at the code that this function is actually safe and updates the state immutably. Please make sure you understand the concepts of immutable updates fully. If you do use this, it may help to add some comments to your code that explain your reducers are using Redux Toolkit and Immer.

此外,Redux Toolkit 的 createSlice 实用程序 会根据你提供的 reducer 函数自动生成动作创建者和动作类型,内部具有相同的由 Immer 驱动的更新功能。

¥In addition, Redux Toolkit's createSlice utility will auto-generate action creators and action types based on the reducer functions you provide, with the same Immer-powered update capabilities inside.

更多信息

¥Further Information