Dieses Kapitel beantwortet die folgenden Fragen
Gemeinsam genutzter veränderbarer Zustand funktioniert wie folgt
Beachten Sie, dass diese Definition für Funktionsaufrufe, kooperatives Multitasking (z. B. asynchrone Funktionen in JavaScript) usw. gilt. Die Risiken sind in jedem Fall ähnlich.
Der folgende Code ist ein Beispiel. Das Beispiel ist nicht realistisch, aber es demonstriert die Risiken und ist leicht verständlich
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:'In diesem Fall gibt es zwei unabhängige Parteien
main() möchte ein Array vor und nach dem Sortieren protokollieren.logElements() protokolliert die Elemente ihres Parameters arr, entfernt sie aber dabei.logElements() beeinträchtigt main() und führt dazu, dass diese in Zeile A ein leeres Array protokolliert.
Im Rest dieses Kapitels betrachten wir drei Möglichkeiten, die Probleme von gemeinsam genutztem veränderbarem Zustand zu vermeiden
Insbesondere werden wir auf das gerade gesehene Beispiel zurückkommen und es beheben.
Das Kopieren von Daten ist eine Möglichkeit, das Teilen davon zu vermeiden.
Hintergrund
Für Hintergrundinformationen zum Kopieren von Daten in JavaScript verweisen wir auf die folgenden beiden Kapitel dieses Buches
Solange wir nur *lesen* aus gemeinsam genutztem Zustand, haben wir keine Probleme. Bevor wir ihn *modifizieren*, müssen wir ihn „ent-teilen“, indem wir ihn kopieren (so tief wie nötig).
Defensives Kopieren ist eine Technik, bei der immer kopiert wird, wenn Probleme *auftreten könnten*. Sein Ziel ist es, die aktuelle Entität (Funktion, Klasse usw.) zu schützen
Beachten Sie, dass diese Maßnahmen uns vor anderen Parteien schützen, aber sie schützen auch andere Parteien vor uns.
Die nächsten Abschnitte veranschaulichen beide Arten von defensivem Kopieren.
Erinnern Sie sich, dass wir im motivierenden Beispiel am Anfang dieses Kapitels in Schwierigkeiten gerieten, weil logElements() seinen Parameter arr modifizierte
Fügen wir dieser Funktion defensives Kopieren hinzu
function logElements(arr) {
arr = [...arr]; // defensive copy
while (arr.length > 0) {
console.log(arr.shift());
}
}Nun verursacht logElements() keine Probleme mehr, wenn sie innerhalb von main() aufgerufen wird
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'Beginnen wir mit einer Klasse StringBuilder, die interne Daten, die sie preisgibt, nicht kopiert (Zeile 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('');
}
}Solange .getParts() nicht verwendet wird, funktioniert alles gut
const sb1 = new StringBuilder();
sb1.add('Hello');
sb1.add(' world!');
assert.equal(sb1.toString(), 'Hello world!');Wenn jedoch das Ergebnis von .getParts() geändert wird (Zeile A), funktioniert der StringBuilder nicht mehr richtig
const sb2 = new StringBuilder();
sb2.add('Hello');
sb2.add(' world!');
sb2.getParts().length = 0; // (A)
assert.equal(sb2.toString(), ''); // not OKDie Lösung besteht darin, die internen ._data defensiv zu kopieren, bevor sie preisgegeben werden (Zeile A)
class StringBuilder {
this._data = [];
add(str) {
this._data.push(str);
}
getParts() {
// Copy defensively
return [...this._data]; // (A)
}
toString() {
return this._data.join('');
}
}Nun stört die Änderung des Ergebnisses von .getParts() die Funktionsweise von sb nicht mehr
const sb = new StringBuilder();
sb.add('Hello');
sb.add(' world!');
sb.getParts().length = 0;
assert.equal(sb.toString(), 'Hello world!'); // OKWir können Mutationen vermeiden, wenn wir Daten nur nicht-destruktiv aktualisieren.
Hintergrund
Weitere Informationen zur Aktualisierung von Daten finden Sie unter §7 „Daten destruktiv und nicht-destruktiv aktualisieren“.
Bei nicht-destruktiver Aktualisierung wird das Teilen von Daten unproblematisch, da wir die gemeinsam genutzten Daten nie verändern. (Dies funktioniert nur, wenn jeder, der auf die Daten zugreift, dies tut!)
Interessanterweise wird das Kopieren von Daten trivial einfach
Dies funktioniert, weil wir nur nicht-destruktive Änderungen vornehmen und daher Daten bei Bedarf kopieren.
Wir können Mutationen von gemeinsam genutzten Daten verhindern, indem wir diese Daten unveränderlich machen.
Hintergrund
Für Hintergrundinformationen zum Unveränderlichmachen von Daten in JavaScript verweisen wir auf die folgenden beiden Kapitel dieses Buches
Wenn Daten unveränderlich sind, können sie ohne Risiko geteilt werden. Insbesondere ist kein defensives Kopieren erforderlich.
Nicht-destruktive Aktualisierung ist eine wichtige Ergänzung zu unveränderlichen Daten
Wenn wir beides kombinieren, werden unveränderliche Daten praktisch so vielseitig wie veränderliche Daten, aber ohne die damit verbundenen Risiken.
Es gibt mehrere Bibliotheken für JavaScript, die unveränderliche Daten mit nicht-destruktiven Aktualisierungen unterstützen. Zwei beliebte sind
Diese Bibliotheken werden in den nächsten beiden Abschnitten detaillierter beschrieben.
In seinem Repository wird die Bibliothek Immutable.js wie folgt beschrieben
Unveränderliche persistente Datensammlungen für JavaScript, die Effizienz und Einfachheit erhöhen.
Immutable.js bietet unveränderliche Datenstrukturen wie
ListeStackSet (was sich von JavaScripts eingebautem Set unterscheidet)Map (was sich von JavaScripts eingebautem Map unterscheidet)Im folgenden Beispiel verwenden wir eine unveränderliche 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)Hinweise
.set() modifizierte Kopien zurück..equals() (Zeile A und Zeile B).In seinem Repository wird die Bibliothek Immer wie folgt beschrieben
Erstellen Sie den nächsten unveränderlichen Zustand, indem Sie den aktuellen modifizieren.
Immer hilft bei der nicht-destruktiven Aktualisierung von (potenziell verschachtelten) einfachen Objekten, Arrays, Sets und Maps. Das heißt, es sind keine benutzerdefinierten Datenstrukturen beteiligt.
So sieht die Verwendung von Immer aus
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'}},
]);Die ursprünglichen Daten sind in people gespeichert. produce() stellt uns eine Variable draft zur Verfügung. Wir tun so, als ob diese Variable people wäre und verwenden Operationen, mit denen wir normalerweise destruktive Änderungen vornehmen würden. Immer fängt diese Operationen ab. Anstatt draft zu modifizieren, ändert es people nicht-destruktiv. Das Ergebnis wird von modifiedPeople referenziert. Als Bonus ist es tief unveränderlich.
assert.deepEqual() funktioniert, weil Immer einfache Objekte und Arrays zurückgibt.