[譯] React 中的 Immutability:可變物件並沒有什麼問題

jonjia發表於2018-04-19

當開始使用 React 時,你要學習的第一件事就是不應該改變(修改)一個 陣列:

// bad, push 操作會修改原陣列
items.push(newItem);

// good, concat 操作不會修改原陣列
const newItems = items.concat([newItem]);
複製程式碼

但是

你知道為什麼要這麼做嗎?

可變物件有什麼不對嗎?

[譯] React 中的 Immutability:可變物件並沒有什麼問題

沒什麼不對的,真的。可變物件沒有任何問題。

當然,在涉及併發情況時會有問題。但這是最簡單的開發方法,和程式設計中許多問題一樣,這是一種折衷。

函數語言程式設計和 immutability 等概念很流行,都是很酷的主題。但就 React 而言,immutability 會給你一些實際的好處。不僅僅是因為流行。而是有實用價值。

什麼是 immutability?

Immutability 表示經過一些處理後值或狀態保持不變的變數。

概念很簡單,但深究起來並不簡單。

你可以在 JavaScript 語言本身中找到 immutable 型別。String 物件的值型別就是一個很好的例子。

如果你宣告一個字串變數,如下:

var str = 'abc';
複製程式碼

你無法直接修改字串中的字元。

在 JavaScript 中,字串型別的值不是陣列,所以你不能像下面這樣做:

str[2] = 'd';
複製程式碼

可以試試這樣:

str = 'abd';
複製程式碼

將另一個字串賦值給 str

你甚至可以將 str 重新宣告為一個常量:

const str = 'abc'
複製程式碼

結果,重新宣告會產生一個錯誤(但是這個錯誤和 immutability 無關)。

如果你想修改字串的值,可以使用字串方法,例如:replacetoUpperCasetrim

所有這些方法都會返回一個新的字串,而不會改變原字串的值。

值型別

可能你沒注意到,之前我加粗強調過值型別

字串的值是 immutable(不可變的)。字串物件就不是了。

如果一個物件是 immutable 的,你不能改變他的狀態(及他的屬性值)。也意味著不能給他新增新的屬性。

試試下面的程式碼, 你可以在 JSFiddle 中檢視

const str = "abc";
str.myNewProperty = "some value";

alert(str.myNewProperty);
複製程式碼

如果你執行他,會彈出一個 undefined

新的屬性並沒有新增上。

但再試試下面這個:你可以在 JSFiddle 中檢視

const str = new String("abc");
str.myNewProperty = "some value";

alert(str.myNewProperty);

str.myNewProperty = "a new value";

alert(str.myNewProperty);
複製程式碼

[譯] React 中的 Immutability:可變物件並沒有什麼問題

String 物件不是 immutable 的。

最後一個示例通過 String() 建構函式建立了一個字串物件,他的值是 immutable 的。但你可以給這個物件新增新的屬性,因為這是一物件並且沒有被 凍結

這就要求我們理解另一個重要概念。引用相等和值相等的不同。

引用相等 vs 值相等

引用相等,你通過 ===!== (或者 ==!=) 操作符比較物件的引用。如果引用指向同一個物件,那他們就是相等的:

var str1 = ‘abc’;
var str2 = str1;

str1 === str2 // true
複製程式碼

在上面的例子中,兩個引用(str1str2)都指向同一個物件('abc'),所以他們是相等的。

[譯] React 中的 Immutability:可變物件並沒有什麼問題

如果兩個引用都指向一個 immutable 的值,他們也是相等的,如下:

var str1 = ‘abc’;
var str2 = ‘abc’;

str1 === str2 // true

var n1 = 1;
var n2 = 1;

n1 === n2 // also true
複製程式碼

[譯] React 中的 Immutability:可變物件並沒有什麼問題

但如果指向的是物件,那就不再相等了:

var str1 =  new String(‘abc’);
var str2 = new String(‘abc’);

str1 === str2 // false

var arr1 = [];
var arr2 = [];

arr1 === arr2 // false
複製程式碼

上面的兩種情況,都會建立兩個不同的物件,所以他們的引用不相等:

[譯] React 中的 Immutability:可變物件並沒有什麼問題

