ITADN
unadlib/mutative
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

Mutative

Mutative Logo

Node CI Coverage Status npm NPM Downloads license

Mutative - 一个用于高效不可变更新的 JavaScript 库,比朴素的自定义 reducer 快 2-6 倍,比 Immer 快 10 倍以上。

为什么 Mutative 比展开运算符(朴素的自定义 reducer)更快?

展开运算符存在性能陷阱,以下文章中有详细说明:

而 Mutative 的优化重点在于浅拷贝优化、更完整的惰性草稿、最终化过程优化等。

动机

手动编写不可变更新通常很困难,容易出错且繁琐。Immer 帮助我们使用“可变”逻辑编写更简单的不可变更新。

但其性能问题导致了运行时性能开销。Immer 必须默认启用 auto-freeze(如果禁用 auto-freeze,性能会更差),使用 Immer 的此类不可变状态并不常见。在跨进程、远程数据传输等场景中,这些不可变数据必须不断被冻结。

还有更多可以改进的地方,例如更好的类型推断、非侵入式标记、支持更多类型的不可变性、更安全的不可变性、更多边缘情况,等等。

这就是创建 Mutative 的原因。

可变式与朴素手工编写 Reducer 的性能

按对象划分的可变式与 Reducer 基准测试:
  • 朴素手工编写的 reducer
// baseState type: Record<string, { value: number }>
const state = {
  ...baseState,
  key0: {
    ...baseState.key0,
    value: i,
  },
};
  • 可变
const state = create(baseState, (draft) => {
  draft.key0.value = i;
});

Mutative vs Reducer benchmark by object

更新 1K-100K 项对象所需的测量时间(秒),越低越好(查看源)。

Mutative 在更新不可变对象时,比朴素的手工编写 reducer 快至 2 倍。

Mutative 与 Reducer 按数组进行的基准测试:
  • 朴素的手工编写 reducer
// baseState type: { value: number }[]

// slower 6x than Mutative
const state = [
  { ...baseState[0], value: i },
  ...baseState.slice(1, baseState.length),
];

// slower 2.5x than Mutative
// const state = baseState.map((item, index) =>
//   index === 0 ? { ...item, value: i } : item
// );

// same performance as Mutative
// const state = [...baseState];
// state[0] = { ...baseState[0], value: i };

实际差异取决于您使用的哪种展开操作语法。

  • 可变
const state = create(baseState, (draft) => {
  draft[0].value = i;
});

Mutative vs Reducer benchmark by array

更新 1K-100K 项数组的耗时(秒),越低越好(查看源)。

Mutative 在更新不可变数组方面,比朴素的手工编写 reducer 快至 6 倍。

Mutative 与 Immer 性能对比

Mutative 通过了 Immer 的所有测试用例。

测量(ops/sec)更新 50K 个数组和 1K 个对象,数值越大越好(查看源码)。[Mutative v1.3.0 对比 Immer v10.1.3]

Benchmark

Naive handcrafted reducer - No Freeze x 4,777 ops/sec ±1.06% (94 runs sampled)
Mutative - No Freeze x 6,783 ops/sec ±0.71% (96 runs sampled)
Immer - No Freeze x 5.72 ops/sec ±0.39% (19 runs sampled)

Mutative - Freeze x 1,069 ops/sec ±0.75% (97 runs sampled)
Immer - Freeze x 392 ops/sec ±0.66% (92 runs sampled)

Mutative - Patches and No Freeze x 1,006 ops/sec ±1.73% (95 runs sampled)
Immer - Patches and No Freeze x 5.73 ops/sec ±0.16% (19 runs sampled)

Mutative - Patches and Freeze x 548 ops/sec ±1.06% (94 runs sampled)
Immer - Patches and Freeze x 287 ops/sec ±0.84% (93 runs sampled)

The fastest method is Mutative - No Freeze

运行 yarn benchmark 以测量性能。

OS: macOS 14.7, CPU: Apple M1 Max, Node.js: v22.11.0

