フル解像度

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アドレスが送信されてきました。少し見て、これは状態管理ライブラリではないと回答しましたが、概念的には不変データの操作を容易にするために使用されるImmutable.jsに近いものです。ユーザーがdraftStateを自由に変更できるようにし、最後に元のデータを変更せずに新しいデータを返します。その当時、私はまたその基本原則を少し見ました。draftStateはProxyであり、それに対する読み書き操作は内部的に定義されたgetter / setterに行きます。代行して割り当てると、元のオブジェクトのコピーオブジェクトが割り当てられます。最後に、コピーオブジェクトが返されます。

私のプロジェクトではProxyも使用しています。これはメッセージングの操作を単純化するために使用されます。しかし、私のプロジェクトの状態は変更可能です(最初は不変でしたが、その後いくつかの機能を達成するために状況が変わりました…)ので、Immerは私には少しずさんなので気にしません。

源码解析

しかし、会社を変更した後、私は新しい同僚からImmerをよく耳にし、それを私たちのプロジェクトで使用したいと思いました。私たちのシーンに沿ったものではないと思いますが、使用するつもりはありませんが、多くのことを聞いているのであれば、まだソースコードを完全に見ているように思います。

作り出す

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
        }
    }
复制代码

Producは3つのパラメータを受け取り、通常baseは元のデータ、recipeはユーザが修正ロジックを実行する場所、patchListenerはユーザがパッチデータを受信してカスタム操作を行う場所です。

プロデュース解説の冒頭の論理はカリー化のためのものであり(実際、厳密にはカリー化されていませんが、この記事の内容とは無関係です。スキップしてください)、baseが関数かどうかを判断します。レシピ、ベースを受け取る関数を返す、どういう意味ですか?通常、 produce(base、(draft)=> {...})のようにproduceを呼び出しますが、レシピ関数を受け取り、その後baseを受け取る場合は、 produce((draft)=> {...})(base)のように呼び出すことができます最も一般的なシナリオは、ReactのsetStateと一致することです。

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

もちろん、デフォルトのベースを渡すこともできます。 const changeFn = produce(recipe、base)、直接 changeFn()または changeFn(newBase) >、newBaseは前のベースを上書きします。

次はメインプロセスです。

  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…

全作物は主に3つのことをしました:

  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…

次に、関係する部分を探ります。

下書きを作成

Immerのクラス宣言にはcreateProxyインスタンスメソッドがないことがわかりますが、 this.createProxy(base)をproduce内で実行できます。魔法ですか?実際、createProxyはproxy.jsファイルとes5.jsファイルに存在しますes5.jsのコンテンツは互換性のあるソリューションですプロキシをサポートしない環境では、immer.jsは最初に2つのファイルのコンテンツをインポートします。

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

this.setUseProxies(this.useProxies)はImmerコンストラクタで実行され、useProxiesは現在の環境がProxyをサポートしているかどうかを示すために使用され、setUseProxiesはuseProxiesを決定します。

  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…

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
}
复制代码
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…

arrayTrapsは基本的にobjectTrapsにパラメータを転送しますが、objectTrapsの重要な要素はgetおよびsetで、プロキシの値と割り当てはこれら2つの関数によってインターセプトされます。

インターセプト値演算

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は2つのパラメータを受け取ります。1つはstate、つまりProxyが作成されたときに渡される1つ目のパラメータ(ターゲットオブジェクト)、2つ目のパラメータはprop、つまり取得されるプロパティの名前です。

  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…

取得が完了したら、子オブジェクトの生成に使用されたプロキシであることを確認し、プロキシをキャッシュしてからプロキシを返します。プロキシを生成できない場合は、直接値を返します

代入代入操作

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は3つの引数を受け取り、最初の2つはgetと同じで、3番目の値は新しい値です。具体的なロジックは次のとおりです。

  • まず状態が変化でマークされているかどうかを判断し、そうでなければ、それから:

    • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
    • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • state.assigned [prop] をtrueに設定し、割り当てられる属性をマークします

  • state.copy [prop] に値を割り当てます

セット全体の中核は、実際にはマーカーの変更とコピーオブジェクトの対応するプロパティへの新しい値の割り当てです。それでは、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)
    }
}
复制代码

次のように、状態は1回しかマークできません。

  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです…

解析結果の戻り

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の例では、ユーザーがレシピ内のドラフトを直接変更することを推奨していますが、ユーザーはレシピ内の結果を返すように選択することもできますが、「ドラフトの変更」および「新しい値を返す」操作のみ選択できます。まず、 processResult 関数が同時にエラーをスローします。ドラフトの直接操作に焦点を当てます。コアロジックは result = this.finalize(baseDraft、[]、scope)を実行することです。結果の戻りも同様です。 finalize <を呼び出す必要があります。 / code>では、この関数を見てみましょう。

/**
 * @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
}
复制代码

フック関数のように onDelete onCopy をスキップします。メインフローを見てください。

  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです...
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです...
  • 長い間それを読んだ後、それから学ぶべき特別な何もないようです...

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)を実行します。これはルート属性名です(propそして、パラメータループの属性値(value)は finalizeProperty finalizeProperty を呼び出します。たくさんのコードを見ていますが、実際にはドラフト(プロキシ)属性値をコピーしています。 ドラフト[DRAFT_STATE] .copy (これらのエージェントは前述のようにmarkChangedに割り当てられています)に置き換えられたので、実際のコピーを入手できます。そして最後に戻ることができます。ユーザー

总结

プロジェクト全体は予想以上に複雑ですが、コアロジックは主に上記の太字部分です。

長い間それを読んだ後、それから学ぶべき特別な何もないようです...

元のリンク