本章回答以下问题
共享可变状态的工作原理如下
请注意,此定义适用于函数调用、协作式多任务处理(例如,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 返回普通对象和数组。