如果你想檢查兩個物件的值是否相等,你需要比較他們的值屬性。

在 JavaScript 中,沒有直接比較陣列和物件值的方法。

如果你要比較字串物件,可以使用返回新字串的 valueOftrim 方法:

var str1 =  new String(‘abc’);
var str2 = new String(‘abc’);

str1.valueOf() === str2.valueOf() // true
str1.trim() === str2.trim() // true
複製程式碼

但對於其他型別的物件,你只能實現自己的比較方法或者使用第三方工具,可以參考 這篇文章

但這和 immutability 和 React 有什麼關係呢?

如果兩個物件是不可變的,那麼比較他們是否相等比較容易。React 就是利用了這個概念來進行效能優化的。

我們來具體談談吧。

React 中的效能優化

React 內部會維護一份 UI 表述,就是 虛擬 DOM

如果一個元件的屬性和狀態改變了,他對應的虛擬 DOM 資料也會更新這些變化。因為不用修改真實頁面,操作虛擬 DOM 更加方便快捷。

然後,React 會對現在和更新前版本的虛擬 DOM 進行比較,來找出哪些改變了。這就是 一致性比較 的過程。

這樣,就只有有變化的元素會在真實 DOM 中更新。

有時,一些 DOM 元素自身沒變化,但會被其他元素影響,造成重新渲染。

這種情況下,你可以通過 shouldComponentUpdate 方法來判斷屬性和方法是不是真的改變了,是否返回 true 來更新這個元件:

class MyComponent extends Component {

  // ...

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.myProp !== nextProps.color) {
      return true;
    }
    return false;
  }

  // ...

}
複製程式碼

如果元件的屬性和狀態是 immutable 的物件或值,你可以通過相等比較判斷他們是否改變了。

從這個角度看,immutability 降低了複雜度。

因為,有時候很難知道什麼改變了。

考慮下面的深巢狀:

myPackage.sender.address.country.id = 1;
複製程式碼

如何跟蹤是哪個物件改變了呢?

再考慮下陣列。

兩個長度一致的陣列,比較他們是否相等的唯一方式就是比較每個元素是否都相等。對於大型陣列,這樣的操作消耗很大。

最簡單的解決方法就是使用 immutable 物件。

如果需要更新一個物件,就用新的值建立一個新的物件,因為原物件是 immutable 的。

你也可以通過引用比較來確定他有沒有改變。

但對有些人來說,這個概念可能與效能和程式碼簡潔性方面的理念不一致。

那我們來回顧下建立新物件並保證 immutability 的觀點。

實現 immutability

在實際應用中,state 和 property 可能是物件或陣列。

JavaScript 提供了一些建立這些資料新版本的方法。

對於物件,不是手動建立具有新屬性的物件(如下):

const modifyShirt = (shirt, newColor, newSize) => {
  return {
    id: shirt.id,
    desc: shirt.desc,
    color: newColor,
    size: newSize
  };
}
複製程式碼

而是可以使用 Object.assign 這個方法避免定義未修改的屬性(如下):

const modifyShirt = (shirt, newColor, newSize) => {
  return Object.assign( {}, shirt, {
    color: newColor,
    size: newSize
  });
}
複製程式碼

Object.assign 方法用於將(從第二個引數開始)所有源物件的屬性複製到第一個引數宣告的目標物件。

或者你也可以使用 擴充套件運算子 達到目的(不同的是 Object.assign() 使用 setter 方法分配新的值,而擴充套件運算子不是,參考):

const modifyShirt = (shirt, newColor, newSize) => {
  return {
    ...shirt,
    color: newColor,
    size: newSize
  };
}
複製程式碼

對於陣列,你也可以使用擴充套件運算子建立具有新元素的陣列:

const addValue = (arr) => {
  return [...arr, 1];
};
複製程式碼

或者使用像 concatslice 這樣的方法返回一個新的陣列,而不會修改原陣列:

const addValue = (arr) => {
  return arr.concat([1]);
};

const removeValue = (arr, index) => {
  return arr.slice(0, index)
    .concat(
        arr.slice(index+1)
    );
};
複製程式碼

在這個 程式碼片段 中,你可以看到在進行一些常見操作時,如何用這些方法結合擴充套件運算子避免修改原陣列。