Immer 依赖于启用 auto-freeze,如果 auto-freeze 被禁用,Immer 的性能将出现大幅下降,而 Mutative 将获得巨大的性能优势,特别是在处理大型数据结构时,其性能优势将超过 50 倍。

因此,如果你正在使用 Immer,为了性能你必须启用 auto-freeze。Mutative 默认禁用了 auto-freeze。在两者的默认配置下,我们可以看到 Mutative (6,783 ops/sec) 和 Immer (392 ops/sec) 之间 17 倍的性能差距。

总体而言,在更多性能测试场景中,Mutative 相比 Immer 具有巨大的性能优势。运行 yarn performance 以在本地获取所有性能结果。

更多性能测试场景,Mutative 比 Immer 快至多 `2.5X-82.9`:

Mutative vs Immer - All benchmark results by average multiplier

查看源文件.

特性与优势

  • Mutation 实现不可变更新 - 支持对象、数组、Set 和 Map 的不可变数据结构。
  • 高性能 - 默认情况下比 immer 快 10 倍,甚至比朴素的手写 reducer 更快。
  • 可选的冻结状态 - 默认不冻结不可变数据。
  • 支持 JSON Patch - 完全符合 JSON Patch 规范。
  • 自定义浅拷贝 - 支持更多类型的不可变数据。
  • 支持对不可变和可变数据进行标记 - 允许非侵入式标记。
  • 严格模式下更安全的可变数据访问 - 带来更安全的不可变更新。
  • 支持 reducer - 支持 reducer 函数及其他任何不可变状态库。

Mutative 与 Immer 的区别

MutativeImmer
自定义浅拷贝
严格模式
默认不冻结数据
非侵入式标记
完全冻结数据
非全局配置
异步 draft 函数
完全兼容 JSON Patch 规范
新的 Set 方法 (Mutative v1.1.0+)

Mutative 比 Immer 拥有更少的 bug,例如意外的草稿逃逸,查看详情

安装

Yarn

yarn add mutative

NPM

npm install mutative

CDN

  • Unpkg: <script src="https://unpkg.com/mutative"></script>
  • JSDelivr: <script src="https://cdn.jsdelivr.net/npm/mutative"></script>

用法

import { create } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'coding' }],
};

const state = create(baseState, (draft) => {
  draft.list.push({ text: 'learning' });
});

expect(state).not.toBe(baseState);
expect(state.list).not.toBe(baseState.list);

create(baseState, (draft) => void, options?: Options): newState

create() 的第一个参数是基础状态。Mutative 会对其进行草稿化,并将其传递给草稿函数的参数,然后执行草稿变更,直到草稿函数结束,随后 Mutative 会将其最终化并生成新状态。

通过 设置 options] 使用 create() 来访问更高级的功能。

APIs

create()

使用 create() 进行 draft mutation 以获取新状态,该操作也支持柯里化。

import { create } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'todo' }],
};

const state = create(baseState, (draft) => {
  draft.foo = 'foobar';
  draft.list.push({ text: 'learning' });
});

在这个基本示例中,对草稿的更改在草稿回调内是“可变”的,并且 create() 最终在一个新的不可变状态下执行。

create(state, fn, options)

