Immer 全解析

栏目: JavaScript · 发布时间: 5年前

内容简介:第一次听说最近经常听身边的同事提到 Immer,想用到我们的项目里,虽然我觉得它还是不符合我们的场景,并不打算用,但听得多了就觉得还是完整地看一下源码吧,或许能借鉴点什么边边角角的东西呢……produce 是直接暴露给用户使用的函数,它是 Immer 类的一个实例方法(可以先不看代码直接看我下面的解释):
Immer 全解析

Example

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]

// baseState 不变,nextState 是变更后的新对象
const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})
复制代码

初识 Immer

第一次听说 Immer 差不多在几个月前吧,那会儿写了个状态管理库想在公司推广,组内同学发了 Immer 的 GitHub 地址给我,说是有个基于 Proxy 的状态管理库,自称性能很好。我就去看了下,回复说这称不上状态管理库吧,概念上更像 Immutable.js,就是用来方便操作 immutable 数据的。它提供给用户一个 draftState,用户可以随意对它进行修改,最后会返回一个新数据,原数据不变。当时也稍微看了下它的核心原理,draftState 是个 Proxy,对它的读写操作会走到内部定义好的 getter/setter 里,简单来说就是当你获取 draftState 内部的对象时,它都会返回一个 Proxy,而当你进行赋值时,它都会对原对象的 copy 对象进行赋值。最后返回 copy 对象。我的项目里也恰好用到了 Proxy,但 Immer 本身对我好像没什么用,就没怎么放在心上。

源码解析

最近经常听身边的同事提到 Immer,想用到我们的项目里,虽然我觉得它还是不符合我们的场景,并不打算用,但听得多了就觉得还是完整地看一下源码吧,或许能借鉴点什么边边角角的东西呢……

produce

produce 是直接暴露给用户使用的函数,它是 Immer 类的一个实例方法(可以先不看代码直接看我下面的解释):

export class Immer {
    constructor(config) {
        assign(this, configDefaults, config)
        this.setUseProxies(this.useProxies)
        this.produce = this.produce.bind(this)
    }
    produce(base, recipe, patchListener) {
        // curried invocation
        if (typeof base === "function" && typeof recipe !== "function") {
            const defaultBase = recipe
            recipe = base

            // prettier-ignore
            return (base = defaultBase, ...args) =>
                this.produce(base, draft => recipe.call(draft, draft, ...args))
        }

        // prettier-ignore
        {
            if (typeof recipe !== "function") throw new Error("if first argument is not a function, the second argument to produce should be a function")
            if (patchListener !== undefined && typeof patchListener !== "function") throw new Error("the third argument of a producer should not be set or a function")
        }

        let result

        // Only plain objects, arrays, and "immerable classes" are drafted.
        if (isDraftable(base)) {
            const scope = ImmerScope.enter()
            const proxy = this.createProxy(base)
            let hasError = true
            try {
                result = recipe.call(proxy, proxy)
                hasError = false
            } finally {
                // finally instead of catch + rethrow better preserves original stack
                if (hasError) scope.revoke()
                else scope.leave()
            }
            if (result instanceof Promise) {
                return result.then(
                    result => {
                        scope.usePatches(patchListener)
                        return this.processResult(result, scope)
                    },
                    error => {
                        scope.revoke()
                        throw error
                    }
                )
            }
            scope.usePatches(patchListener)
            return this.processResult(result, scope)
        } else {
            result = recipe(base)
            if (result === undefined) return base
            return result !== NOTHING ? result : undefined
        }
    }
复制代码

produce 接收三个参数,正常来说 base 是原数据,recipe 是用户执行修改逻辑的地方,patchListener 是用户接收 patch 数据然后做一些自定义操作的地方。

produce 一开始的逻辑看注释是为了柯里化(其实并不是严格的柯里化,不过和本文内容无关,略过不谈),它判断了下 base 是不是函数,如果是的话把 base 赋值给 recipe,然后再返回一个接收 base 的函数,什么意思呢?就是一般情况你是像 produce(base, (draft) => { ... }) 这样调用 produce,但如果某些情况下你要先接收 recipe 函数再接收 base,那你可以像 produce((draft) => { ... })(base) 这样调用,最常见的场景是配合 React 的 setState:

// state = { user: { age: 18 } }
this.setState(
    produce(draft => {
        draft.user.age += 1
    })
)
复制代码

当然你也可以传入默认 base, const changeFn = produce(recipe, base) ,可以直接 changeFn() 也可以 changeFn(newBase) ,newBase 会覆盖之前的 base。

接下来是 主流程

