本章節將回答以下問題
共享可變狀態的運作方式如下
請注意,這個定義適用於函式呼叫、合作式多工處理(例如 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 提供不可變資料結構,例如
清單
堆疊
集合
(不同於 JavaScript 內建的 集合
)對應
(不同於 JavaScript 內建的 對應
)在以下範例中,我們使用不可變的 對應
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 會傳回純粹物件和陣列。