然后 options 是可选的。

  • strict - boolean,默认值为 false。

    在严格模式下禁止访问非可草稿化的值(除非使用 unsafe())。

    当启用严格模式时,可变数据只能通过 unsafe() 访问。

    建议在开发模式下启用 strict,在生产模式下禁用 strict 这将确保安全的显式返回,同时保持生产构建的良好性能。如果返回的值不混合任何当前草稿或是 undefined,则使用 rawReturn()

    如果您希望在开发构建中默认启用严格模式,并在生产环境中关闭它,可以使用 strict: process.env.NODE_ENV !== 'production'

  • enablePatches - boolean | { pathAsArray?: boolean; arrayLengthAssignment?: boolean; },默认值为 false。

    启用 patch,并返回 patches/inversePatches。

    如果您需要更详细地设置生成的 patch 的形状,则可以设置 pathAsArrayarrayLengthAssignmentpathAsArray 的默认值是 true,如果为 true,路径将是一个数组,否则是一个字符串;arrayLengthAssignment 的默认值是 true,如果为 true,数组长度将包含在 patches 中,否则不包含数组长度(注意:如果 arrayLengthAssignmentfalse,则完全兼容 JSON Patch 规范,但可能会有额外的性能损失),查看相关讨论

  • enableAutoFreeze - boolean,默认值为 false。

    启用 autoFreeze,并返回冻结状态,仅在 development 模式下启用循环引用检查。

  • mark - (target) => ('mutable'|'immutable'|function) | (target) => ('mutable'|'immutable'|function)[]

    设置一个标记以确定值是可变还是实例是不可变的,它还可以返回一个浅拷贝函数(AutoFreezePatches 都应禁用,某些补丁操作可能并不等价)。 当标记函数为 (target) => 'immutable' 时,意味着状态结构中的所有对象都是不可变的。在这种特定情况下,您可以完全开启 AutoFreezePatchesmark 支持多个标记,标记按顺序执行,第一个返回值的标记将被使用。 当对象树节点被 mark 函数标记为 mutable 时,其所有子节点也不会被 Mutative 草稿化,并将保留其原始值。

create() - 柯里化

  • 创建 draft
const [draft, finalize] = create(baseState);
draft.foobar.bar = 'baz';
const state = finalize();

支持集选项,例如 const [draft, finalize] = create(baseState, { enableAutoFreeze: true });

  • 创建 producer
const produce = create((draft) => {
  draft.foobar.bar = 'baz';
});
const state = produce(baseState);

还支持设置诸如 const produce = create((draft) => {}, { enableAutoFreeze: true }); 之类的选项

apply()

使用 apply() 来应用补丁以获取新状态。

import { create, apply } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'todo' }],
};

const [state, patches, inversePatches] = create(
  baseState,
  (draft) => {
    draft.foo = 'foobar';
    draft.list.push({ text: 'learning' });
  },
  {
    enablePatches: true,
  }
);

const nextState = apply(baseState, patches);
expect(nextState).toEqual(state);
const prevState = apply(state, inversePatches);
expect(prevState).toEqual(baseState);

apply(state, patches, options)

options 参数是可选的,支持两种类型的配置:

  1. 不可变选项(类似于创建选项,但不包含 enablePatches):

    • strict - boolean,在严格模式下禁止访问非可草稿化的值
    • enableAutoFreeze - boolean,启用 autoFreeze 并返回冻结状态
    • mark - 用于确定值是否为可变/不可变的标记函数
const baseState = { foo: { bar: 'test' } };

// This will create a new state.
const result = apply(baseState, [
  {
    op: 'replace',
    path: ['foo', 'bar'],
    value: 'test2',
  },
]);
expect(baseState).not.toEqual({ foo: { bar: 'test2' } });
expect(result).toEqual({ foo: { bar: 'test2' } });
  1. 可变选项(Mutative v1.2.0+):
    • mutable - boolean,如果为 true,状态将被直接修改,而不是创建新状态

可变选项示例:

const baseState = { foo: { bar: 'test' } };

// This will modify baseState directly
apply(
  baseState,
  [
    {
      op: 'replace',
      path: ['foo', 'bar'],
      value: 'test2',
    },
  ],
  {
    mutable: true,
  }
);
expect(baseState).toEqual({ foo: { bar: 'test2' } });

⚠️注意:mutable 选项不能与其他选项组合使用。使用 mutable 选项时,apply() 将返回 void 而不是新的状态。

current()

从草稿中获取当前值。

  • 对于任何子节点已被修改的草稿,每次执行 current() 获得的状态都将是一个新的引用对象。
  • 对于没有子节点被修改的草稿,执行 current() 始终返回原始状态。

建议在执行只读操作时,尽量减少执行 current() 的次数,理想情况下仅执行一次。