但是,使用這些方法會有兩個主要缺點:

  • 他們通過將屬性/元素從一個物件/陣列複製到另一個來工作。對於大型物件/陣列來說,這樣的操作比較慢。
  • 物件和陣列預設是可變的,沒什麼來確保 immutability。你必須時刻記住要使用這些方法。

由於上述原因,使用外部庫來實現 immutability 是更好的選擇。

React 團隊推薦使用 Immutable.jsimmutability-helper,但 這裡 有很多同樣功能的庫。主要有下面三種型別:

  • 配合持久的資料結構工作的庫。
  • 通過凍結物件工作的庫。
  • 提供輔助方法執行不可變操作的庫。

大部分庫都是配合 持久的資料結構 來工作。

持久的資料結構

當有些資料需要修改時,持久的資料結構會建立一個新的版本(這實現了資料的 immutable),同時提供所有版本的訪問許可權。

如果資料部分持久化,所有版本的資料都可以訪問,但只有最新版可以修改。如果資料完全持久化,那每個版本都可以訪問和修改。

基於樹和共享的理念,新版本的建立非常高效。

資料結構表層是一個 list 或 map,但在底層是使用一種叫做 trie 的樹來實現(具體來說就是 點陣圖向量 tire),其中只有葉節點儲存值,二進位制表示的屬性名是內部節點。

比如,對於下面的陣列:

[1, 2, 3, 4, 5]
複製程式碼

你可以將索引轉化為 4 位的二進位制數:

0: 0000
1: 0001
2: 0010
3: 0011
4: 0100
複製程式碼

將陣列按下面的樹形展示:

[譯] React 中的 Immutability:可變物件並沒有什麼問題

每個層級都有兩個位元組形成到達值的路徑。

現在如果我們想將 1 修改為 6

[譯] React 中的 Immutability:可變物件並沒有什麼問題

不是直接修改樹中的那個值,而是將從根節點到你要修改的那個值整體複製一份:

[譯] React 中的 Immutability:可變物件並沒有什麼問題

會在新複製的樹中更新那個值:

[譯] React 中的 Immutability:可變物件並沒有什麼問題

原樹中的其他節點可以繼續使用:

[譯] React 中的 Immutability:可變物件並沒有什麼問題

也可以說,未修改的節點會被新舊兩個版本共享

當然,這些 4 位的樹形並不普適於這些持久的資料結構。這只是結構共享的基本理念。

我不會介紹更多細節了,想了解更多關於持久化資料和結構共享的知識,可以閱讀 這篇文章這個演講

缺點

Immutability 也不是沒有問題。

正如我前面提到的,處理物件和陣列時,你要麼必須記住使用保證 immutability 的方法,要麼就使用第三方庫。

但這些庫大多都使用自己的資料型別。

儘管這些庫提供了相容的 API 和將這些型別轉為 JavaScript 型別的方法,但在設計你自己的應用時,也要小心處理:

  • 避免高耦合
  • 避免使用像 toJs() 這樣有效能弊病的方法

如果庫沒有實現新的資料結構(比如使用凍結物件工作的庫),就不能體現結構共享的好處。很可能更新資料時要複製物件,有些情況效能會受到影響。

此外,你必須考慮這些庫的學習曲線。

當需要選擇 immutability 方案時,要仔細考慮。

也可以閱讀下這篇文章 immutability 的反對觀點

結論

Immutability 是 React 開發者需要理解的一個概念。

一個 immutable 的值或物件不能被改變,所以每次更新資料都會建立新的值,將舊版本的資料隔離。

例如,如果你應用的 state 是 immutable 的,就可以將所有 state 物件儲存在單個 store 中,這樣很容易實現撤銷/重做功能。

聽起來是不是很熟悉?是的。

Git 這種版本管理系統以類似方式工作。

Redux 也是基於這個 原則

但是,人們更關注 Redux 的 純函式 和 應用狀態的快照。StackOverflow 上的 這個回答 很好地解釋了 Redux 和 immutability 的關係。

Immutability 還有其他像避免意外的副作用和 減少耦合 等優點,但也有缺點。

記住,和程式設計中許多事一樣,這也是一種折衷。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章