React實戰系列-immutable上下文講解

真正的無知不是知識的貧乏, 而是拒絕獲取知識! ——波普爾 (哲學家 思想家)

目標

  • diff算法
  • 優化方式
  • Immutable以及替代品【重點】

文章流程

React -> 遇到問題 -> 優化 -> 優化方式 -> 原理以及選擇

第一部分 diff算法以及問題

React 採用的是虛擬 DOM (即 VDOM ),每次屬性 (props) 和狀態 (state) 發生變化的時候,render 函數返回不同的元素樹,React 會檢測當前返回的元素樹和上次渲染的元素樹之前的差異,然後針對差異的地方進行更新操作,最後渲染為真實 DOM,這就是整個 Reconciliation 過程,其核心就是進行新舊 DOM 樹對比的 diff 算法。

React實戰系列-immutable上下文講解

為了獲得更優秀的性能,首當其衝的工作便是 減少 diff 的過程,那麼在保證應該更新的節點能夠得到更新的前提下,這個 diff 的過程如何來避免呢?

答案是利用 shouldComponentUpdate 這個生命週期函數。這個函數做了什麼事情呢?

React實戰系列-immutable上下文講解

第二部分 解決方案

由第一部分中的流程圖以及生命週期函數shouldComponentUpdate可知,如果想要該渲染的時候才渲染,就只能在此做操作。

重點就在於:屬性 (props) 和狀態 (state) 什麼時候發生變化。

<code>1. PureComponent (memo) 進行淺層比較
2. shouldComponentUpdate 中進行深層比對
3. immutable 數據結構 + SCU (memo) 淺層比對/<code>

深淺比較

PureComponent (memo) 進行淺層比較。源碼中有段代碼中,一旦屬性的值為引用類型的時候淺比較就失靈了。這種方式僅僅適用於無狀態組件或者狀態數據非常簡單的組件,對於大量的應用型組件,它是無能為力的。

那麼,如果在shouldComponentUpdate進行深層比較,即全比較,如果數據量大,則性能損耗嚴重。

怎麼解決這個問題?

<code>function shallowEqual (objA: mixed, objB: mixed): boolean {
  // 下面的 is 相當於 === 的功能,只是對 + 0 和 - 0,以及 NaN 和 NaN 的情況進行了特殊處理
  // 第一關:基礎數據類型直接比較出結果
  if (is (objA, objB)) {
    return true;
  }
  // 第二關:只要有一個不是對象數據類型就返回 false
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }
  // 第三關:在這裡已經可以保證兩個都是對象數據類型,比較兩者的屬性數量
  const keysA = Object.keys (objA);
  const keysB = Object.keys (objB);
  if (keysA.length !== keysB.length) {
    return false;
  }
  // 第四關:比較兩者的屬性是否相等,值是否相等
  for (let i = 0; i  
< keysA.length; i++) { if ( !hasOwnProperty.call (objB, keysA [i]) || !is (objA [keysA [i]], objB [keysA [i]]) ) { return false; } } return true; }/<code>

狀態對比

關於如何做對比以及採用何種方式,在第三部分由始說起

第三部分 Immutable Data 相關

鑑於上述問題,針對數據羅列主要的幾種類型的數據結構,如下:

  • 不可變(Immutable)數據
    • 新數據結構
      • facebook/immutable-js
  • 不可變更新(Immutable Update)實用程序
    • mweststrate/immer
  • Immutable/Redux 互操作
    • gajus/redux-immutable

immutable-js

參考來源 Immutable 詳解及 React 中實踐

Shared mutable state is the root of all evil(共享的可變狀態是萬惡之源) -- Pete Hunt


React實戰系列-immutable上下文講解

JavaScript 中的對象一般是

可變的(Mutable),因為使用了引用賦值,新的對象簡單的引用了原始對象,改變新的對象將影響到原始對象。如 foo={a: 1}; bar=foo; bar.a=2 你會發現此時 foo.a 也被改成了 2。

雖然這樣做可以節約內存,但當應用複雜後,這就造成了非常大的隱患,Mutable 帶來的優點變得得不償失。

為了解決這個問題,一般的做法是使用 shallowCopy(淺拷貝)或 deepCopy(深拷貝)來避免被修改,但這樣做造成了 CPU 和內存的浪費。

Immutable 可以很好地解決這些問題。

Immutable Data

Immutable Data 就是一旦創建,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操作都會返回一個新的 Immutable 對象

Immutable 實現的原理是 Persistent Data Structure(持久化數據結構),也就是使用舊數據創建新數據時,要保證舊數據同時可用且不變。

同時為了避免 deepCopy 把所有節點都複製一遍帶來的性能損耗,Immutable 使用了 Structural Sharing(結構共享),即如果對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享。請看下面動畫:

React實戰系列-immutable上下文講解


Facebook 工程師 Lee Byron 花費 3 年時間打造,與 React 同期出現,但沒有被默認放到 React 工具集裡(React 提供了簡化的 Helper)。它內部實現了一套完整的 Persistent Data Structure,還有很多易用的數據類型。像 Collection、List、Map、Set、Record、Seq。有非常全面的map、filter、groupBy、reduce、find函數式操作方法。同時 API 也儘量與 Object 或 Array 類似。

其中有 3 種最重要的數據結構說明一下:(Java 程序員應該最熟悉了)

<code>Map:鍵值對集合,對應於 Object,ES6 也有專門的 Map 對象
List:有序可重複的列表,對應於 Array
Set:無序且不可重複的列表/<code>
<code>// 原來的寫法
let foo = {a: {b: 1}};
let bar = foo;
bar.a.b = 2;
console.log(foo.a.b);  // 打印 2
console.log(foo === bar);  //  打印 true
// 使用 immutable.js 後
import Immutable from 'immutable';
foo = Immutable.fromJS({a: {b: 1}});
bar = foo.setIn(['a', 'b'], 2);   // 使用 setIn 賦值
console.log(foo.getIn(['a', 'b']));  // 使用 getIn 取值,打印 1
console.log(foo === bar);  //  打印 false/<code>

