Skip to content

Redux - 纯函数

Redux 的核心原则是 state transformations(状态转换) 由 pure functions(纯函数) 处理,特别是在 reducers(处理器) 中。理解一个函数为何 ‘纯’ 对于编写正确的 Redux 代码至关重要。

如果一个函数满足两个主要条件,则被认为是 Pure Function:

  • 相同的输入,相同的输出: 如果使用相同的参数调用,它总是返回相同的结果。它的输出 只 取决于它的输入参数。
  • 没有 Side Effects(副作用): 它的执行不会在函数作用域之外产生任何可观察到的效果。这意味着它不会:
    • 修改其输入参数(没有 Mutation(修改))。
    • 直接修改全局变量或应用 state。
    • 执行 I/O 操作(例如 API 调用、console.log* 或写入磁盘)。
    • 调用其他带有 side effects 的函数。

*注意:虽然 console.log 技术上来说是一个 side effect,但在调试期间通常可以容忍,但在最终的 reducer 代码中理想情况下应该移除。

Pure function 示例:

// Pure function:对于相同的输入总是返回相同的输出,没有 side effects。
const multiply = (a, b) => a * b;
console.log(multiply(2, 3)); // 总是输出 6
console.log(multiply(2, 3)); // 仍然输出 6

Impure 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 - 它是同一个被修改的数组对象

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 或设置。

编写 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 的 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 的抽象。