  • 如果 base 是对象(包括数组),能生成 draft,则:
    • 执行 const scope = ImmerScope.enter() ,生成一个 ImmerScope 的实例 scope,scope 和当前的 produce 调用绑定
    • 执行 this.createProxy(base) 创建 proxy(draft),并执行 scope.drafts.push(proxy) 将 proxy 保存到 scope 里
    • 以 proxy 为参数调用用户传入的 recipe 函数,并把返回值保存为 result
    • 如果执行 recipe 期间没有出错则调用 scope.leave ,把 ImmerScope.current 重置为初始状态(这里是 null),如果出错了则执行 scope.revoke() ,重置所有状态。
    • 判断 result 是否为 promise,是则返回 result.then(result => this.processResult(result, scope)) ,否则直接返回 this.processResult(result, scope) (返回前其实还要执行 scope.usePatches(patchListener) ,patch 相关的东西不算主流程,先不管)
  • 如果 base 不能生成 draft,则:
    result = recipe(base)
    NOTHING
    

整个 produce 主要就做了三个事情:

  • 调用 createProxy 生成 draft 供用户使用
  • 执行用户传入的 recipe,拦截读写操作,走到 proxy 内部的 getter/setter
  • 调用 processResult 解析组装最后的结果返回给用户

接下来我们一步步探究涉及到的部分。

创建 draft

你会发现 Immer 的 class 声明里并没有 createProxy 这个实例方法,但却能在 produce 内执行 this.createProxy(base) 。Is it magic? 实际上 createProxy 是存在于 proxy.js 和 es5.js 文件内的,es5.js 里的内容是个兼容方案,用于不支持 Proxy 的环境,immer.js 的开头会 import 两个文件的内容:

import * as legacyProxy from "./es5"
import * as modernProxy from "./proxy"
复制代码

在 Immer 的 constructor 里会执行 this.setUseProxies(this.useProxies) ,useProxies 用来表示当前环境是否支持 Proxy,setUseProxies 里会判断 useProxies:

  • is true:assign(this, modernProxy)
  • is false: assign(this, legacyProxy)

这样 createProxy 函数就被挂载到 this 上了,这里我们详细看看 proxy.js 里的 createProxy

export function createProxy(base, parent) {
    const scope = parent ? parent.scope : ImmerScope.current
    const state = {
        // Track which produce call this is associated with.
        scope,
        // True for both shallow and deep changes.
        modified: false,
        // Used during finalization.
        finalized: false,
        // Track which properties have been assigned (true) or deleted (false).
        assigned: {},
        // The parent draft state.
        parent,
        // The base state.
        base,
        // The base proxy.
        draft: null,
        // Any property proxies.
        drafts: {},
        // The base copy with any updated values.
        copy: null,
        // Called by the `produce` function.
        revoke: null
    }

    const {revoke, proxy} = Array.isArray(base)
        ? // [state] is used for arrays, to make sure the proxy is array-ish and not violate invariants,
          // although state itself is an object
          Proxy.revocable([state], arrayTraps)
        : Proxy.revocable(state, objectTraps)

    state.draft = proxy
    state.revoke = revoke

    scope.drafts.push(proxy)
    return proxy
}
复制代码
  • 根据 base 构建一个 state 对象,里面的属性我们等用到的时候再细说
  • 判断 base 是否为数组,是则基于 arrayTraps 创建 [state] 的 Proxy,否则基于 objectTraps 创建 state 的 Proxy

arrayTraps 基本就是转发参数到 objectTraps,而 objectTraps 里比较关键的是 get 和 set,对 proxy 的取值和赋值操作都会被这两个函数拦截。

拦截取值操作

function get(state, prop) {
    if (prop === DRAFT_STATE) return state
    let {drafts} = state

    // Check for existing draft in unmodified state.
    if (!state.modified && has(drafts, prop)) {
        return drafts[prop]
    }

    const value = source(state)[prop]
    if (state.finalized || !isDraftable(value)) return value

    // Check for existing draft in modified state.
    if (state.modified) {
        // Assigned values are never drafted. This catches any drafts we created, too.
        if (value !== state.base[prop]) return value
        // Store drafts on the copy (when one exists).
        drafts = state.copy
    }

    return (drafts[prop] = createProxy(value, state))
}
复制代码

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

