做者:Dr. Axel Rauschmayer翻譯:瘋狂的技術宅html
原文:https://2ality.com/2019/10/sh...前端
未經容許嚴禁轉載git
本文回答瞭如下問題:程序員
標有「(高級)」的部分會更深刻,若是你想更快地閱讀本文,能夠跳過。es6
共享可變狀態的解釋以下:github
則可能會有一方修改會致使另外一方沒法正常工做的風險。如下是一個例子:面試
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:'
這裏有兩個獨立的部分:函數logElements()
和函數main()
。後者想要在對數組進行排序的先後都打印其內容。可是它到用了 logElements()
,會致使數組被清空。因此 main()
會在A行輸出一個空數組。正則表達式
在本文的剩餘部分,咱們將介紹三種避免共享可變狀態問題的方法:編程
針對每一種方法,咱們都會回到剛纔看到的示例並進行修復。json
在開始研究如何避免共享以前,咱們須要看一下如何在 JavaScript 中複製數據。
對於數據,有兩個可複製的「深度」:
不幸的是,JavaScript 僅內置了對淺拷貝的支持。若是須要深拷貝,則須要本身實現。
讓咱們看一下淺拷貝的幾種方法。
const copyOfObject = {...originalObject}; const copyOfArray = [...originalArray];
可是傳播有幾個限制:
class MyClass {} const original = new MyClass(); assert.equal(MyClass.prototype.isPrototypeOf(original), true); const copy = {...original}; assert.equal(MyClass.prototype.isPrototypeOf(copy), false);
copy
中沒有 original
的繼承屬性 .inheritedProp
,由於咱們僅複製本身的屬性,而未保留原型。const proto = { inheritedProp: 'a' }; const original = {__proto__: proto, ownProp: 'b' }; assert.equal(original.inheritedProp, 'a'); assert.equal(original.ownProp, 'b'); const copy = {...original}; assert.equal(copy.inheritedProp, undefined); assert.equal(copy.ownProp, 'b');
.length
不可枚舉,也不能複製:const arr = ['a', 'b']; assert.equal(arr.length, 2); assert.equal({}.hasOwnProperty.call(arr, 'length'), true); const copy = {...arr}; assert.equal({}.hasOwnProperty.call(copy, 'length'), false);
const original = Object.defineProperties({}, { prop: { value: 1, writable: false, configurable: false, enumerable: true, }, }); assert.deepEqual(original, {prop: 1}); const copy = {...original}; // Attributes `writable` and `configurable` of copy are different: assert.deepEqual(Object.getOwnPropertyDescriptors(copy), { prop: { value: 1, writable: true, configurable: true, enumerable: true, }, });
這意味着,getter 和 setter 都不會被如實地被複制:value
屬性(用於數據屬性),get
屬性(用於 getter)和set
屬性(用於 setter)是互斥的。
const original = { get myGetter() { return 123 }, set mySetter(x) {}, }; assert.deepEqual({...original}, { myGetter: 123, // not a getter anymore! mySetter: undefined, });
const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = {...original}; // Property .name is a copy copy.name = 'John'; assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}}); assert.deepEqual(copy, {name: 'John', work: {employer: 'Acme'}}); // The value of .work is shared copy.work.employer = 'Spectre'; assert.deepEqual( original, {name: 'Jane', work: {employer: 'Spectre'}}); assert.deepEqual( copy, {name: 'John', work: {employer: 'Spectre'}});
這些限制有的能夠消除,而其餘則不能:
class MyClass {} const original = new MyClass(); const copy = { __proto__: Object.getPrototypeOf(original), ...original, }; assert.equal(MyClass.prototype.isPrototypeOf(copy), true);
另外,咱們能夠在副本建立後經過 Object.setPrototypeOf()
設置原型。
咱們能夠用 Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
複製對象(操做方法稍後說明):
value
),所以正確地複製了getters,setters,只讀屬性等。Object.getOwnPropertyDescriptors()
檢索可枚舉和不可枚舉的屬性。Object.assign()
進行淺拷貝(高級)Object.assign()
的工做原理就像傳播到對象中同樣。也就是說如下兩種複製方式大體相同:
const copy1 = {...original}; const copy2 = Object.assign({}, original);
使用方法而不是語法的好處是能夠經過庫在舊的 JavaScript 引擎上對其進行填充。
不過 Object.assign()
並不徹底像傳播。它在一個相對微妙的方面有所不一樣:它以不一樣的方式建立屬性。
Object.assign()
使用 assignment 建立副本的屬性。除其餘事項外,assignment 會調用本身的和繼承的設置器,而 definition 不會(關於 assignment 與 definition 的更多信息)。這種差別不多引發注意。如下代碼是一個例子,但它是人爲設計的:
const original = {['__proto__']: null}; const copy1 = {...original}; // copy1 has the own property '__proto__' assert.deepEqual( Object.keys(copy1), ['__proto__']); const copy2 = Object.assign({}, original); // copy2 has the prototype null assert.equal(Object.getPrototypeOf(copy2), null);
Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
進行淺拷貝(高級)JavaScript 使咱們能夠經過屬性描述符建立屬性,這些對象指定屬性屬性。例如,經過 Object.defineProperties()
,咱們已經看到了它。若是將該方法與 Object.getOwnPropertyDescriptors()
結合使用,則能夠更加忠實地進行復制:
function copyAllOwnProperties(original) { return Object.defineProperties( {}, Object.getOwnPropertyDescriptors(original)); }
這消除了經過傳播複製對象的兩個限制。
首先,可以正確複製本身 property 的全部 attribute。咱們如今能夠複製本身的 getter 和 setter:
const original = { get myGetter() { return 123 }, set mySetter(x) {}, }; assert.deepEqual(copyAllOwnProperties(original), original);
其次,因爲使用了 Object.getOwnPropertyDescriptors()
,非枚舉屬性也被複制了:
const arr = ['a', 'b']; assert.equal(arr.length, 2); assert.equal({}.hasOwnProperty.call(arr, 'length'), true); const copy = copyAllOwnProperties(arr); assert.equal({}.hasOwnProperty.call(copy, 'length'), true);
如今該解決深拷貝了。首先咱們將手動進行深拷貝,而後再研究通用方法。
若是嵌套傳播,則會獲得深層副本:
const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = {name: original.name, work: {...original.work}}; // We copied successfully: assert.deepEqual(original, copy); // The copy is deep: assert.ok(original.work !== copy.work);
儘管這是一個 hack,可是在緊要關頭,它提供了一個快速的解決方案:爲了對 `original
對象進行深拷貝」,咱們首先將其轉換爲 JSON 字符串,而後再解析該它:
function jsonDeepCopy(original) { return JSON.parse(JSON.stringify(original)); } const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = jsonDeepCopy(original); assert.deepEqual(original, copy);
這種方法的主要缺點是,咱們只能複製具備 JSON 支持的鍵和值的屬性。
一些不受支持的鍵和值將被忽略:
assert.deepEqual( jsonDeepCopy({ [Symbol('a')]: 'abc', b: function () {}, c: undefined, }), {} // empty object );
其餘致使的例外:
assert.throws( () => jsonDeepCopy({a: 123n}), /^TypeError: Do not know how to serialize a BigInt$/);
能夠用如下函數進行通用深拷貝:
function deepCopy(original) { if (Array.isArray(original)) { const copy = []; for (const [index, value] of original.entries()) { copy[index] = deepCopy(value); } return copy; } else if (typeof original === 'object' && original !== null) { const copy = {}; for (const [key, value] of Object.entries(original)) { copy[key] = deepCopy(value); } return copy; } else { // Primitive value: atomic, no need to copy return original; } }
該函數處理三種狀況:
original
是一個數組,咱們建立一個新的 Array,並將 original
的元素複製到其中。original
是一個對象,咱們將使用相似的方法。original
是原始值,則無需執行任何操做。讓咱們嘗試一下deepCopy()
:
const original = {a: 1, b: {c: 2, d: {e: 3}}}; const copy = deepCopy(original); // Are copy and original deeply equal? assert.deepEqual(copy, original); // Did we really copy all levels // (equal content, but different objects)? assert.ok(copy !== original); assert.ok(copy.b !== original.b); assert.ok(copy.b.d !== original.b.d);
注意,deepCopy()
僅解決了一個擴展問題:淺拷貝。而其餘全部內容:不復制原型,僅部分複製特殊對象,忽略不可枚舉的屬性,忽略大多數屬性。
一般徹底徹底實現複製是不可能的:並不是全部數據的都是一棵樹,有時你並不須要全部屬性,等等。
deepCopy()
版本若是咱們使用 .map()
和 Object.fromEntries()
,可使之前的 deepCopy()
實現更加簡潔:
function deepCopy(original) { if (Array.isArray(original)) { return original.map(elem => deepCopy(elem)); } else if (typeof original === 'object' && original !== null) { return Object.fromEntries( Object.entries(original) .map(([k, v]) => [k, deepCopy(v)])); } else { // Primitive value: atomic, no need to copy return original; } }
一般使用兩種技術能夠實現類實例的深拷貝:
.clone()
方法.clone()
方法該技術爲每一個類引入了一個方法 .clone()
,其實例將被深拷貝。它返回 this
的深層副本。如下例子顯示了能夠克隆的三個類。
class Point { constructor(x, y) { this.x = x; this.y = y; } clone() { return new Point(this.x, this.y); } } class Color { constructor(name) { this.name = name; } clone() { return new Color(this.name); } } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; } clone() { return new ColorPoint( this.x, this.y, this.color.clone()); // (A) } }
A 行展現了此技術的一個重要方面:複合實例屬性值也必須遞歸克隆。
拷貝構造函數是用當前類的另外一個實例來設置當前實例的構造函數。拷貝構造函數在靜態語言(例如 C++ 和 Java)中很流行,你能夠在其中經過 static 重載(static 表示它在編譯時發生)提供構造函數的多個版本。
在 JavaScript 中,你能夠執行如下操做(但不是很優雅):
class Point { constructor(...args) { if (args[0] instanceof Point) { // Copy constructor const [other] = args; this.x = other.x; this.y = other.y; } else { const [x, y] = args; this.x = x; this.y = y; } } }
這是使用方法:
const original = new Point(-1, 4); const copy = new Point(original); assert.deepEqual(copy, original);
相反,靜態工廠方法在 JavaScript 中效果更好(static 意味着它們是類方法)。
在如下示例中,三個類 Point
,Color
和 ColorPoint
分別具備靜態工廠方法 .from()
:
class Point { constructor(x, y) { this.x = x; this.y = y; } static from(other) { return new Point(other.x, other.y); } } class Color { constructor(name) { this.name = name; } static from(other) { return new Color(other.name); } } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; } static from(other) { return new ColorPoint( other.x, other.y, Color.from(other.color)); // (A) } }
在 A 行中,咱們再次使用遞歸複製。
這是 ColorPoint.from()
的工做方式:
const original = new ColorPoint(-1, 4, new Color('red')); const copy = ColorPoint.from(original); assert.deepEqual(copy, original);
只要咱們僅從共享狀態讀取,就不會有任何問題。在修改它以前,咱們須要經過複製(必要的深度)來「取消共享」。
防護性複製是一種在問題可能出現時始終進行復制的技術。其目的是確保當前實體(函數、類等)的安全:
請注意,這些措施能夠保護咱們免受其餘各方的侵害,同時也能夠保護其餘各方免受咱們的侵害。
下一節說明兩種防護性複製。
請記住,在本文開頭的例子中,咱們遇到了麻煩,由於 logElements()
修改了其參數 arr
:
function logElements(arr) { while (arr.length > 0) { console.log(arr.shift()); } }
讓咱們在此函數中添加防護性複製:
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 { constructor() { this._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 { constructor() { 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
咱們將首先探討以破壞性方式和非破壞性方式更新數據之間的區別。而後將學習非破壞性更新如何避免數據改變。
咱們能夠區分兩種不一樣的數據更新方式:
後一種方法相似於先複製而後破壞性地更改它,但二者同時進行。
這就是咱們破壞性地設置對象的屬性 .city
的方式:
const obj = {city: 'Berlin', country: 'Germany'}; const key = 'city'; obj[key] = 'Munich'; assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});
如下函數以非破壞性的方式更改屬性:
function setObjectNonDestructively(obj, key, value) { const updatedObj = {}; for (const [k, v] of Object.entries(obj)) { updatedObj[k] = (k === key ? value : v); } return updatedObj; }
它的用法以下:
const obj = {city: 'Berlin', country: 'Germany'}; const updatedObj = setObjectNonDestructively(obj, 'city', 'Munich'); assert.deepEqual(updatedObj, {city: 'Munich', country: 'Germany'}); assert.deepEqual(obj, {city: 'Berlin', country: 'Germany'});
傳播使 setObjectNonDestructively()
更加簡潔:
function setObjectNonDestructively(obj, key, value) { return {...obj, [key]: value}; }
注意:setObject NonDestructively()
的兩個版本都進行了較淺的更新。
如下是破壞性地設置數組元素的方式:
const original = ['a', 'b', 'c', 'd', 'e']; original[2] = 'x'; assert.deepEqual(original, ['a', 'b', 'x', 'd', 'e']);
非破壞性地更新數組要複雜得多。
function setArrayNonDestructively(arr, index, value) { const updatedArr = []; for (const [i, v] of arr.entries()) { updatedArr.push(i === index ? value : v); } return updatedArr; } const arr = ['a', 'b', 'c', 'd', 'e']; const updatedArr = setArrayNonDestructively(arr, 2, 'x'); assert.deepEqual(updatedArr, ['a', 'b', 'x', 'd', 'e']); assert.deepEqual(arr, ['a', 'b', 'c', 'd', 'e']);
.slice()
和擴展使 setArrayNonDestructively()
更加簡潔:
function setArrayNonDestructively(arr, index, value) { return [ ...arr.slice(0, index), value, ...arr.slice(index+1)] }
注意:setArrayNonDestructively()
的兩個版本都進行了較淺的更新。
到目前爲止,咱們只是淺層地更新了數據。讓咱們來解決深度更新。如下代碼顯示瞭如何手動執行此操做。咱們正在更改 name
和 employer
。
const original = {name: 'Jane', work: {employer: 'Acme'}}; const updatedOriginal = { ...original, name: 'John', work: { ...original.work, employer: 'Spectre' }, }; assert.deepEqual( original, {name: 'Jane', work: {employer: 'Acme'}}); assert.deepEqual( updatedOriginal, {name: 'John', work: {employer: 'Spectre'}});
如下函數實現了通用的深度更新。
function deepUpdate(original, keys, value) { if (keys.length === 0) { return value; } const currentKey = keys[0]; if (Array.isArray(original)) { return original.map( (v, index) => index === currentKey ? deepUpdate(v, keys.slice(1), value) // (A) : v); // (B) } else if (typeof original === 'object' && original !== null) { return Object.fromEntries( Object.entries(original).map( (keyValuePair) => { const [k,v] = keyValuePair; if (k === currentKey) { return [k, deepUpdate(v, keys.slice(1), value)]; // (C) } else { return keyValuePair; // (D) } })); } else { // Primitive value return original; } }
若是咱們將 value
視爲要更新的樹的根,則 deepUpdate()
只會深度更改單個分支(A 和 C 行)。全部其餘分支均被淺複製(B 和 D 行)。
如下是使用 deepUpdate()
的樣子:
const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = deepUpdate(original, ['work', 'employer'], 'Spectre'); assert.deepEqual(copy, {name: 'Jane', work: {employer: 'Spectre'}}); assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}});
使用非破壞性更新,共享數據將變得毫無問題,由於咱們永遠不會改變共享數據。 (顯然,這隻有在各方都這樣作的狀況下才有效。)
有趣的是,複製數據變得很是簡單:
const original = {city: 'Berlin', country: 'Germany'}; const copy = original;
僅在必要時以及在咱們進行無損更改的狀況下,才進行 original
的實際複製。
咱們能夠經過使共享數據不變來防止共享數據發生改變。接下來,咱們將研究 JavaScript 如何支持不變性。以後,討論不可變數據如何幫助共享可變狀態。
JavaScript 具備三個級別的保護對象:
Preventing extensions 使得沒法向對象添加新屬性。可是,你仍然能夠刪除和更改屬性。
Object.preventExtensions(obj)
Sealing 能夠防止擴展,並使全部屬性都沒法配置(大約:您沒法再更改屬性的工做方式)。
Object.seal(obj)
Freezing 使對象的全部屬性不可寫後將其密封。也就是說,對象是不可擴展的,全部屬性都是隻讀的,沒法更改它。
Object.freeze(obj)
有關更多信息,請參見 「Speaking JavaScript」。
鑑於咱們但願對象是徹底不變的,所以在本文中僅使用 Object.freeze()
。
Object.freeze(obj)
僅凍結 obj 及其屬性。它不會凍結那些屬性的值,例如:
const teacher = { name: 'Edna Krabappel', students: ['Bart'], }; Object.freeze(teacher); assert.throws( () => teacher.name = 'Elizabeth Hoover', /^TypeError: Cannot assign to read only property 'name'/); teacher.students.push('Lisa'); assert.deepEqual( teacher, { name: 'Edna Krabappel', students: ['Bart', 'Lisa'], });
若是要深度凍結,則須要本身實現:
function deepFreeze(value) { if (Array.isArray(value)) { for (const element of value) { deepFreeze(element); } Object.freeze(value); } else if (typeof value === 'object' && value !== null) { for (const v of Object.values(value)) { deepFreeze(v); } Object.freeze(value); } else { // Nothing to do: primitive values are already immutable } return value; }
回顧上一節中的例子,咱們能夠檢查 deepFreeze()
是否真的凍結了:
const teacher = { name: 'Edna Krabappel', students: ['Bart'], }; deepFreeze(teacher); assert.throws( () => teacher.name = 'Elizabeth Hoover', /^TypeError: Cannot assign to read only property 'name'/); assert.throws( () => teacher.students.push('Lisa'), /^TypeError: Cannot add property 1, object is not extensible$/);
用不可變的包裝器包裝可變的集合並提供相同的 API,但沒有破壞性的操做。如今對於同一集合,咱們有兩個接口:一個是可變的,另外一個是不可變的。當咱們具備要安全的公開內部可變數據時,這頗有用。
接下來展現了 Maps 和 Arrays 的包裝器。它們都有如下限制:
類 ImmutableMapWrapper
爲 map 生成包裝器:
class ImmutableMapWrapper { constructor(map) { this._self = map; } } // Only forward non-destructive methods to the wrapped Map: for (const methodName of ['get', 'has', 'keys', 'size']) { ImmutableMapWrapper.prototype[methodName] = function (...args) { return this._self[methodName](...args); } }
這是 action 中的類:
const map = new Map([[false, 'no'], [true, 'yes']]); const wrapped = new ImmutableMapWrapper(map); // Non-destructive operations work as usual: assert.equal( wrapped.get(true), 'yes'); assert.equal( wrapped.has(false), true); assert.deepEqual( [...wrapped.keys()], [false, true]); // Destructive operations are not available: assert.throws( () => wrapped.set(false, 'never!'), /^TypeError: wrapped.set is not a function$/); assert.throws( () => wrapped.clear(), /^TypeError: wrapped.clear is not a function$/);
對於數組 arr
,常規包裝是不夠的,由於咱們不只須要攔截方法調用,並且還須要攔截諸如 arr [1] = true
之類的屬性訪問。 JavaScript proxies 使咱們可以執行這種操做:
const RE_INDEX_PROP_KEY = /^[0-9]+$/; const ALLOWED_PROPERTIES = new Set([ 'length', 'constructor', 'slice', 'concat']); function wrapArrayImmutably(arr) { const handler = { get(target, propKey, receiver) { // We assume that propKey is a string (not a symbol) if (RE_INDEX_PROP_KEY.test(propKey) // simplified check! || ALLOWED_PROPERTIES.has(propKey)) { return Reflect.get(target, propKey, receiver); } throw new TypeError(`Property "${propKey}" can’t be accessed`); }, set(target, propKey, value, receiver) { throw new TypeError('Setting is not allowed'); }, deleteProperty(target, propKey) { throw new TypeError('Deleting is not allowed'); }, }; return new Proxy(arr, handler); }
讓咱們包裝一個數組:
const arr = ['a', 'b', 'c']; const wrapped = wrapArrayImmutably(arr); // Non-destructive operations are allowed: assert.deepEqual( wrapped.slice(1), ['b', 'c']); assert.equal( wrapped[1], 'b'); // Destructive operations are not allowed: assert.throws( () => wrapped[1] = 'x', /^TypeError: Setting is not allowed$/); assert.throws( () => wrapped.shift(), /^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
:
import {Map} from 'immutable/dist/immutable.es.js'; const map0 = Map([ [false, 'no'], [true, 'yes'], ]); const map1 = map0.set(true, 'maybe'); // (A) assert.ok(map1 !== map0); // (B) assert.equal(map1.equals(map0), false); const map2 = map1.set(true, 'yes'); // (C) assert.ok(map2 !== map1); assert.ok(map2 !== map0); assert.equal(map2.equals(map0), true); // (D)
說明:
map0
的不一樣版本 map1
,其中 true
映射到了 'maybe'
。map1
,並撤消在 A 行中所作的更改。.equals()
方法來檢查是否確實撤消了更改。在其存儲庫中,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
引用。它是一成不變的。
傳播:
Property attributes:
原型鏈: