博客 / 詳情

返回

理想的JavaScript不可變數據結構

一.簡介

Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way.

Immer提供了一種更方便的不可變狀態操作方式

二.核心優勢

方便之處主要體現在:

  • 只有一個(核心)API:produce(currentState, producer: (draftState) => void): nextState
  • 不引入額外的數據結構:沒有 List、Map、Set 等任何自定義數據結構,因此也不需要特殊的相等性比較方法
  • 數據操作完全基於類型:用純原生 API 操作數據,符合直覺

例如:

const myStructure = {
  a: [1, 2, 3],
  b: 0
};
const copy = produce(myStructure, () => {
  // nothings to do
});
const modified = produce(myStructure, myStructure => {
  myStructure.a.push(4);
  myStructure.b++;
});

copy === myStructure  // true
modified !== myStructure  // true
JSON.stringify(modified) === JSON.stringify({ a: [1, 2, 3, 4], b: 1 })  // true
JSON.stringify(myStructure) === JSON.stringify({ a: [1, 2, 3], b: 0 })  // true

比起Immutable提供的全套數據結構及其操作 API:

const { Map } = require('immutable');
const originalMap = Map({ a: 1, b: 2, c: 3 });
const updatedMap = originalMap.set('b', 1000);
// New instance, leaving the original immutable.
updatedMap !== originalMap;
const anotherUpdatedMap = originalMap.set('b', 1000);
// Despite both the results of the same operation, each created a new reference.
anotherUpdatedMap !== updatedMap;
// However the two are value equal.
anotherUpdatedMap.equals(updatedMap);

Immer 顯得太過簡潔

三.實現原理

兩個關鍵點:Copy-on-write 與 Proxy

Copy-on-write

概念

Copy-on-write (CoW or COW), sometimes referred to as implicit sharing or shadowing, is a resource-management technique used in computer programming to efficiently implement a "duplicate" or "copy" operation on modifiable resources.

寫時複製(copy-on-write,簡稱 CoW 或 COW),也叫隱式共享(implicit sharing)或隱藏(shadowing),是計算機編程中的一種資源管理技術,用於高效地複製或拷貝可修改資源

If a resource is duplicated but not modified, it is not necessary to create a new resource; the resource can be shared between the copy and the original. Modifications must still create a copy, hence the technique: the copy operation is deferred to the first write. By sharing resources in this way, it is possible to significantly reduce the resource consumption of unmodified copies, while adding a small overhead to resource-modifying operations.

具體的,如果複製了一個資源但沒有改動,就沒必要創建這個新的資源,此時副本能夠與原版共享同一資源,在修改時仍需要創建副本。因此,關鍵在於:將拷貝操作推遲到第一次寫入的時候。通過這種方式來共享資源,能夠顯著減少無改動副本的資源消耗,而只是略微增加了資源修改操作的開銷

應用

COW 策略主要應用在以下幾方面:

  • 虛擬內存管理:進程共享虛擬內存、fork()系統調用等
  • 存儲:邏輯卷管理、文件系統、數據庫快照
  • 編程語言:PHP、Qt 中的許多數據類型
  • 數據結構:實現不可變的數據結構,如狀態樹

以 fork()系統調用為例:

DbUc7vDX0AAm9n6.jpg

通過 COW 機制來實現進程間的內存共享,按需拷貝

Immer 與 Copy-on-write

在 Immer 中,Copy-on-write 機制用來解決拷貝數據結構產生的性能負擔,如下圖:

copy-on-write.png

只在數據發生改變(write)時才拷貝數據結構(copy),否則共享同一個,因此:

copy === myStructure  // true
modified !== myStructure  // true

Proxy

Proxy 提供了一種 Hook 原生數據操作 API 的方式,例如:

const data = { a: 1 };
const proxy = new Proxy(data, {
  set(target, key, value, receiver) {
    console.log(`Set key = ${key}, value = ${value}`);
    return Reflect.set(target, key, value, receiver);
  }
});

proxy.a = 2;
// 輸出 Set key = a, value = 2
data.a === 2  // true

不僅能夠監聽到數據變化,還允許進行操作攔截、甚至重定向:

const data = { a: 1 };
const copy = {};
const p = new Proxy(data, {
  set(target, key, value, receiver) {
    // 不寫回data
    // return Reflect.set(target, key, value, receiver);
    // 全都寫到copy上
    Reflect.set(copy, key, value, copy);
  }
});

p.a = 2;
data.a === 1  // true
copy.a === 2  // true

發現了什麼?

data就這樣成為了不可變的數據結構

P.S.關於 Proxy 語法及應用場景的更多信息,見proxy(代理機制)_ES6 筆記 9

Copy-on-write + Proxy

回到最初的示例:

const modified = produce(myStructure, myStructure => {
  myStructure.a.push(4);
  myStructure.b++;
});

我們試着將 Proxy 與 Copy-on-write 通過魔法融為一體

function produce(data, producer) {
  let copy;
  const copyOnWrite = value => {
    copy = Object.assign({}, value);
  };

  const proxy = new Proxy(data, {
    set(target, key, value, receiver) {
      // 寫時複製
      !copy && copyOnWrite(data);
      // 全都寫到copy上
      Reflect.set(copy, key, value, copy);
    }
  });
  producer(proxy);
  return copy || data;
}

P.S.注意,這裏提供的produce實現僅用來説明 Immer 原理,存在淺顯的 bug,不具有實用價值

就得到了核心 API produce

produce(currentState, producer: (draftState) => void): nextState

在 Immer 中,data之上的proxy被稱為 Draft(草稿):

非常形象,在草稿上的修改(即對draftState的修改,會按 Copy-on-write 機制拷貝)不影響源數據,草稿完成(即producer執行完畢)之後,按照草稿對源數據打補丁,得到新數據

很巧妙的設計,就像 Photoshop 中的圖層操作:

  • 打開圖片
  • 新建圖層,在新圖層上塗塗抹抹
  • 合併圖層

參考資料

  • Copy-on-write
  • ZFS Administration, Part IX- Copy-on-write
  • Immer: Immutability the easy way

有所得、有所惑,真好

關注「前端向後」微信公眾號,你將收穫一系列「用原創」的高質量技術文章,主題包括但不限於前端、Node.js以及服務端技術

本文首發於 ayqy.net ,原文鏈接:http://www.ayqy.net/blog/immer/

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.