  • 若 prop 为 DRAFT_STATE 则直接返回 state 对象(会在最后处理结果时用到)
  • 取 state 的 drafts 属性。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]
    state.copy
    
  • 若未提前返回则执行 createProxy(value, state) 生成以 value 为 base、state 为 parent 的子 state 的 proxy,存到 drafts 里并返回

讲完了 get, 我们发现它就是用来生成子对象的 proxy,缓存 proxy,然后返回 proxy,如果不能生成 proxy 则直接返回一个值

拦截赋值操作

function set(state, prop, value) {
    if (!state.modified) {
        // Optimize based on value's truthiness. Truthy values are guaranteed to
        // never be undefined, so we can avoid the `in` operator. Lastly, truthy
        // values may be drafts, but falsy values are never drafts.
        const isUnchanged = value
            ? is(state.base[prop], value) || value === state.drafts[prop]
            : is(state.base[prop], value) && prop in state.base
        if (isUnchanged) return true
        markChanged(state)
    }
    state.assigned[prop] = true
    state.copy[prop] = value
    return true
}
复制代码

set 接受三个参数,前两个和 get 的一样,第三个 value 是将要赋予的新值,具体逻辑如下:

  • 先判断 state 是否被标记更改,若没有,则:

    markChanged(state)
    
  • state.assigned[prop] 置为 true,标记该属性被赋值

  • 将 value 赋值给 state.copy[prop]

整个 set 的核心其实是 标记修改并把新值赋给 copy 对象的对应属性 ,现在我们看下 margeChanged:

function markChanged(state) {
    if (!state.modified) {
        state.modified = true
        state.copy = assign(shallowCopy(state.base), state.drafts)
        state.drafts = null
        if (state.parent) markChanged(state.parent)
    }
}
复制代码

一个 state 只需被标记一次,具体如下:

  • state.modified 置为 true
  • 浅拷贝 state.base ,并把 state.drafts assign 到拷贝对象,赋值给 state.copy 。也就是说 state.copy 中含有子对象的 proxy,会在 get 中用到,之前我们已经说过了
  • state.drafts 置为 null
  • 如果 state 有 parent,递归执行 markChanged(state.parent) 。这很好理解,譬如 draft.person.name = 'Sheepy' 这个操作,我们不止要把 person 标记修改,也要把 draft 标记修改

解析结果返回

processResult(result, scope) {
  const baseDraft = scope.drafts[0]
  const isReplaced = result !== undefined && result !== baseDraft
  this.willFinalize(scope, result, isReplaced)
  if (isReplaced) {
    if (baseDraft[DRAFT_STATE].modified) {
      scope.revoke()
      throw new Error("An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.") // prettier-ignore
    }
    if (isDraftable(result)) {
      // Finalize the result in case it contains (or is) a subset of the draft.
      result = this.finalize(result, null, scope)
    }
    if (scope.patches) {
      scope.patches.push({
        op: "replace",
        path: [],
        value: result
      })
      scope.inversePatches.push({
        op: "replace",
        path: [],
        value: baseDraft[DRAFT_STATE].base
      })
    }
  } else {
    // Finalize the base draft.
    result = this.finalize(baseDraft, [], scope)
  }
  scope.revoke()
  if (scope.patches) {
    scope.patchListener(scope.patches, scope.inversePatches)
  }
  return result !== NOTHING ? result : undefined
}

复制代码

虽然 Immer 的 Example 里都是建议用户在 recipe 里直接修改 draft,但用户也可以选择在 recipe 最后返回一个 result,不过得注意“修改 draft”和“返回新值”这个两个操作只能任选其一,同时做了的话 processResult 函数就会抛出错误。我们重点关注直接操作 draft 的情况,核心逻辑是执行 result = this.finalize(baseDraft, [], scope) ,返回 result 的情况也是相似的,都要调用 finalize ,我们看一下这个函数:

