Redux - 纯函数
Redux - Pure Functions(纯函数)
Section titled “Redux - Pure Functions(纯函数)”Redux 的核心原则是 state transformations(状态转换) 由 pure functions(纯函数) 处理,特别是在 reducers(处理器) 中。理解一个函数为何 ‘纯’ 对于编写正确的 Redux 代码至关重要。
什么是 Pure Function?
Section titled “什么是 Pure Function?”如果一个函数满足两个主要条件,则被认为是 Pure Function:
- 相同的输入,相同的输出: 如果使用相同的参数调用,它总是返回相同的结果。它的输出 只 取决于它的输入参数。
- 没有 Side Effects(副作用): 它的执行不会在函数作用域之外产生任何可观察到的效果。这意味着它不会:
-
- 修改其输入参数(没有 Mutation(修改))。
-
- 直接修改全局变量或应用 state。
-
- 执行 I/O 操作(例如 API 调用、
console.log* 或写入磁盘)。
- 执行 I/O 操作(例如 API 调用、
-
- 调用其他带有 side effects 的函数。
*注意:虽然 console.log 技术上来说是一个 side effect,但在调试期间通常可以容忍,但在最终的 reducer 代码中理想情况下应该移除。
Pure function 示例:
// Pure function:对于相同的输入总是返回相同的输出,没有 side effects。const multiply = (a, b) => a * b;
console.log(multiply(2, 3)); // 总是输出 6console.log(multiply(2, 3)); // 仍然输出 6Impure Function(非纯函数) 示例(由于 side effect - 修改外部 state):
let counter = 0;
// Impure function:修改函数作用域之外的变量。const incrementCounter = () => { counter += 1; return counter;};
console.log(incrementCounter()); // 输出:1(并且 counter 现在是 1)console.log(incrementCounter()); // 输出:2(并且 counter 现在是 2)Impure Function 示例(由于修改输入 - mutation):
// Impure function:修改(mutate)其输入参数。const addItemToArrayImmutably = (arr, item) => { // 正确:创建一个添加了 item 的 *新* 数组 return [...arr, item];}
// Impure function:修改(mutate)其输入参数。const addItemToArrayMutably = (arr, item) => { arr.push(item); // 错误:修改了原始数组! return arr;}
const originalArray = [1, 2];const newArray = addItemToArrayMutably(originalArray, 3);
console.log(originalArray); // 输出:[1, 2, 3] - 原始数组被改变了!console.log(newArray); // 输出:[1, 2, 3]console.log(originalArray === newArray); // 输出:true - 它是同一个被修改的数组对象为什么 Reducers 必须是 Pure?
Section titled “为什么 Reducers 必须是 Pure?”Redux 在很大程度上依赖于 reducers 的 purity(纯洁性),原因如下:
- 可预测性: Pure functions 保证下一个 state 只取决于当前 state 和 action。这使得 state 流程可预测且更易于调试。
- 性能优化: React Redux 等 UI 库对 state 对象执行 shallow equality checks(浅层相等性检查) 来确定 components 是否需要重新渲染。如果你修改了 state 对象而不是返回一个新对象,这些检查就会失败(因为对象引用没有改变),并且 UI 可能不会正确更新。
- Time Travel Debugging(时间旅行调试): Redux DevTools 等工具依赖于 reducers 的纯洁性来准确计算过去的 state,并启用 action skipping 和 replaying 等功能。
- 可测试性: Pure functions 易于测试。只需提供输入 state 和一个 action,然后断言输出 state,无需复杂的 mocks 或设置。
编写 Pure Reducers
Section titled “编写 Pure Reducers”编写 reducers 时,如果需要更改 state,请始终确保返回一个 新 的 state 对象或数组。切勿直接修改现有的 state 参数。
用于 immutable updates(不可变更新) 的常见技术包括:
- 扩展语法 (
...) 用于对象和数组。 - 返回新数组的数组方法 (
.map()、.filter()、.concat()、.slice())。避免使用会修改原数组的方法(.push()、.splice())。 Object.assign({}, ...)(现在不如扩展语法常用)。
const initialState = { count: 0, user: null};
function counterReducer(state = initialState, action) { switch (action.type) { case 'counter/increment': // 正确:返回一个包含更新后 count 的新对象 return { ...state, // 复制现有的 state 属性 count: state.count + 1 // 更新 count }; case 'user/set': // 正确:返回一个包含更新后 user 的新对象 return { ...state, user: action.payload }; // 错误:修改了 state 参数! /* case 'counter/increment': state.count += 1; // 修改了 state! return state; // 返回了被修改的原始 state */ default: // 如果没有变化,返回现有的 state 对象 return state; }}关于 Redux Toolkit 和 Immer 的说明
Section titled “关于 Redux Toolkit 和 Immer 的说明”Redux Toolkit 的 createSlice 内部使用了一个名为 Immer 的库。Immer 允许你在 reducer 函数中编写看起来像直接修改 state 的代码,但它会在后台安全地跟踪你的更改并生成一个正确的、不可变更新的 state。这大大简化了编写 immutable update 逻辑的过程。
// 在 createSlice 内部(使用了 Immer)reducers: { increment(state) { // 这看起来像是 mutation,但 Immer 在后台以不可变的方式处理它 state.count += 1; }, setUser(state, action) { state.user = action.payload; }}即使通过 Redux Toolkit 使用 Immer,潜在的原理 仍然是:由 Redux 执行的实际 state 更新是不可变的,这得益于 Immer 的抽象。