const state = create({ a: { b: { c: 1 } }, d: { f: 1 } }, (draft) => {
  draft.a.b.c = 2;
  expect(current(draft.a)).toEqual({ b: { c: 2 } });
  // The node `a` has been modified.
  expect(current(draft.a) === current(draft.a)).toBeFalsy();
  // The node `d` has not been modified.
  expect(current(draft.d) === current(draft.d)).toBeTruthy();
});

original()

从草稿中获取原始值。

const baseState = {
  foo: 'bar',
  list: [{ text: 'todo' }],
};

const state = create(baseState, (draft) => {
  draft.foo = 'foobar';
  draft.list.push({ text: 'learning' });
  expect(original(draft.list)).toEqual([{ text: 'todo' }]);
});

unsafe()

当启用严格模式时,可变数据只能通过 unsafe() 访问。

const baseState = {
  list: [],
  date: new Date(),
};

const state = create(
  baseState,
  (draft) => {
    unsafe(() => {
      draft.date.setFullYear(2000);
    });
    // or return the mutable data:
    // const date = unsafe(() => draft.date);
  },
  {
    strict: true,
  }
);

如果你希望在开发构建中默认启用严格模式,并在生产环境中将其关闭,可以使用 strict: process.env.NODE_ENV !== 'production'

isDraft()

检查某个值是否为草稿。

const baseState = {
  date: new Date(),
  list: [{ text: 'todo' }],
};

const state = create(baseState, (draft) => {
  expect(isDraft(draft.date)).toBeFalsy();
  expect(isDraft(draft.list)).toBeTruthy();
});

isDraftable()

检查某个值是否可草稿化

const baseState = {
  date: new Date(),
  list: [{ text: 'todo' }],
};

expect(isDraftable(baseState.date)).toBeFalsy();
expect(isDraftable(baseState.list)).toBeTruthy();

您可以设置一个标记来确定值是否可草稿化,并且标记函数应与传入的 create() 标记选项相同。

rawReturn()

对于不包含任何草稿的返回值,你可以使用 rawReturn() 来包装该返回值以提升性能。它确保该返回值仅被显式返回。

const baseState = { id: 'test' };
const state = create(baseState as { id: string } | undefined, (draft) => {
  return rawReturn(undefined);
});
expect(state).toBe(undefined);

如果返回值混合了草稿,则不应使用 rawReturn()

const baseState = { a: 1, b: { c: 1 } };
const state = create(baseState, (draft) => {
  if (draft.b.c === 1) {
    return {
      ...draft,
      a: 2,
    };
  }
});
expect(state).toEqual({ a: 2, b: { c: 1 } });
expect(isDraft(state.b)).toBeFalsy();

如果你使用 rawReturn(),我们建议你在开发中启用 strict 模式。

const baseState = { a: 1, b: { c: 1 } };
const state = create(
  baseState,
  (draft) => {
    if (draft.b.c === 1) {
      return rawReturn({
        ...draft,
        a: 2,
      });
    }
  },
  {
    strict: true,
  }
);
// it will warn `The return value contains drafts, please don't use 'rawReturn()' to wrap the return value.` in strict mode.
expect(state).toEqual({ a: 2, b: { c: 1 } });
expect(isDraft(state.b)).toBeFalsy();

makeCreator()

makeCreator() 仅接受 options 作为第一个参数,从而生成一个自定义的 create() 函数。

const baseState = {
  foo: {
    bar: 'str',
  },
};

const create = makeCreator({
  enablePatches: true,
});

const [state, patches, inversePatches] = create(baseState, (draft) => {
  draft.foo.bar = 'new str';
});

markSimpleObject()

markSimpleObject() 是一个标记函数,用于将所有对象标记为不可变。

const baseState = {
  foo: {
    bar: 'str',
  },
  simpleObject: Object.create(null),
};

const state = create(
  baseState,
  (draft) => {
    draft.foo.bar = 'new str';
    draft.simpleObject.a = 'a';
  },
  {
    mark: markSimpleObject,
  }
);

expect(state.simpleObject).not.toBe(baseState.simpleObject);

