Skip to content

为什么说 Immer.js 在现代 Redux 生态中扮演着如此重要的角色?

为什么说 Immer 是现代 Redux 的灵魂?

在 Redux 的世界里,有一个核心原则不可动摇:状态(State)是只读的,且唯一修改它的方式是创建并派发(dispatch)一个动作(Action)。这一设计保证了状态变更的可预测性,也带来了时间旅行调试等强大功能。但长久以来,开发者们在践行这一“不可变性(Immutability)”原则时,却常常陷入繁琐和痛苦的境地。

这正是 Immer.js 出现的意义。它并非要颠覆 Redux,恰恰相反,它是为了让开发者能更优雅、更直观地遵循 Redux 的核心原则。可以说,Immer 是现代 Redux 开发体验的灵魂所在。

Redux 不可变性的“初心”与现实困境

首先,我们回顾一下为什么 Redux 如此强调不可变性。

  1. 变更可追溯:每次状态变更都产生一个全新的状态对象,我们可以清晰地追踪每一次变化,这对于调试,尤其是“时间旅行”,至关重要。

  2. 性能优化:在 React 这类视图库中,判断组件是否需要重新渲染,最高效的方式就是进行一次浅比较(shallow comparison)。如果 state 的引用地址没有变,就意味着它没有更新,其关联的组件也无需重新渲染。

这些优点是 Redux 架构的基石。然而,在实践中,遵循不可变性往往意味着大量的样板代码。

想象一个常见的场景:我们需要更新一个深层嵌套对象中的某个属性。比如,在一个用户设置中,更新主题颜色。

我们的 State 结构可能如下:

javascript
const state = {
  user: {
    id: 1,
    profile: {
      name: '张三',
      settings: {
        theme: 'light',
        notifications: true,
      },
    },
  },
  // ... 其他 state
};

为了在 reducer 中将 themelight 修改为 dark,同时保持不可变性,我们需要手动复制每一层路径上的对象:

javascript
// Before Immer.js
const nextState = {
  ...state, // 1. 复制顶层 state
  user: {
    ...state.user, // 2. 复制 user 对象
    profile: {
      ...state.user.profile, // 3. 复制 profile 对象
      settings: {
        ...state.user.profile.settings, // 4. 复制 settings 对象
        theme: 'dark', // 5. 最后,应用我们的修改
      },
    },
  },
};

这种写法不仅冗长、丑陋,而且极易出错。我们很容易忘记复制其中某一层,导致直接修改了原始 state,从而引发难以预料的 bug。这种开发体验,无疑是 Redux 长期以来被诟病“繁琐”的主要原因之一。

Immer.js 的魔法:像“可变”一样操作“不可变”

Immer 提供了一种截然不同的思路。它允许我们用看似“可变”的、最直观的方式来编写代码,然后在底层为我们处理好所有关于“不可变”的复杂工作。

Immer 的核心 API 是 produce 函数。它接收一个原始 state,并提供一个该 state 的“草稿”(draft)。我们可以在这个草稿上随心所欲地进行“修改”,就像操作普通 JavaScript 对象一样。当操作完成后,produce 函数会根据我们的修改,安全地生成一个全新的、符合不可变性原则的 nextState

我们用 Immer 来重写上面的例子:

javascript
import { produce } from 'immer';

// After Immer.js
const nextState = produce(state, (draft) => {
  draft.user.profile.settings.theme = 'dark';
});

代码瞬间变得简洁、清晰,并且意图明确。我们不再关心层层嵌套的扩展运算符(...),而是直接表达我们的目的:“在 user 的 profile 的 settings 中,把 theme 改成 dark”

这几乎就像是在写普通的、可变的代码,但其结果却是一个全新的、不可变的状态树,并且未被修改的部分与原始 state 共享引用,兼顾了性能。

深入一层:Immer 的工作机制

Immer 的魔法主要依赖于 ES6 的 Proxy 对象和一种被称为**“写时复制”(Copy-on-Write)**的机制。

当我们调用 produce(state, recipe) 时,背后发生了几件事:

  1. 创建代理:Immer 不会直接将原始 state 交给我们,而是用 Proxy 将其包装起来,生成一个 draft 对象。

  2. 追踪修改:当我们对 draft 对象进行操作时(例如 draft.user.profile.settings.theme = 'dark'),Proxy 会拦截这些操作。它不会修改原始 state,而是将这些修改记录在一个内部树中。

  3. 生成新状态:当我们的 recipe 函数执行完毕后,Immer 会检查所有被记录的修改。它会根据这些修改,创建一个新的状态对象。在这个过程中,只有被“触碰”到的路径上的对象才会被复制,所有未被修改的部分则直接重用原始 state 的引用。这就是所谓的**“结构共享”(Structural Sharing)**。

为什么说 Immer 是现代 Redux 的基石?

如果 Immer 仅仅是一个可选的第三方库,它的影响力或许有限。但如今,它已经深度集成到了官方推荐的 Redux 工具集 Redux Toolkit (RTK) 之中。

在 RTK 的 createSlicecreateReducer API 中,Immer 是内置且默认开启的。这意味着,每一个使用现代 Redux 方案的开发者,都在不知不觉中享受着 Immer 带来的便利。

javascript
import { createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    updateTheme(state, action) {
      // 在这里,我们可以直接“修改” state,因为 RTK 在底层已经集成了 Immer
      state.user.profile.settings.theme = action.payload;
    },
  },
});

这带来的影响是深远的:

  1. 极大地降低了 Redux 的上手门槛:开发者不再需要与繁琐的扩展运算符作斗争,可以将精力更聚焦于业务逻辑本身。

  2. 提升了代码的可读性和可维护性:Reducer 的逻辑变得像是在描述一个简单的操作序列,一目了然。

  3. 统一了状态管理的最佳实践:通过将 Immer 内置,RTK фактически将“使用 Immer 处理不可变性”确立为了官方推荐的最佳实践,结束了社区中关于如何管理状态更新的长期争论。

总结:告别样板代码,回归意图本身

综上,Immer 并非是对 Redux 不可变性原则的背离,而是对其最优雅的实现。它巧妙地通过一层代理,将开发者从手动维护不可变性的繁重工作中解放出来,让我们得以用最符合直觉的方式来描述状态的变更。

正是因为 Immer 的存在,Redux Toolkit 才得以将 Redux 的开发体验提升到一个全新的高度,使其在现代前端框架的浪潮中依然葆有强大的生命力。它让 Redux 不再是那个“充满样板代码的恶龙”,而是一个真正专注于可预测状态管理的强大工具。因此,说 Immer 是现代 Redux 的灵魂,实至名归。

不知道说啥了很无语了