メインコンテンツへ移動

Immer

無料2019-10-13#Front-End#JS#JavaScript immutable#immutable JavaScript#JS 不可变数据结构#React immutable#Redux immutable

理想的な不変データ構造がついに現れた

一.简介

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

Immer は、より便利な方法で不変状態を操作できるようにする小さなパッケージです

二.核心優位性

その便利な点は主に以下に現れます:

  • 1 つの(コア)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 は非常にシンプルに見えます

三.実装原理

2 つの重要なポイント: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() システムコールを例に:

COW メカニズムを通じてプロセス間のメモリ共有を実現し、必要に応じてコピー

Immer と Copy-on-write

Immer において、Copy-on-write メカニズムはデータ構造のコピーによって生じるパフォーマンス負担を解決するために使用され、下図の通り:

データが変更された(write)時のみデータ構造をコピー(copy)し、否则同じものを共有 するため、したがって:

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

Proxy

Proxy はネイティブデータ操作 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](/articles/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 中のレイヤー操作のようです:

  • 画像を開く

  • 新規レイヤーを作成し、新規レイヤー上で塗る

  • レイヤーをマージ

参考資料

コメント

コメントはまだありません

コメントを書く