/**
 * @internal
 * Finalize a draft, returning either the unmodified base state or a modified
 * copy of the base state.
 */
finalize(draft, path, scope) {
  const state = draft[DRAFT_STATE]
  if (!state) {
    if (Object.isFrozen(draft)) return draft
    return this.finalizeTree(draft, null, scope)
  }
  // Never finalize drafts owned by another scope.
  if (state.scope !== scope) {
    return draft
  }
  if (!state.modified) {
    return state.base
  }
  if (!state.finalized) {
    state.finalized = true
    this.finalizeTree(state.draft, path, scope)

    if (this.onDelete) {
      // The `assigned` object is unreliable with ES5 drafts.
      if (this.useProxies) {
        const {assigned} = state
        for (const prop in assigned) {
          if (!assigned[prop]) this.onDelete(state, prop)
        }
      } else {
        const {base, copy} = state
        each(base, prop => {
          if (!has(copy, prop)) this.onDelete(state, prop)
        })
      }
    }
    if (this.onCopy) {
      this.onCopy(state)
    }

    // At this point, all descendants of `state.copy` have been finalized,
    // so we can be sure that `scope.canAutoFreeze` is accurate.
    if (this.autoFreeze && scope.canAutoFreeze) {
      Object.freeze(state.copy)
    }

    if (path && scope.patches) {
      generatePatches(
        state,
        path,
        scope.patches,
        scope.inversePatches
      )
    }
  }
  return state.copy
}
复制代码

我们略过类似钩子函数的 onDeleteonCopy ,只看主流程:

  • 通过 draft 拿到 state(在 createProxy 里生成的 state 对象,包含 base、copy、drafts 等属性)
  • 若 state 未被标记修改,直接返回 state.base
  • 若 state 未被标记结束,执行 this.finalizeTree(state.draft, path, scope ,最后返回 state.copy

我们看下 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")
    }

    // In the `finalizeTree` method, only the `root` object may be a draft.
    const isDraftProp = !!state && parent === root

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

      // Drafts owned by `scope` are finalized here.
      value = this.finalize(value, path, scope)

      // Drafts from another scope must prevent auto-freezing.
      if (isDraft(value)) {
        scope.canAutoFreeze = false
      }

      // Preserve non-enumerable properties.
      if (Array.isArray(parent) || isEnumerable(parent, prop)) {
        parent[prop] = value
      } else {
        Object.defineProperty(parent, prop, {value})
      }

      // Unchanged drafts are never passed to the `onAssign` hook.
      if (isDraftProp && value === state.base[prop]) return
    }
    // Unchanged draft properties are ignored.
    else if (isDraftProp && is(value, state.base[prop])) {
      return
    }
    // Search new objects for unfinalized drafts. Frozen objects should never contain drafts.
    else if (isDraftable(value) && !Object.isFrozen(value)) {
      each(value, finalizeProperty)
    }

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

  each(root, finalizeProperty)
  return root
}
复制代码

函数一开始把 state.copy 赋值给 root ,最后执行 each(root, finalizeProperty) ,即以 root 的属性名(prop)和属性值(value)为参数循环调用 finalizePropertyfinalizeProperty 虽然看着代码很多,实际上就是 把 copy 中的 draft(proxy) 属性值替换成 draft[DRAFT_STATE].copy (这些 proxy 是在 markChanged 时 assign 上去的,前面我们说过),这样我们就得到了一个真正的 copy ,最后可以返回给用户。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

CSS权威指南(第三版)

CSS权威指南(第三版)

[美] Eric A.Meyer / 侯妍、尹志忠 / 中国电力出版社 / 2007-10 / 58.00

你是否既想获得丰富复杂的网页样式,同时又想节省时间和精力?本书为你展示了如何遵循CSS最新规范(CSS2和CSS2.1)将层叠样式表的方方面面应用于实践。 通过本书提供的诸多示例,你将了解如何做到仅在一处建立样式表就能创建或修改整个网站的外观,以及如何得到HTML力不能及的更丰富的表现效果。 资深CSS专家Eric A.Meyer。利用他独有的睿智和丰富的经验对属性、标记、标记属性和实......一起来看看 《CSS权威指南(第三版)》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换