• 理想的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()系统调用为例

    通过 COW 机制来实现进程间的内存共享,按需拷贝

     

    Immer 与 Copy-on-write

    在 Immer 中,Copy-on-write 机制用来解决拷贝数据结构产生的性能负担,如下图:

     

     

    只在数据发生改变(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 中的图层操作:

    • 打开图片

    • 新建图层,在新图层上涂涂抹抹

    • 合并图层

     

    参考资料

     

  • 相关阅读:
    PostgreSQL事务特性之嵌套事务
    __attribute__((format(printf, a, b)))
    N个数依次入栈,出栈顺序有多少种?
    操作系统页面置换算法(opt,lru,fifo,clock)实现
    codeforces Round #320 (Div. 2) C. A Problem about Polyline(数学) D. "Or" Game(暴力,数学)
    基于X86平台的PC机通过网络发送一个int(32位)整数的字节顺序
    c/c++多线程模拟系统资源分配(并通过银行家算法避免死锁产生)
    Windows下使用Dev-C++开发基于pthread.h的多线程程序
    斐波那契的四种求法
    红黑树的插入
  • 原文地址:https://www.cnblogs.com/ayqy/p/immer.html
Copyright © 2020-2023  润新知