查看更多 API 文档

使用 TypeScript

  • castDraft()
  • castImmutable()
  • castMutable()
  • Draft<T>
  • Immutable<T>
  • Patches
  • Patch
  • Options<O, F>

与 React 集成

  • use-mutative - 一种比使用展开运算符的 useState 快 2-6 倍的替代方案
  • use-travel - 一个用于状态时间旅行的 React Hook,具备撤销、重做、重置和归档功能。
  • zustand-mutative - 一个用于 Zustand 的 Mutative 中间件,可提升不可变状态更新的效率。

常见问题

  • 我已经在用 Immer,能否平滑迁移到 Mutative?

可以。除非你需要兼容 Internet Explorer,否则 Mutative 支持几乎所有 Immer 的功能,你可以轻松地从 Immer 迁移到 Mutative。

对于不支持 Proxy 的 React Native,迁移也是不可能的。React Native 在重构期间使用了一个新的 JS 引擎 - Hermes,它(如果 < v0.59 或在 React Native < v0.64 上使用 Hermes 引擎时)不支持 Android 上的 Proxy,但 React Native v0.64 配合 Hermes 引擎支持 Proxy

  • Mutative 能否与 Redux 集成?

可以。Mutative 支持 reducer 的返回值,并且 redux-toolkit 正在考虑支持 可配置的 produce()

  • Mutative 是否支持共享引用?

是的,Mutative 支持共享引用,但通往共享对象的每条路径都会获得其自己独立的草稿。对一条路径的修改不会自动反映到其他路径中。如果你希望在结果中保留共享引用,你必须显式地分配它们(例如,draft.b = draft.a)。阅读更多细节

从 Immer 迁移到 Mutative

mutative-compat - Mutative 包装器,提供完整的 Immer API 兼容性,您可以使用它快速从 Immer 迁移到 Mutative。

  1. produce() -> create()

Mutative 的自动冻结选项默认禁用,Immer 的自动冻结选项默认启用(如果禁用,Immer 的性能将出现更大幅度的下降)。

您需要检查自动冻结是否会对您的项目产生影响。如果它依赖于自动冻结,您可以在 Mutative 中自行启用。

import produce from 'immer';

const nextState = produce(baseState, (draft) => {
  draft[1].done = true;
  draft.push({ title: 'something' });
});

使用 Mutative

import { create } from 'mutative';

const nextState = create(baseState, (draft) => {
  draft[1].done = true;
  draft.push({ title: 'something' });
});
  1. Patches
import { produceWithPatches, applyPatches } from 'immer';

enablePatches();

const baseState = {
  age: 33,
};

const [nextState, patches, inversePatches] = produceWithPatches(
  baseState,
  (draft) => {
    draft.age++;
  }
);

const state = applyPatches(nextState, inversePatches);

expect(state).toEqual(baseState);

使用 Mutative

import { create, apply } from 'mutative';

const baseState = {
  age: 33,
};

const [nextState, patches, inversePatches] = create(
  baseState,
  (draft) => {
    draft.age++;
  },
  {
    enablePatches: true,
  }
);

const state = apply(nextState, inversePatches);

expect(state).toEqual(baseState);
  1. 返回 undefined
import produce, { nothing } from 'immer';

const nextState = produce(baseState, (draft) => {
  return nothing;
});

使用 Mutative

import { create, rawReturn } from 'mutative';

const nextState = create(baseState, (draft) => {
  return rawReturn(undefined);
});

贡献

Mutative 的目标是提供高效且不可变的更新。重点在于性能优化以及提供更好的 API 以获得更好的开发体验。我们仍在持续改进,欢迎提交可能有助于 Mutative 的 PR。

开发工作流:

  • 克隆 Mutative 仓库。
  • 运行 yarn install 以安装所有依赖项。
  • 运行 yarn prettier 以格式化代码。
  • yarn test --watch 运行交互式测试监视器。
  • 运行 yarn commit 以创建 git 提交。

许可证

Mutative 采用 MIT 许可证