本章回答以下问题
共享可变状态的工作原理如下
请注意,此定义适用于函数调用、协作式多任务处理(例如,JavaScript 中的异步函数)等。每种情况下的风险都相似。
以下代码是一个示例。该示例不切实际,但它演示了风险并且易于理解
function logElements(arr) {
while (arr.length > 0) {
console.log(arr.shift());
}
}
function main() {
const arr = ['banana', 'orange', 'apple'];
console.log('Before sorting:');
logElements(arr);
arr.sort(); // changes arr
console.log('After sorting:');
logElements(arr); // (A)
}
main();
// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
在这种情况下,有两个独立的参与方
main()
想要在排序前后记录一个数组。logElements()
记录其参数 arr
的元素,但在记录时删除它们。logElements()
破坏了 main()
并导致它在 A 行记录一个空数组。
在本章的其余部分,我们将研究三种避免共享可变状态问题的方法
特别是,我们将回到我们刚刚看到的示例并修复它。
复制数据是避免共享数据的一种方法。
只要我们只从共享状态中*读取*数据,就不会有任何问题。在*修改*它之前,我们需要通过复制它(深度复制)来“取消共享”它。
*防御性复制*是一种在*可能*出现问题时始终复制的技术。其目标是确保当前实体(函数、类等)的安全
请注意,这些措施可以保护我们免受其他参与方的侵害,但也可以保护其他参与方免受我们的侵害。
接下来的部分将说明两种防御性复制。
请记住,在本章开头的激励示例中,我们遇到了麻烦,因为 logElements()
修改了其参数 arr
让我们向此函数添加防御性复制
function logElements(arr) {
arr = [...arr]; // defensive copy
while (arr.length > 0) {
console.log(arr.shift());
}
}
现在,如果在 main()
内部调用 logElements()
,它就不会再引起问题了
function main() {
const arr = ['banana', 'orange', 'apple'];
console.log('Before sorting:');
logElements(arr);
arr.sort(); // changes arr
console.log('After sorting:');
logElements(arr); // (A)
}
main();
// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
// 'apple'
// 'banana'
// 'orange'
让我们从一个不复制其暴露的内部数据的类 StringBuilder
开始(A 行)
class StringBuilder {
_data = [];
add(str) {
this._data.push(str);
}
getParts() {
// We expose internals without copying them:
return this._data; // (A)
}
toString() {
return this._data.join('');
}
}
只要不使用 .getParts()
,一切都会正常工作
const sb1 = new StringBuilder();
sb1.add('Hello');
sb1.add(' world!');
assert.equal(sb1.toString(), 'Hello world!');
但是,如果更改了 .getParts()
的结果(A 行),则 StringBuilder
将停止正常工作
const sb2 = new StringBuilder();
sb2.add('Hello');
sb2.add(' world!');
sb2.getParts().length = 0; // (A)
assert.equal(sb2.toString(), ''); // not OK
解决方案是在暴露内部 ._data
之前对其进行防御性复制(A 行)
class StringBuilder {
this._data = [];
add(str) {
this._data.push(str);
}
getParts() {
// Copy defensively
return [...this._data]; // (A)
}
toString() {
return this._data.join('');
}
}
现在,更改 .getParts()
的结果不再会干扰 sb
的操作
const sb = new StringBuilder();
sb.add('Hello');
sb.add(' world!');
sb.getParts().length = 0;
assert.equal(sb.toString(), 'Hello world!'); // OK
如果我们只进行非破坏性更新数据,就可以避免修改。
背景
有关更新数据的更多信息,请参阅 §7 “破坏性和非破坏性更新数据”。
使用非破坏性更新,共享数据变得没有问题,因为我们从不修改共享数据。(这仅在访问数据的每个人都这样做的情况下才有效!)
有趣的是,复制数据变得非常简单
这是可行的,因为我们只是进行非破坏性更改,因此可以按需复制数据。
我们可以通过使数据不可变来防止共享数据被修改。
如果数据是不可变的,则可以安全地共享它。特别是,无需进行防御性复制。
非破坏性更新是不可变数据的重要补充
如果我们将两者结合起来,不可变数据实际上将变得与可变数据一样通用,但没有相关的风险。
JavaScript 中有几个库支持具有非破坏性更新的不可变数据。两个流行的库是
接下来的两节将更详细地描述这些库。
在其存储库中,库 Immutable.js 被描述为
用于 JavaScript 的不可变持久数据集合,可提高效率和简单性。
Immutable.js 提供不可变数据结构,例如
列表
堆栈
Set
(与 JavaScript 的内置 Set
不同)Map
(与 JavaScript 的内置 Map
不同)在以下示例中,我们使用不可变的 Map
import {Map} from 'immutable/dist/immutable.es.js';
const map0 = Map([
[false, 'no'],
[true, 'yes'],
]);
// We create a modified version of map0:
const map1 = map0.set(true, 'maybe');
// The modified version is different from the original:
assert.ok(map1 !== map0);
assert.equal(map1.equals(map0), false); // (A)
// We undo the change we just made:
const map2 = map1.set(true, 'yes');
// map2 is a different object than map0,
// but it has the same content
assert.ok(map2 !== map0);
assert.equal(map2.equals(map0), true); // (B)
注意
.set()
)不会修改接收者,而是返回修改后的副本。.equals()
方法(A 行和 B 行)。在其存储库中,库 Immer 被描述为
通过修改当前状态来创建下一个不可变状态。
Immer 帮助非破坏性地更新(可能是嵌套的)普通对象、数组、集合和映射。也就是说,不涉及自定义数据结构。
这就是使用 Immer 的样子
import {produce} from 'immer/dist/immer.module.js';
const people = [
{name: 'Jane', work: {employer: 'Acme'}},
];
const modifiedPeople = produce(people, (draft) => {
draft[0].work.employer = 'Cyberdyne';
draft.push({name: 'John', work: {employer: 'Spectre'}});
});
assert.deepEqual(modifiedPeople, [
{name: 'Jane', work: {employer: 'Cyberdyne'}},
{name: 'John', work: {employer: 'Spectre'}},
]);
assert.deepEqual(people, [
{name: 'Jane', work: {employer: 'Acme'}},
]);
原始数据存储在 people
中。produce()
为我们提供了一个变量 draft
。我们假设此变量是 people
,并使用通常会进行破坏性更改的操作。Immer 会拦截这些操作。它不会修改 draft
,而是非破坏性地更改 people
。结果由 modifiedPeople
引用。此外,它是深度不可变的。
assert.deepEqual()
之所以有效,是因为 Immer 返回普通对象和数组。