Immutable.is

<code>let map1 = Immutable.Map({a:1, b:1, c:1});
let map2 = Immutable.Map({a:1, b:1, c:1});
// 比較內存地址
map1 === map2;             // false
// Immutable.is 比較的是兩個對象的 hashCode 或 valueOf(對於 JavaScript 對象)
Immutable.is(map1, map2);  // true/<code>

優缺點

  1. Immutable 降低了 Mutable 帶來的複雜度
  2. 節省內存
  3. Undo/Redo,Copy/Paste,甚至時間旅行這些功能做起來小菜一碟
  4. 併發安全
  1. 需要學習新的 API
  2. 增加了資源文件大小
  3. 容易與原生對象混淆
<code>import { is } from 'immutable';
shouldComponentUpdate: (nextProps = {}, nextState = {}) => {
  const thisProps = this.props || {}, thisState = this.state || {};
  if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
      Object.keys(thisState).length !== Object.keys(nextState).length) {
    return true;
  }
  for (const key in nextProps) {
    if (!is(thisProps[key], nextProps[key])) {
      return true;
    }
  }
  for (const key in nextState) {
    if (thisState[key] !== nextState[key] && !is(thisState[key], nextState[key])) {
      return true;
    }
  }
  return false;
}/<code>


React實戰系列-immutable上下文講解

immerjs

以下摘錄--精讀《Immer.js》源碼

Immer 想解決的問題,是利用元編程簡化 Immutable 使用的複雜度。 immer 是使用原生數據結構的 API 而不是內置的 API.

<code>const produce = require('immer')
const state = {
  done: false,
  val: 'string',
}
const newState = produce(state, (draft) => {
  draft.done = true
})
console.log(state.done)    // false
console.log(newState.done) // true/<code>
<code>produce(obj, draft => {
  draft.count++
})/<code>

整體思路:draft 是 obj 的代理,對 draft mutable 的修改都會流入到自定義 setter 函數

,它並不修改原始對象的值,而是遞歸父級不斷淺拷貝,最終返回新的頂層對象,作為 produce 函數的返回值。

構建代理

<code>{
  modified, // 是否被修改過
  finalized, // 是否已經完成(所有 setter 執行完,並且已經生成了 copy)
  parent, // 父級對象
  base, // 原始對象(也就是 obj)
  copy, // base(也就是 obj)的淺拷貝,使用 Object.assign(Object.create(null), obj) 實現
  proxies, // 存儲每個 propertyKey 的代理對象,採用懶初始化策略
}/<code>

在這個代理對象上,綁定了自定義的 getter setter,然後直接將其扔給 produce 執行

getter

produce 回調函數中包含了用戶的 mutable 代碼。所以現在入口變成了 getter 與 setter。

getter 主要用來懶初始化代理對象,也就是當代理對象子屬性被訪問的時候,才會生成其代理對象。

  1. 不浪費資源;
  2. 子對象可以被訪問到。

setter

當對 draft 修改時,會對 base 也就是原始值進行淺拷貝,保存到 copy 屬性,同時將 modified 屬性設置為 true。這樣就完成了最重要的 Immutable 過程,而且淺拷貝並不是很消耗性能,加上是按需淺拷貝,因此 Immer 的性能還可以。

同時為了保證整條鏈路的對象都是新對象,會根據 parent 屬性遞歸父級,不斷淺拷貝,直到這個葉子結點到根結點整條鏈路對象都換新為止。

完成了 modified 對象再有屬性被修改時,會將這個新值保存在 copy 對象上。

生成 Immutable 對象

執行完 produce 後,用戶的所有修改已經完成(所以 Immer 沒有支持異步),如果 modified 屬性為 false,說明用戶根本沒有改這個對象,那直接返回原始 base 屬性即可。

如果 modified 屬性為 true,說明對象發生了修改,返回 copy 屬性即可。但是 setter 過程是遞歸的,draft 的子對象也是 draft(包含了 base copy modified 等額外屬性的代理),我們必須一層層遞歸,拿到真正的值。

所以在這個階段,所有 draft 的 finalized 都是 false,copy 內部可能還存在大量 draft 屬性,因此遞歸 base 與 copy 的子屬性,如果相同,就直接返回;如果不同,遞歸一次整個過程(從這小節第一行開始)。

最後返回的對象是由 base 的一些屬性(沒有修改的部分)和 copy 的一些屬性(修改的部分)最終拼接而成的。最後使用 freeze 凍結 copy 屬性,將 finalized 屬性設置為 true。

至此,返回值生成完畢,我們將最終值保存在 copy 屬性上,並將其凍結,返回了 Immutable 的值。

Immer 因此完成了不可思議的操作:Create the next immutable state by mutating the current one。

源碼讀到這裡,發現 Immer 其實可以支持異步,只要支持 produce 函數返回 Promise 即可。最大的問題是,最後對代理的 revoke 清洗,需要藉助全局變量,這一點阻礙了 Immer 對異步的支持。

immer官網

優點

<code>Immutability with normal JavaScript objects, arrays, Sets and Maps. No new APIs to learn!
Strongly typed, no string based paths selectors etc.
Structural sharing out of the box
Object freezing out of the box
Deep updates are a breeze
Boilerplate reduction. Less noise, more concise code.
First class support for patches
Small: 3KB gzipped/<code>


分享到:


相關文章: