Immer源码解读

Mweststrate的另一杰作

作者 Trekerz 日期 2020-04-18
Immer源码解读
// 以此例讲解
produce(obj, state => {
state.products.count++
})
  1. 生成代理

    obj包装一下,转为draftdraft是一个Proxy。步骤如下:

    const state = {
    modified, // 是否被修改过
    finalized, // 是否已经完成(所有setter执行完,且已生成最终的copy)
    parent, // 父级对象
    base, // 原始对象(也就是obj)
    copy, // base的浅拷贝(Object.assign)
    draft, // 自身的代理
    drafts, // 属性的代理
    scope, // draft链数组
    }
    const draft = new Proxy(state, {
    // getter
    get(state, prop) {
    if (prop === DRAFT_STATE) {
    return state;
    }
    var drafts = state.drafts;

    if (!state.modified && has(drafts, prop)) {
    return drafts[prop];
    }

    var value = source$1(state)[prop];

    if (state.finalized || !isDraftable(value)) {
    return value;
    }

    if (state.modified) {
    if (value !== peek$1(state.base, prop)) {
    return value;
    }

    drafts = state.copy;
    }

    // 生成当前prop的draft,放到父级的drafts里
    return drafts[prop] = createProxy$1(value, state);
    },

    // setter
    set(state, prop, value) {
    if (!state.modified) {
    var baseValue = peek$1(state.base, prop);

    var isUnchanged = value ? is(baseValue, value) || value === state.drafts[prop] : is(baseValue, value) && prop in state.base;
    if (isUnchanged) {
    return true;
    }
    markChanged$1(state);
    }

    state.assigned[prop] = true;
    state.copy[prop] = value;
    return true;
    }
    })

    这个draft暴露给用户去取值和赋值时,会触发gettersetter函数。

  2. getter

    主要作用:

    • 懒初始化代理对象

      当代理对象子属性被访问时,才会生成子属性的代理对象。

    以开头的例子来说:

    首先,statedraft是在执行reducer之前就生成的;

    当执行到state.products时,即触发getter,此时才会去生成state.productsdraft,并放入statedrafts里。

    • 根据内部状态(modifiedfinalized)决定返回值还是代理对象。

      get接收两个参数,第一个为 state,即创建 Proxy 时传入的第一个参数(目标对象),第二个参数为prop,即想要获取的属性名,具体逻辑如下:

      • propDRAFT_STATE则直接返回 state 对象(会在最后处理结果时用到);
      • statedrafts 属性。drafts 中保存了state.base子对象的 proxy,譬如base = { key1: obj1, key2: obj2 },则drafts = { key1: proxyOfObj1, key2: proxyOfObj2 }
      • state 尚未被修改并且 drafts 中存在 prop 对应的 proxy,则返回该 proxy
      • state.copy存在,则取state.copy[prop],否则取state.base[prop],存于 value
      • state 已经结束计算了或者 value 不能用来生成 proxy,则直接返回 value
      • state 已被标记修改
        • value !== state.base[prop]则直接返回 value
        • 否则把state.copy赋值给 draftscopy 里也包含了子对象的 proxy,具体会在 set 部分细说);
      • 若未提前返回则执行createProxy(value, state)生成以 valuebasestateparent 的子 stateproxy,存到 drafts 里并返回。
  3. setter

    draft修改时,首先对base值进行浅拷贝,保存到copy,将被修改的属性值保存到copy,同时将modified置为true

    同时,为了保证整条链路的对象都是新对象,会根据parent来递归父级,不断浅拷贝(即markChanged方法,此时会往父级的copy里写入子级的draft),以保证从叶子节点到根节点链路上的每个节点都是新对象。

    function markChanged$1(state) {
    if (!state.modified) {
    state.modified = true;
    // 往父级的copy写入子级的draft(这样就可以在第4步的生成环节递归子级)
    state.copy = assign(shallowCopy(state.base), state.drafts);
    state.drafts = null;
    // 递归父级
    if (state.parent) {
    markChanged$1(state.parent);
    }
    }
    }

    以开头的例子来说:

    上一步中state.products被执行时,触发的getter函数生成了state.productsdraft,放入了statedrafts里,同时也返回给了state.products这个表达式,供后续操作使用;

    此时对state.products.count赋值value,就是对state.products返回的draft赋值value,此时会做两件事:

    • count: value键值对放入state.productsdraftcopy
    • 递归父级draft,即statedraft,这时会把state.productsdraft放入statedraftcopy里(具体就是把statedraftdrafts挪到copy),以此来标明state节点的更新。
  4. 生成Immutable对象

    (1) 如果modifiedtrue

    说明对象没有被修改,那么直接返回base属性。

    (2) 如果modifiedfalse

    说明对象被修改了,按理是返回copy属性。但是setter过程是递归的,draft的子对象也是draft,此时copy里还含有很多draft,所以想拿到真正的copy属性,必须对state再进行递归(finalize),拿到真正的copy值。

    // 开始递归,对root执行finalizeTree
    finalizeTree(root, rootPath, scope) {
    const state = root[DRAFT_STATE]
    if (state) {
    if (!this.useProxies) {
    state.finalizing = true
    state.copy = shallowCopy(state.draft, true)
    state.finalizing = false
    }
    root = state.copy
    }

    const needPatches = !!rootPath && !!scope.patches
    const finalizeProperty = (prop, value, parent) => {
    if (value === parent) {
    throw Error("Immer forbids circular references")
    }

    const isDraftProp = !!state && parent === root

    if (isDraft(value)) {
    const path =
    isDraftProp && needPatches && !state.assigned[prop]
    ? rootPath.concat(prop)
    : null

    // 递归finalize(内部会用root的子节点去执行finalizeTree)
    value = this.finalize(value, path, scope)

    if (isDraft(value)) {
    scope.canAutoFreeze = false
    }

    if (Array.isArray(parent) || isEnumerable(parent, prop)) {
    parent[prop] = value
    } else {
    // 赋值
    Object.defineProperty(parent, prop, {value})
    }

    if (isDraftProp && value === state.base[prop]) return
    }
    else if (isDraftProp && is(value, state.base[prop])) {
    return
    }

    // 检查没有冻结的对象,把它们都进行递归,防止再次被写入draft
    else if (isDraftable(value) && !Object.isFrozen(value)) {
    each(value, finalizeProperty)
    }

    if (isDraftProp && this.onAssign) {
    this.onAssign(state, prop, value)
    }
    }

    // 运行
    each(root, finalizeProperty)
    return root
    }

    从对象最外层往里递归,嵌套执行finalizeTreefinalizeProperty,把copy中的draft都替换成真正的值(即子节点)。

  5. revoke

    停止根节点的代理。

参考资料:

  1. 精读《Immer.js》源码
  2. Immer全解析
  3. Github官方仓库


–end–