[譯] React 中的 Immutability:可變對象並無什麼問題

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

// bad, push 操做會修改原數組
items.push(newItem);

// good, concat 操做不會修改原數組
const newItems = items.concat([newItem]);
複製代碼

可是html

你知道爲何要這麼作嗎?前端

可變對象有什麼不對嗎?java

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

固然,在涉及併發狀況時會有問題。但這是最簡單的開發方法,和編程中許多問題同樣,這是一種折衷。android

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

什麼是 immutability?

Immutability 表示通過一些處理後值或狀態保持不變的變量。git

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

你能夠在 JavaScript 語言自己中找到 immutable 類型。String 對象的值類型就是一個很好的例子。web

若是你聲明一個字符串變量,以下:

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);
複製代碼

String 對象不是 immutable 的。

最後一個示例經過 String() 構造函數建立了一個字符串對象,他的值是 immutable 的。但你能夠給這個對象添加新的屬性,由於這是一對象而且沒有被 凍結

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

引用相等 vs 值相等

引用相等,你經過 ===!== (或者 ==!=) 操做符比較對象的引用。若是引用指向同一個對象,那他們就是相等的:

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

str1 === str2 // true
複製代碼

在上面的例子中,兩個引用(str1str2)都指向同一個對象('abc'),因此他們是相等的。

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

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

str1 === str2 // true

var n1 = 1;
var n2 = 1;

n1 === n2 // also true
複製代碼

但若是指向的是對象,那就再也不相等了:

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

str1 === str2 // false

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

arr1 === arr2 // false
複製代碼

上面的兩種狀況,都會建立兩個不一樣的對象,因此他們的引用不相等:

若是你想檢查兩個對象的值是否相等,你須要比較他們的值屬性。

在 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
複製代碼

將數組按下面的樹形展現:

每一個層級都有兩個字節造成到達值的路徑。

如今若是咱們想將 1 修改成 6

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

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

原樹中的其餘節點能夠繼續使用:

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

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

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

缺點

Immutability 也不是沒有問題。

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

但這些庫大多都使用本身的數據類型。

儘管這些庫提供了兼容的 API 和將這些類型轉爲 JavaScript 類型的方法,但在設計你本身的應用時,也要當心處理:

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

若是庫沒有實現新的數據結構(好比使用凍結對象工做的庫),就不能體現結構共享的好處。極可能更新數據時要複製對象,有些狀況性能會受到影響。

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

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

也能夠閱讀下這篇文章 immutability 的反對觀點

結論

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

一個 immutable 的值或對象不能被改變,因此每次更新數據都會建立新的值,將舊版本的數據隔離。

例如,若是你應用的 state 是 immutable 的,就能夠將全部 state 對象保存在單個 store 中,這樣很容易實現撤銷/重作功能。

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

Git 這種版本管理系統以相似方式工做。

Redux 也是基於這個 原則

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

Immutability 還有其餘像避免意外的反作用和 減小耦合 等優勢,但也有缺點。

記住,和編程中許多事同樣,這也是一種折衷。


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

相關文章
相關標籤/搜索