则可能会有一方修改会导致另一方无法正常工作的风险。以下是一个例子:
1function logElements(arr) {
2 while (arr.length > 0) {
3 console.log(arr.shift());
4 }
5}
6
7function main() {
8 const arr = ['banana', 'orange', 'apple'];
9
10 console.log('Before sorting:');
11 logElements(arr);
12
13 arr.sort(); // changes arr
14
15 console.log('After sorting:');
16 logElements(arr); // (A)
17}
18main();
19
20// Output:
21// 'Before sorting:'
22// 'banana'
23// 'orange'
24// 'apple'
25// 'After sorting:'
这里有两个独立的部分:函数logElements()
和函数main()
。后者想要在对数组进行排序的前后都打印其内容。但是它到用了 logElements()
,会导致数组被清空。所以 main()
会在A行输出一个空数组。
在本文的剩余部分,我们将介绍三种避免共享可变状态问题的方法:
针对每一种方法,我们都会回到刚才看到的示例并进行修复。
在开始研究如何避免共享之前,我们需要看一下如何在 JavaScript 中复制数据。
对于数据,有两个可复制的“深度”:
不幸的是,JavaScript 仅内置了对浅拷贝的支持。如果需要深拷贝,则需要自己实现。
让我们看一下浅拷贝的几种方法。
我们可以扩展为对象字面量和扩展为数组字面量进行复制:
1const copyOfObject = {...originalObject};
2const copyOfArray = [...originalArray];
但是传播有几个限制:
copy
中没有 original
的继承属性 .inheritedProp
,因为我们仅复制自己的属性,而未保留原型。
1const proto = { inheritedProp: 'a' }; 2const original = {__proto__: proto, ownProp: 'b' }; 3assert.equal(original.inheritedProp, 'a'); 4assert.equal(original.ownProp, 'b'); 5 6const copy = {...original}; 7assert.equal(copy.inheritedProp, undefined); 8assert.equal(copy.ownProp, 'b');.length
不可枚举,也不能复制:
1const arr = ['a', 'b']; 2assert.equal(arr.length, 2); 3assert.equal({}.hasOwnProperty.call(arr, 'length'), true); 4 5const copy = {...arr}; 6assert.equal({}.hasOwnProperty.call(copy, 'length'), false);这意味着,getter 和 setter 都不会被如实地被复制:value
属性(用于数据属性),get
属性(用于 getter)和set
属性(用于 setter)是互斥的。
js const original = { get myGetter() { return 123 }, set mySetter(x) {}, }; assert.deepEqual({...original}, { myGetter: 123, // not a getter anymore! mySetter: undefined, });
这些限制有的可以消除,而其他则不能:
另外,我们可以在副本创建后通过 Object.setPrototypeOf()
设置原型。
Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
复制对象(操作方法稍后说明):value
),因此正确地复制了getters,setters,只读属性等。Object.getOwnPropertyDescriptors()
检索可枚举和不可枚举的属性。Object.assign()
的工作原理就像传播到对象中一样。也就是说以下两种复制方式大致相同:
1const copy1 = {...original};
2const copy2 = Object.assign({}, original);
使用方法而不是语法的好处是可以通过库在旧的 JavaScript 引擎上对其进行填充。
不过 Object.assign()
并不完全像传播。它在一个相对微妙的方面有所不同:它以不同的方式创建属性。
Object.assign()
使用 assignment 创建副本的属性。除其他事项外,assignment 会调用自己的和继承的设置器,而 definition 不会(关于 assignment 与 definition 的更多信息)。这种差异很少引起注意。以下代码是一个例子,但它是人为设计的:
1const original = {['__proto__']: null};
2const copy1 = {...original};
3// copy1 has the own property '__proto__'
4assert.deepEqual(
5 Object.keys(copy1), ['__proto__']);
6
7const copy2 = Object.assign({}, original);
8// copy2 has the prototype null
9assert.equal(Object.getPrototypeOf(copy2), null);
JavaScript 使我们可以通过属性描述符创建属性,这些对象指定属性属性。例如,通过 Object.defineProperties()
,我们已经看到了它。如果将该方法与 Object.getOwnPropertyDescriptors()
结合使用,则可以更加忠实地进行复制:
1function copyAllOwnProperties(original) {
2 return Object.defineProperties(
3 {}, Object.getOwnPropertyDescriptors(original));
4}
这消除了通过传播复制对象的两个限制。
首先,能够正确复制自己 property 的所有 attribute。我们现在可以复制自己的 getter 和 setter:
1const original = {
2 get myGetter() { return 123 },
3 set mySetter(x) {},
4};
5assert.deepEqual(copyAllOwnProperties(original), original);
其次,由于使用了 Object.getOwnPropertyDescriptors()
,非枚举属性也被复制了:
1const arr = ['a', 'b'];
2assert.equal(arr.length, 2);
3assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
4
5const copy = copyAllOwnProperties(arr);
6assert.equal({}.hasOwnProperty.call(copy, 'length'), true);
现在该解决深拷贝了。首先我们将手动进行深拷贝,然后再研究通用方法。
如果嵌套传播,则会得到深层副本:
1const original = {name: 'Jane', work: {employer: 'Acme'}};
2const copy = {name: original.name, work: {...original.work}};
3
4// We copied successfully:
5assert.deepEqual(original, copy);
6// The copy is deep:
7assert.ok(original.work !== copy.work);
尽管这是一个 hack,但是在紧要关头,它提供了一个快速的解决方案:为了对 `original
对象进行深拷贝”,我们首先将其转换为 JSON 字符串,然后再解析该它:
1function jsonDeepCopy(original) {
2 return JSON.parse(JSON.stringify(original));
3}
4const original = {name: 'Jane', work: {employer: 'Acme'}};
5const copy = jsonDeepCopy(original);
6assert.deepEqual(original, copy);
这种方法的主要缺点是,我们只能复制具有 JSON 支持的键和值的属性。
一些不受支持的键和值将被忽略:
1assert.deepEqual(
2 jsonDeepCopy({
3 [Symbol('a')]: 'abc',
4 b: function () {},
5 c: undefined,
6 }),
7 {} // empty object
8);
其他导致的例外:
1assert.throws(
2 () => jsonDeepCopy({a: 123n}),
3 /^TypeError: Do not know how to serialize a BigInt$/);
可以用以下函数进行通用深拷贝:
1function deepCopy(original) {
2 if (Array.isArray(original)) {
3 const copy = [];
4 for (const [index, value] of original.entries()) {
5 copy[index] = deepCopy(value);
6 }
7 return copy;
8 } else if (typeof original === 'object' && original !== null) {
9 const copy = {};
10 for (const [key, value] of Object.entries(original)) {
11 copy[key] = deepCopy(value);
12 }
13 return copy;
14 } else {
15 // Primitive value: atomic, no need to copy
16 return original;
17 }
18}
该函数处理三种情况:
original
是一个数组,我们创建一个新的 Array,并将 original
的元素复制到其中。original
是一个对象,我们将使用类似的方法。original
是原始值,则无需执行任何操作。让我们尝试一下deepCopy()
:
1const original = {a: 1, b: {c: 2, d: {e: 3}}};
2const copy = deepCopy(original);
3
4// Are copy and original deeply equal?
5assert.deepEqual(copy, original);
6
7// Did we really copy all levels
8// (equal content, but different objects)?
9assert.ok(copy !== original);
10assert.ok(copy.b !== original.b);
11assert.ok(copy.b.d !== original.b.d);
注意,deepCopy()
仅解决了一个扩展问题:浅拷贝。而其他所有内容:不复制原型,仅部分复制特殊对象,忽略不可枚举的属性,忽略大多数属性。
通常完全完全实现复制是不可能的:并非所有数据的都是一棵树,有时你并不需要所有属性,等等。
如果我们使用 .map()
和 Object.fromEntries()
,可以使以前的 deepCopy()
实现更加简洁:
1function deepCopy(original) {
2 if (Array.isArray(original)) {
3 return original.map(elem => deepCopy(elem));
4 } else if (typeof original === 'object' && original !== null) {
5 return Object.fromEntries(
6 Object.entries(original)
7 .map(([k, v]) => [k, deepCopy(v)]));
8 } else {
9 // Primitive value: atomic, no need to copy
10 return original;
11 }
12}
通常使用两种技术可以实现类实例的深拷贝:
.clone()
方法该技术为每个类引入了一个方法 .clone()
,其实例将被深拷贝。它返回 this
的深层副本。以下例子显示了可以克隆的三个类。
1class Point {
2 constructor(x, y) {
3 this.x = x;
4 this.y = y;
5 }
6 clone() {
7 return new Point(this.x, this.y);
8 }
9}
10class Color {
11 constructor(name) {
12 this.name = name;
13 }
14 clone() {
15 return new Color(this.name);
16 }
17}
18class ColorPoint extends Point {
19 constructor(x, y, color) {
20 super(x, y);
21 this.color = color;
22 }
23 clone() {
24 return new ColorPoint(
25 this.x, this.y, this.color.clone()); // (A)
26 }
27}
A 行展示了此技术的一个重要方面:复合实例属性值也必须递归克隆。
拷贝构造函数是用当前类的另一个实例来设置当前实例的构造函数。拷贝构造函数在静态语言(例如 C++ 和 Java)中很流行,你可以在其中通过 static 重载(static 表示它在编译时发生)提供构造函数的多个版本。
在 JavaScript 中,你可以执行以下操作(但不是很优雅):
1class Point {
2 constructor(...args) {
3 if (args[0] instanceof Point) {
4 // Copy constructor
5 const [other] = args;
6 this.x = other.x;
7 this.y = other.y;
8 } else {
9 const [x, y] = args;
10 this.x = x;
11 this.y = y;
12 }
13 }
14}
这是使用方法:
1const original = new Point(-1, 4);
2const copy = new Point(original);
3assert.deepEqual(copy, original);
相反,静态工厂方法在 JavaScript 中效果更好(static 意味着它们是类方法)。
在以下示例中,三个类 Point
,Color
和 ColorPoint
分别具有静态工厂方法 .from()
:
1class Point {
2 constructor(x, y) {
3 this.x = x;
4 this.y = y;
5 }
6 static from(other) {
7 return new Point(other.x, other.y);
8 }
9}
10class Color {
11 constructor(name) {
12 this.name = name;
13 }
14 static from(other) {
15 return new Color(other.name);
16 }
17}
18class ColorPoint extends Point {
19 constructor(x, y, color) {
20 super(x, y);
21 this.color = color;
22 }
23 static from(other) {
24 return new ColorPoint(
25 other.x, other.y, Color.from(other.color)); // (A)
26 }
27}
在 A 行中,我们再次使用递归复制。
这是 ColorPoint.from()
的工作方式:
1const original = new ColorPoint(-1, 4, new Color('red'));
2const copy = ColorPoint.from(original);
3assert.deepEqual(copy, original);
只要我们仅从共享状态读取,就不会有任何问题。在修改它之前,我们需要通过复制(必要的深度)来“取消共享”。
防御性复制是一种在问题可能出现时始终进行复制的技术。其目的是确保当前实体(函数、类等)的安全:
请注意,这些措施可以保护我们免受其他各方的侵害,同时也可以保护其他各方免受我们的侵害。
下一节说明两种防御性复制。
请记住,在本文开头的例子中,我们遇到了麻烦,因为 logElements()
修改了其参数 arr
:
1function logElements(arr) {
2 while (arr.length > 0) {
3 console.log(arr.shift());
4 }
5}
让我们在此函数中添加防御性复制:
1function logElements(arr) {
2 arr = [...arr]; // defensive copy
3 while (arr.length > 0) {
4 console.log(arr.shift());
5 }
6}
现在,如果在 main()
内部调用 logElements()
不会再引发问题:
1function main() {
2 const arr = ['banana', 'orange', 'apple'];
3
4 console.log('Before sorting:');
5 logElements(arr);
6
7 arr.sort(); // changes arr
8
9 console.log('After sorting:');
10 logElements(arr); // (A)
11}
12main();
13
14// Output:
15// 'Before sorting:'
16// 'banana'
17// 'orange'
18// 'apple'
19// 'After sorting:'
20// 'apple'
21// 'banana'
22// 'orange'
让我们从 StringBuilder
类开始,该类不会复制它公开的内部数据(A行):
1class StringBuilder {
2 constructor() {
3 this._data = [];
4 }
5 add(str) {
6 this._data.push(str);
7 }
8 getParts() {
9 // We expose internals without copying them:
10 return this._data; // (A)
11 }
12 toString() {
13 return this._data.join('');
14 }
15}
只要不使用 .getParts()
,一切就可以正常工作:
1const sb1 = new StringBuilder();
2sb1.add('Hello');
3sb1.add(' world!');
4assert.equal(sb1.toString(), 'Hello world!');
但是,如果更改了 .getParts()
的结果(A行),则 StringBuilder
会停止正常工作:
1const sb2 = new StringBuilder();
2sb2.add('Hello');
3sb2.add(' world!');
4sb2.getParts().length = 0; // (A)
5assert.equal(sb2.toString(), ''); // not OK
解决方案是在内部 ._data
被公开之前防御性地对它进行复制(A行):
1class StringBuilder {
2 constructor() {
3 this._data = [];
4 }
5 add(str) {
6 this._data.push(str);
7 }
8 getParts() {
9 // Copy defensively
10 return [...this._data]; // (A)
11 }
12 toString() {
13 return this._data.join('');
14 }
15}
现在,更改 .getParts()
的结果不再干扰 sb
的操作:
1const sb = new StringBuilder();
2sb.add('Hello');
3sb.add(' world!');
4sb.getParts().length = 0;
5assert.equal(sb.toString(), 'Hello world!'); // OK
我们将首先探讨以破坏性方式和非破坏性方式更新数据之间的区别。然后将学习非破坏性更新如何避免数据改变。
我们可以区分两种不同的数据更新方式:
后一种方法类似于先复制然后破坏性地更改它,但两者同时进行。
这就是我们破坏性地设置对象的属性 .city
的方式:
1const obj = {city: 'Berlin', country: 'Germany'};
2const key = 'city';
3obj[key] = 'Munich';
4assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});
以下函数以非破坏性的方式更改属性:
1function setObjectNonDestructively(obj, key, value) {
2 const updatedObj = {};
3 for (const [k, v] of Object.entries(obj)) {
4 updatedObj[k] = (k === key ? value : v);
5 }
6 return updatedObj;
7}
它的用法如下:
1const obj = {city: 'Berlin', country: 'Germany'};
2const updatedObj = setObjectNonDestructively(obj, 'city', 'Munich');
3assert.deepEqual(updatedObj, {city: 'Munich', country: 'Germany'});
4assert.deepEqual(obj, {city: 'Berlin', country: 'Germany'});
传播使 setObjectNonDestructively()
更加简洁:
1function setObjectNonDestructively(obj, key, value) {
2 return {...obj, [key]: value};
3}
注意:setObject NonDestructively()
的两个版本都进行了较浅的更新。
以下是破坏性地设置数组元素的方式:
1const original = ['a', 'b', 'c', 'd', 'e'];
2original[2] = 'x';
3assert.deepEqual(original, ['a', 'b', 'x', 'd', 'e']);
非破坏性地更新数组要复杂得多。
1function setArrayNonDestructively(arr, index, value) {
2 const updatedArr = [];
3 for (const [i, v] of arr.entries()) {
4 updatedArr.push(i === index ? value : v);
5 }
6 return updatedArr;
7}
8
9const arr = ['a', 'b', 'c', 'd', 'e'];
10const updatedArr = setArrayNonDestructively(arr, 2, 'x');
11assert.deepEqual(updatedArr, ['a', 'b', 'x', 'd', 'e']);
12assert.deepEqual(arr, ['a', 'b', 'c', 'd', 'e']);
.slice()
和扩展使 setArrayNonDestructively()
更加简洁:
1function setArrayNonDestructively(arr, index, value) {
2 return [
3 ...arr.slice(0, index), value, ...arr.slice(index+1)]
4}
注意:setArrayNonDestructively()
的两个版本都进行了较浅的更新。
到目前为止,我们只是浅层地更新了数据。让我们来解决深度更新。以下代码显示了如何手动执行此操作。我们正在更改 name
和 employer
。
1const original = {name: 'Jane', work: {employer: 'Acme'}};
2const updatedOriginal = {
3 ...original,
4 name: 'John',
5 work: {
6 ...original.work,
7 employer: 'Spectre'
8 },
9};
10
11assert.deepEqual(
12 original, {name: 'Jane', work: {employer: 'Acme'}});
13assert.deepEqual(
14 updatedOriginal, {name: 'John', work: {employer: 'Spectre'}});
以下函数实现了通用的深度更新。
1function deepUpdate(original, keys, value) {
2 if (keys.length === 0) {
3 return value;
4 }
5 const currentKey = keys[0];
6 if (Array.isArray(original)) {
7 return original.map(
8 (v, index) => index === currentKey
9 ? deepUpdate(v, keys.slice(1), value) // (A)
10 : v); // (B)
11 } else if (typeof original === 'object' && original !== null) {
12 return Object.fromEntries(
13 Object.entries(original).map(
14 (keyValuePair) => {
15 const [k,v] = keyValuePair;
16 if (k === currentKey) {
17 return [k, deepUpdate(v, keys.slice(1), value)]; // (C)
18 } else {
19 return keyValuePair; // (D)
20 }
21 }));
22 } else {
23 // Primitive value
24 return original;
25 }
26}
如果我们将 value
视为要更新的树的根,则 deepUpdate()
只会深度更改单个分支(A 和 C 行)。所有其他分支均被浅复制(B 和 D 行)。
以下是使用 deepUpdate()
的样子:
1const original = {name: 'Jane', work: {employer: 'Acme'}};
2
3const copy = deepUpdate(original, ['work', 'employer'], 'Spectre');
4assert.deepEqual(copy, {name: 'Jane', work: {employer: 'Spectre'}});
5assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}});
使用非破坏性更新,共享数据将变得毫无问题,因为我们永远不会改变共享数据。(显然,这只有在各方都这样做的情况下才有效。)
有趣的是,复制数据变得非常简单:
1const original = {city: 'Berlin', country: 'Germany'};
2const copy = original;
仅在必要时以及在我们进行无损更改的情况下,才进行 original
的实际复制。
我们可以通过使共享数据不变来防止共享数据发生改变。接下来,我们将研究 JavaScript 如何支持不变性。之后,讨论不可变数据如何帮助共享可变状态。
JavaScript 具有三个级别的保护对象:
Object.preventExtensions(obj)
Object.seal(obj)
Object.freeze(obj)
有关更多信息,请参见 “Speaking JavaScript”【】。
鉴于我们希望对象是完全不变的,因此在本文中仅使用 Object.freeze()
。
Object.freeze(obj)
仅冻结 obj 及其属性。它不会冻结那些属性的值,例如:
1const teacher = {
2 name: 'Edna Krabappel',
3 students: ['Bart'],
4};
5Object.freeze(teacher);
6
7assert.throws(
8 () => teacher.name = 'Elizabeth Hoover',
9 /^TypeError: Cannot assign to read only property 'name'/);
10
11teacher.students.push('Lisa');
12assert.deepEqual(
13 teacher, {
14 name: 'Edna Krabappel',
15 students: ['Bart', 'Lisa'],
16 });
如果要深度冻结,则需要自己实现:
1function deepFreeze(value) {
2 if (Array.isArray(value)) {
3 for (const element of value) {
4 deepFreeze(element);
5 }
6 Object.freeze(value);
7 } else if (typeof value === 'object' && value !== null) {
8 for (const v of Object.values(value)) {
9 deepFreeze(v);
10 }
11 Object.freeze(value);
12 } else {
13 // Nothing to do: primitive values are already immutable
14 }
15 return value;
16}
回顾上一节中的例子,我们可以检查 deepFreeze()
是否真的冻结了:
1const teacher = {
2 name: 'Edna Krabappel',
3 students: ['Bart'],
4};
5deepFreeze(teacher);
6
7assert.throws(
8 () => teacher.name = 'Elizabeth Hoover',
9 /^TypeError: Cannot assign to read only property 'name'/);
10
11assert.throws(
12 () => teacher.students.push('Lisa'),
13 /^TypeError: Cannot add property 1, object is not extensible$/);
用不可变的包装器包装可变的集合并提供相同的 API,但没有破坏性的操作。现在对于同一集合,我们有两个接口:一个是可变的,另一个是不可变的。当我们具有要安全的公开内部可变数据时,这很有用。
接下来展示了 Maps 和 Arrays 的包装器。它们都有以下限制:
类 ImmutableMapWrapper
为 map 生成包装器:
1class ImmutableMapWrapper {
2 constructor(map) {
3 this._self = map;
4 }
5}
6
7// Only forward non-destructive methods to the wrapped Map:
8for (const methodName of ['get', 'has', 'keys', 'size']) {
9 ImmutableMapWrapper.prototype[methodName] = function (...args) {
10 return this._self[methodName](...args);
11 }
12}
这是 action 中的类:
1const map = new Map([[false, 'no'], [true, 'yes']]);
2const wrapped = new ImmutableMapWrapper(map);
3
4// Non-destructive operations work as usual:
5assert.equal(
6 wrapped.get(true), 'yes');
7assert.equal(
8 wrapped.has(false), true);
9assert.deepEqual(
10 [...wrapped.keys()], [false, true]);
11
12// Destructive operations are not available:
13assert.throws(
14 () => wrapped.set(false, 'never!'),
15 /^TypeError: wrapped.set is not a function$/);
16assert.throws(
17 () => wrapped.clear(),
18 /^TypeError: wrapped.clear is not a function$/);
19
对于数组 arr
,常规包装是不够的,因为我们不仅需要拦截方法调用,而且还需要拦截诸如 arr [1] = true
之类的属性访问。JavaScript proxies 使我们能够执行这种操作:
1const RE_INDEX_PROP_KEY = /^[0-9]+$/;
2const ALLOWED_PROPERTIES = new Set([
3 'length', 'constructor', 'slice', 'concat']);
4
5function wrapArrayImmutably(arr) {
6 const handler = {
7 get(target, propKey, receiver) {
8 // We assume that propKey is a string (not a symbol)
9 if (RE_INDEX_PROP_KEY.test(propKey) // simplified check!
10 || ALLOWED_PROPERTIES.has(propKey)) {
11 return Reflect.get(target, propKey, receiver);
12 }
13 throw new TypeError(`Property "${propKey}" can’t be accessed`);
14 },
15 set(target, propKey, value, receiver) {
16 throw new TypeError('Setting is not allowed');
17 },
18 deleteProperty(target, propKey) {
19 throw new TypeError('Deleting is not allowed');
20 },
21 };
22 return new Proxy(arr, handler);
23}
让我们包装一个数组:
1const arr = ['a', 'b', 'c'];
2const wrapped = wrapArrayImmutably(arr);
3
4// Non-destructive operations are allowed:
5assert.deepEqual(
6 wrapped.slice(1), ['b', 'c']);
7assert.equal(
8 wrapped[1], 'b');
9
10// Destructive operations are not allowed:
11assert.throws(
12 () => wrapped[1] = 'x',
13 /^TypeError: Setting is not allowed$/);
14assert.throws(
15 () => wrapped.shift(),
16 /^TypeError: Property "shift" can’t be accessed$/);
如果数据是不可变的,则可以共享数据而没有任何风险。特别是无需防御性复制。
非破坏性更新是对不变数据的补充,使其与可变数据一样通用,但没有相关风险。
有几种可用于 JavaScript 的库,它们支持对不可变数据进行无损更新。其中流行的两种是:
List
,Map
,Set
和 Stack
。在其存储库中,Immutable.js 的描述为:
用于 JavaScript 的不可变的持久数据集,可提高效率和简便性。
Immutable.js 提供了不可变的数据结构,例如:
List
Map
(不同于JavaScript的内置Map
)Set
(不同于JavaScript的内置 Set
)Stack
在以下示例中,我们使用不可变的 Map
:
1import {Map} from 'immutable/dist/immutable.es.js';
2const map0 = Map([
3 [false, 'no'],
4 [true, 'yes'],
5]);
6
7const map1 = map0.set(true, 'maybe'); // (A)
8assert.ok(map1 !== map0); // (B)
9assert.equal(map1.equals(map0), false);
10
11const map2 = map1.set(true, 'yes'); // (C)
12assert.ok(map2 !== map1);
13assert.ok(map2 !== map0);
14assert.equal(map2.equals(map0), true); // (D)
说明:
map0
的不同版本 map1
,其中 true
映射到了 'maybe'
。map1
,并撤消在 A 行中所做的更改。.equals()
方法来检查是否确实撤消了更改。在其存储库中,Immer 库 的描述为:
通过更改当前状态来创建下一个不可变状态。
Immer 有助于非破坏性地更新(可能嵌套)普通对象和数组。也就是说,不涉及特殊的数据结构。
这是使用 Immer 的样子:
1import {produce} from 'immer/dist/immer.module.js';
2
3const people = [
4 {name: 'Jane', work: {employer: 'Acme'}},
5];
6
7const modifiedPeople = produce(people, (draft) => {
8 draft[0].work.employer = 'Cyberdyne';
9 draft.push({name: 'John', work: {employer: 'Spectre'}});
10});
11
12assert.deepEqual(modifiedPeople, [
13 {name: 'Jane', work: {employer: 'Cyberdyne'}},
14 {name: 'John', work: {employer: 'Spectre'}},
15]);
16assert.deepEqual(people, [
17 {name: 'Jane', work: {employer: 'Acme'}},
18]);
原始数据存储在 people
中。produce()
为我们提供了一个变量 draft
。我们假设这个变量是 people
,并使用通常会进行破坏性更改的操作。Immer 拦截了这些操作。代替变异draft
,它无损地改变 people
。结果由 modifiedPeople
引用。它是一成不变的。