写框架的框架—— Cordis 源码解析 (个人向)

#Ubuntu#OrangePiPosted ·12138 Views·4099 Words

前言

  1. 拉取代码

    $ git clone git@github.com:cordiverse/cordis.git
    
  2. 拉取依赖

    # 仓库默认使用 yarn 作为包管理器,不折腾
    $ yarn
    
  3. 预编译工作区子包

    # 对应命令为 yarn yakumo build
    $ yarn build
    

项目结构:

  • cordis:Meta-Framework for Modern JavaScript Applications
  • @cordisjs/core:↑
  • create-cordis:Setup a Cordis application
  • @cordisjs/plugin-hmr:Hot Module Replacement Plugin for Cordis、
  • @cordisjs/loader:Loader for cordis
  • @cordisjs/logger:Logger service for cordis
  • @cordisjs/schema:Schema service for cordis
  • @cordisjs/timer:Timer service for cordis

主要依赖:

  • c8:output coverage reports using Node.js' built in coverage.
  • esbuild:An extremely fast bundler for the web.
  • esbuild-register:Transpile JSX, TypeScript and esnext features on the fly with esbuild.
  • mocha:simple, flexible, fun javascript test framework for node.js & the browser.
  • shx:Portable Shell Commands for Node.
  • tsx:TypeScript Execute | The easiest way to run TypeScript in Node.js.
  • yakumo:Manage complex workspaces with ease.
  • yml-register:Hooks into Node's require function to load .yaml and .yml files.

学习源码时,刚拿到一个全新的框架项目,不像常规的业务代码项目,往往抽象程序时是很高的,我的经验是,首先是略读一遍项目文档:介绍 | Cordis,之后针对代码细节回顾文档内容。

当前 Cordis 文档尚未完善,不过已有的内容已经够我理解一段时间了。

扫一眼项目结构和依赖后,可以聚焦到项目的单元测试代码,能最快的了解到整个项目,各个模块单元的情况或者说作用责任。

注意到,在 package.json 确实存在多条用于测试的命令,其中:yarn yakumo mocha --import tsx,执行后发现,只打印了:

⚡Mahoo12138 ❯❯ yarn test
unknown command: mocha

好像有什么不对劲的地方,保持好奇心,继续试验了其他几条:

  • shx rm -rf coverage && c8 -r text yarn test
  • ...

执行后都打印了 unknown command: mocha,但是也如期输出了测试覆盖率,代码逻辑是正常运行的。

接着我们直接对每个用例进行测试:

$ npx mocha ./**/tests/*.spec.ts --require esbuild-register

也都正常输出,那么项目的基本配置应该没问题,可以针对每个测试项进行分析和学习了。

section: Association

case: service injection

// packages\core\tests\associate.spec.ts

const root = new Context()

class Foo extends Service {
  qux = 1
  constructor(ctx: Context) {
    super(ctx, 'foo', true)
  }
}

class FooBar extends Service {
  constructor(ctx: Context) {
    super(ctx, 'foo.bar', true)
  }
}

root.plugin(Foo)
const fork = root.plugin(FooBar)
expect(root.foo).to.be.instanceof(Foo)
expect(root.foo.bar).to.be.instanceof(FooBar)
expect(root.foo.qux).to.equal(1)
fork.dispose()
expect(root.foo.bar).to.be.undefined

Context

首先创建了一个 Context 对象:

export class Context {
    constructor(config?: any) {
        config = resolveConfig(this.constructor, config)
        this[symbols.store] = Object.create(null)
        this[symbols.isolate] = Object.create(null)
        this[symbols.internal] = Object.create(null)
        this[symbols.intercept] = Object.create(null)
        const self: Context = new Proxy(this, ReflectService.handler)
        self.root = self
        self.reflect = new ReflectService(self)
        self.registry = new Registry(self, config)
        self.lifecycle = new Lifecycle(self)

        const attach = (internal: Context[typeof symbols.internal]) => {...}
        attach(this[symbols.internal])
        return self
    }
}

export function resolveConfig(plugin: any, config: any) {
  const schema = plugin['Config'] || plugin['schema']
  if (schema && plugin['schema'] !== false) config = schema(config)
  return config ?? {}
}

export function defineProperty<T, K extends keyof any>(object: T, key: K, value: any) {
  return Object.defineProperty(object, key, { writable: true, value, enumerable: false })
}
  • 接收一个可选的 config 配置参数,并调用了resolveConfig,且这里是把 Context 当作一个插件传入,解析其Configschema,生成一个 config
  • 然后创建一些内部存储对象,如 storeisolateinternalintercept,使用Object.create(null),确保了对象的纯洁性,是没有原型链的。
  • 接着,它创建一个 Proxy 对象,且传入 this,也就是拦截属性访问,交由 ReflectService.handler处理。
  • 执行它初始化 ReflectServiceRegistryLifecycle,并将它们挂载到 context 实例上,这里其实就已经在执行 ReflectService.handler 中的 set 逻辑了 。
  • 其中 self.root = self 这一行很重要,对于使用 new 创建出来的 Context 会有该属性标记,即 root Context;
  • 最后,递归调用 attach函数,附加内部服务来完成上下文对象的初始化。

可以看看 ReflectService.handler 内的set逻辑:

class ReflectService {
    static handler: ProxyHandler<Context> = {
        set: (target, prop, value, ctx: Context) => {
            if (typeof prop !== 'string') return Reflect.set(target, prop, value, ctx)

            const [name, internal] = ReflectService.resolveInject(target, prop)
            if (!internal) {
                // TODO warning
                return Reflect.set(target, name, value, ctx)
            }
            if (internal.type === 'accessor') {
                if (!internal.set) return false
                return internal.set.call(ctx, value, ctx[symbols.receiver])
            } else {
                ctx.reflect.set(name, value)
                return true
            }
        },
    }
    static resolveInject(ctx: Context, name: string) {
        let internal = ctx[symbols.internal][name]
        while (internal?.type === 'alias') {
            name = internal.name
            internal = ctx[symbols.internal][name]
        }
        return [name, internal] as const
    },
}

ReflectService.handler.set 根据 propsinternal 处理了属性赋值的多种情况,根据已有的代码信息还不好理解其作用。

resolveInject

ReflectService.resolveInject 用于获取 ctx[symbols.internal] 中传入的 name 属性,针对internal?.type === 'alias' 做了递进获取;

其中如果 ReflectService.resolveInject 有值,且 type='accessor' (构造阶段初始化的 internal 都是此种类型)则调用 internal.getreflectService._accessor生成对象内的函数)获取值。

ReflectService

class ReflectService {
  constructor(public ctx: Context) {
    defineProperty(this, symbols.tracker, {
      associate: 'reflect',
      property: 'ctx',
    })

    this._mixin('reflect', ['get', 'set', 'provide', 'accessor', 'mixin', 'alias'])
    this._mixin('scope', ['config', 'runtime', 'effect', 'collect', 'accept', 'decline'])
    this._mixin('registry', ['using', 'inject', 'plugin'])
    this._mixin('lifecycle', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'start', 'stop'])
  }
  _mixin(source: any, mixins: string[] | Dict<string>) {
    const entries = Array.isArray(mixins) ? mixins.map((key) => [key, key]) : Object.entries(mixins)
    const getTarget = typeof source === 'string' ? (ctx: Context) => ctx[source] : () => source
    const disposables = entries.map(([key, value]) => {
      return this._accessor(value, {
        get(receiver) {
          const service = getTarget(this)
          if (isNullable(service)) return service
          const mixin = receiver ? withProps(receiver, service) : service
          const value = Reflect.get(service, key, mixin)
          if (typeof value !== 'function') return value
          return value.bind(mixin ?? service)
        },
        set(value, receiver) {
          const service = getTarget(this)
          const mixin = receiver ? withProps(receiver, service) : service
          return Reflect.set(service, key, value, mixin)
        },
      })
    })
    return () => disposables.forEach((dispose) => dispose())
  }
  _accessor(name: string, options: Omit<Context.Internal.Accessor, 'type'>) {
    const internal = this.ctx.root[symbols.internal]
    if (name in internal) return () => {}
    internal[name] = { type: 'accessor', ...options }
    return () => delete internal[name]
  }
}

export function withProps(target: any, props?: {}) {
  if (!props) return target
  return new Proxy(target, {
    get: (target, prop, receiver) => {
      if (prop in props && prop !== 'constructor') return Reflect.get(props, prop, receiver)
      return Reflect.get(target, prop, receiver)
    },
    set: (target, prop, value, receiver) => {
      if (prop in props && prop !== 'constructor') return Reflect.set(props, prop, value, receiver)
      return Reflect.set(target, prop, value, receiver)
    },
  })
}

构造函数中,通过调用 this._mixin 方法,对 reflect,scope,registry,lifecycle 这几个对象中的多个方法调用了 this._accessor 方法:

  • 获取了this.ctx.root[symbols.internal]
  • internal[name]设置为 type=accessorget/set的对象;

// TODO get/set

其中这里的 get/set 也就是 ReflectService.handlerget/set 进行 internal.set/get.call调用时的函数。

Registry

Context 构造时,也初始化了一个 Registry 对象:

class Registry<C extends Context = Context> {
  private _counter = 0
  private _internal = new Map<Function, MainScope<C>>()
  protected context: Context

  constructor(
    public ctx: C,
    config: any
  ) {
    defineProperty(this, symbols.tracker, {
      associate: 'registry',
      property: 'ctx',
    })

    this.context = ctx
    const runtime = new MainScope(ctx, null!, config)
    ctx.scope = runtime
    runtime.ctx = ctx
    runtime.status = ScopeStatus.ACTIVE
    this.set(null!, runtime)
  }
  set(plugin: Plugin, state: MainScope<C>) {
    const key = this.resolve(plugin)
    this._internal.set(key!, state)
  }
  resolve(plugin: Plugin, assert = false): Function | undefined {
    // Allow `null` as a special case.
    if (plugin === null) return plugin
    // ...
  }
}

创建了一个 MainScope 对象赋值到 ctx.scope,初始化 MainScope 的状态,然后 null 这个特殊键及 MainScope 存入 _internal 中。

MainScope

由于代码阅读周期长,最新 cordis 中,MainScope 已被移除,取而代之的是 EffectScope。

Registry 构造时,plugin 传入的是 null

export class MainScope<C extends Context = Context> extends EffectScope<C> {
  name?: string

  constructor(
    ctx: C,
    public plugin: Plugin,
    config: any,
    error?: any
  ) {
    super(ctx, config)
    if (!plugin) {
      this.name = 'root'
      this.isActive = true
    } else {
      this.setup()
      this.init(error)
    }
  }
}
EffectScope

MainScope 继承自 EffectScope

export abstract class EffectScope<C extends Context = Context> {
  public uid: number | null
  public ctx: C
  public acceptors = new DisposableList<() => boolean>()
  public disposables = new DisposableList<Disposable>()
  public status = ScopeStatus.PENDING
  public dispose: () => Promise<void>

  // Same as `this.ctx`, but with a more specific type.
  protected context: Context

  private _active = false
  private _error: any
  private _pending: Promise<void> | undefined

  constructor(
    public parent: C,
    public config: C['config'],
    private apply: (ctx: C, config: any) => any,
    public runtime?: Plugin.Runtime
  ) {
    if (parent.scope) {
      this.uid = parent.registry.counter
      this.ctx = this.context = parent.extend({ scope: this })
      this.dispose = parent.scope.effect(() => {
        const remove = runtime!.scopes.push(this)
        this.context.emit('internal/plugin', this)
        this.active = true
        return async () => {
          this.uid = null
          this.context.emit('internal/plugin', this)
          if (this.ctx.registry.has(runtime!.plugin)) {
            remove()
            if (!runtime!.scopes.length) {
              this.ctx.registry.delete(runtime!.plugin)
            }
          }
          this.active = false
          await this._pending
        }
      })
    } else {
      this.uid = 0
      this.ctx = this.context = parent
      this._active = true
      this.status = ScopeStatus.ACTIVE
      this.dispose = () => {
        throw new Error('cannot dispose root scope')
      }
    }
  }
}

parent.extend({ scope: this })调用后,执行的代码量非常多,整体看下来,是根据 ctx[symbols.shadow][symbols.tracker]返回 context 或基于其的一个 Proxy 对象,以及还有 { scope: this }MainScope ,需要理解 Traceable 这个概念才能搞懂这里代码的作用。

export class Context {
  extend(meta = {}): this {
    const source = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
    const self = Object.assign(Object.create(getTraceable(this, this)), meta)
    if (!source) return self
    return Object.assign(Object.create(self), { [symbols.shadow]: source })
  }
}

getTraceable

export function getTraceable<T>(ctx: Context, value: T, noTrap?: boolean): T {
  if (!isObject(value)) return value
  if (Object.hasOwn(value, symbols.shadow)) {
    return Object.getPrototypeOf(value)
  }
  const tracker = value[symbols.tracker]
  if (!tracker) return value
  return createTraceable(ctx, value, tracker, noTrap)
}

实际调试中发现,对于 root 级 Context ,getTraceable 倒数第二行返回了, createTraceable 并未调用到,也不考虑其逻辑。

Lifecycle

class Lifecycle {
  constructor(private ctx: Context) {
    defineProperty(this, symbols.tracker, {
      associate: 'lifecycle',
      property: 'ctx',
    })

    ctx.scope.leak(this.on('internal/listener', function () {}))

    for (const level of ['info', 'error', 'warning']) {
      ctx.scope.leak(this.on(`internal/${level}`, (format, ...param) => {}))
    }

    // non-reusable plugin forks are not responsive to isolated service changes
    ctx.scope.leak(
      this.on('internal/before-service', function (this: Context, name) {}, {
        global: true,
      })
    )
    // ...
  }
}

Lifecycle 构造函数中调用了this.on 注册了多个事件监听,且也都将解除绑定的 dispose 函数传入了ctx.scope.leak 方法;

export abstract class EffectScope<C extends Context = Context> {
  leak<T>(disposable: T) {
    return defineProperty(disposable, Context.static, this)
  }
  collect(label: string, callback: () => any) {
    const dispose = defineProperty(
      () => {
        remove(this.disposables, dispose)
        return callback()
      },
      'name',
      label
    )
    this.disposables.push(dispose)
    return dispose
  }
}

leak 方法的逻辑很简单,就是在销毁函数 dispose(able) 上挂载 MainScope

class Lifecycle {
  on(name: string, listener: (...args: any) => any, options?: boolean | EventOptions) {
    if (typeof options !== 'object') {
      options = { prepend: options }
    }

    // handle special events
    this.ctx.scope.assertActive()
    listener = this.ctx.reflect.bind(listener)
    const result = this.bail(this.ctx, 'internal/listener', name, listener, options)
    if (result) return result

    const hooks = (this._hooks[name] ||= [])
    const label = typeof name === 'string' ? `event <${name}>` : 'event (Symbol)'
    return this.register(label, hooks, listener, options)
  }
  bail(...args: any[]) {
    for (const result of this.dispatch('bail', args)) {
      if (isBailed(result)) return result
    }
  }
  *dispatch(type: string, args: any[]) {
    const thisArg =
      typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
    const name = args.shift()
    if (name !== 'internal/event') {
      this.emit('internal/event', type, name, args, thisArg)
    }
    for (const hook of this.filterHooks(this._hooks[name] || [], thisArg)) {
      yield hook.callback.apply(thisArg, args)
    }
  }
  emit(...args: any[]) {
    Array.from(this.dispatch('emit', args))
  }
  filterHooks(hooks: Hook[], thisArg?: object) {
    thisArg = getTraceable(this.ctx, thisArg)
    return hooks.slice().filter((hook) => {
      const filter = thisArg?.[Context.filter]
      return hook.global || !filter || filter.call(thisArg, hook.ctx)
    })
  }
  register(label: string, hooks: Hook[], callback: any, options: EventOptions) {
    const method = options.prepend ? 'unshift' : 'push'
    hooks[method]({ ctx: this.ctx, callback, ...options })
    return this.ctx.scope.collect(label, () => this.unregister(hooks, callback))
  }
}

再来看看 Lifecycle.on 方法:

  • 对传入的 listener 调用 ReflectService.bind ,跟 getTraceable 有关;

  • 接着调用 bail 方法,前两个参数是固定的 this.ctx'internal/listener'

    • 其内部再调用了 dispatch 生成器函数,使用 isBailed 判断真值则返回;
    • 生成器内部,获取首个对象类型的参数作为 thisArg,存在则将第二个参数作为 name
    • 其次,所有 name 不为 internal/event 的调用,都会调用emit方法,且事件名为 'internal/event',递归调用 dispatch
    • 最后,通过filterHooks 方法筛选一遍 this._hooks['internal/listener'],返回 hook.callback 执行结果;
  • bail 结果有值,则返回;否则直接获取 _hooks[name],调用 register 方法;

  • register 方法内,则将 listener 放入对应的 hooks 数组中,并通过 EffectScope 收集了 unregister方法;

    • collect 方法的实现,我觉得很巧妙,又很自然;
    • 定义了一个 dispose 函数,函数体则是将从数组 disposables 中移除,并返回unregister回调执行结果;
    • 然后将 dispose push 到 disposables 数组中,返回 dispose 函数,即为 leak 的参数;

整体上来看,on 方法执行时,会调用 bail 做一个前置操作,包括:

  • 触发 internal/event 事件,以及 internal/listener 事件;
  • 如果有返回值,那就终止 on 后续事件注册的 register 方法执行;

// TODO trace bind

class ReflectService {
  trace<T>(value: T) {
    return getTraceable(this.ctx, value)
  }

  bind<T extends Function>(callback: T) {
    return new Proxy(callback, {
      apply: (target, thisArg, args) => {
        return target.apply(
          this.trace(thisArg),
          args.map((arg) => this.trace(arg))
        )
      },
    })
  }
}

再回到 Context 的构造,最后是调用了 attach 函数:

const attach = (internal: Context[typeof symbols.internal]) => {
  if (!internal) return
  attach(Object.getPrototypeOf(internal))
  for (const key of Object.getOwnPropertyNames(internal)) {
    const constructor = internal[key]['prototype']?.constructor
    if (!constructor) continue
    self[internal[key]['key']] = new constructor(self, config)
    defineProperty(self[internal[key]['key']], 'ctx', self)
  }
}

经过 ReflectServiceRegistryLifecycle 这三个对象的初始化后,internal内部已经包含很多函数了。

首先对 internal 的原型递归调用 attach,但是从上文可知,internal 是使用 Object.create(null) 创建的,并没有原型;之后遍历属性,获取对应值的 prototype.constructor ,算是对象实例的类吧。之后获取了该实例的key作为键,创建一个新实例挂载到 Context 上,并设置了 ctx 属性。

从目前来看,这个功能比较抽象,应为测试代码执行到此,internal 都是 type='accessor'get/set 对象。

Context 就这么水灵灵地初始化完毕了,可不是一般的复杂。

接下来,继续回到测试代码,调用了 context.plugin 方法,深入下ReflectService.handler

// const root = new Context()
// root.plugin(Foo)

  static handler: ProxyHandler<Context> = {
    get: (target, prop, ctx: Context) => {
      if (typeof prop !== 'string') return Reflect.get(target, prop, ctx)

      if (Reflect.has(target, prop)) {
        return getTraceable(ctx, Reflect.get(target, prop, ctx), true)
      }

      const [name, internal] = ReflectService.resolveInject(target, prop)
      // trace caller
      const error = new Error(`property ${name} is not registered, declare it as \`inject\` to suppress this warning`)
      if (!internal) {
        ReflectService.checkInject(ctx, name, error)
        return Reflect.get(target, name, ctx)
      } else if (internal.type === 'accessor') {
        return internal.get.call(ctx, ctx[symbols.receiver])
      } else {
        if (!internal.builtin) ReflectService.checkInject(ctx, name, error)
        return ctx.reflect.get(name)
      }
    }
  }

这里 prop 传入的是 string 类型(其余是参数访问的 Symbol,如 symbols.internal等),然后第二个判断主要针对直接在 Context 构造时挂载的几个属性:

self.root = self
self.scope = new EffectScope(self, {}, () => {})
self.reflect = new ReflectService(self)
self.registry = new Registry(self)
self.events = new EventsService(self)

之后是 resolveInject 会在 ctx[symbols.internal] 读取 prop 对应的属性,例如这里是 plugin,属性值为 ReflectService.accessor ReflectService.mixin创建的对象 { type: 'accessor', ...options }

在之后检查 internal 的值,暂且猜测是兜底操作,先走目标逻辑,判断为 internal.type === 'accessor',则调用 internal.get 方法。

  • 这里有个 ctx[symbols.receiver] 又会进来 ReflectService.handler.get,暂时不清楚 symbols.receiver 的作用;
  • 进入 internal.get 函数体,代码在 ReflectService._mixin,这里有很大的闭包,如:
    • source = 'registry',来源于 this._mixin('registry', ['inject', 'plugin']) 调用;
    • 以及 getTargetkey = 'plugin'
  • getTarget 中又会进入 ReflectService.handler.get,此时则对应上述的第二个判断,调用getTraceable,代码在这 getTraceable
    • 非对象直接返回该对象;
    • symbols.shadow 属性,返回其原型对象;
    • 如果对象及其原型链上不包含属性 symbols.tracker 的值,返回该对象;
    • 最后调用 createTraceable 函数;

对于 Registry 对象,其在初始化时,有如下操作:

class Registry<C extends Context = Context> {
  constructor(public ctx: C) {
    defineProperty(this, symbols.tracker, {
      associate: 'registry',
      property: 'ctx',
      noShadow: true,
    })

    this.context = ctx
  }
}

然后则会调用 createTraceable,主要逻辑就是包了一层 Proxy,但是内部很复杂,等到调用时,才继续深入内部:

function createTraceable(ctx: Context, value: any, tracker: Tracker) {
  if (ctx[symbols.shadow]) {
    ctx = Object.getPrototypeOf(ctx)
  }
  const proxy = new Proxy(value, {
    get: (target, prop, receiver) => {...},
    set: (target, prop, value, receiver) => {...},
    apply: (target, thisArg, args) => {...},
  })
  return proxy
}

重新捋一下思路,现在是 context 去读取 plugin 属性的函数,进行调用,传入一个插件,plugin 属性是 Registry 对象提供的方法,且通过 mixin 混入到 context 中的;

调用栈位于 ReflectService._mixin 方法的 get 函数中,获取到 createTraceable 创建的 service 后:

  • 根据 receiver 判断是否需要调用 withPropsctx[symbols.receiver] 值 undefined,跳过;
  • 拿到 Registry 对象实例中 key = 'plugin' 的方法函数,bind 调用;

最终,进入到了 Registry.plugin 函数体内:

plugin(plugin: Plugin<C>, config?: any) {
    // check if it's a valid plugin
    const key = this.resolve(plugin, true)
    this.ctx.scope.assertActive()

    let runtime = this._internal.get(key)
    if (!runtime) {
        runtime = Plugin.resolve<C>(plugin)
        this._internal.set(key!, runtime)
    }

    const outerError = new Error()
    return new EffectScope(this.ctx, config, async (ctx, config) => {
        const innerError = new Error()
        try {
            config = resolveConfig(plugin, config)
            if (typeof plugin !== 'function') {
                await plugin.apply(ctx, config)
            } else if (isConstructor(plugin)) {
                // eslint-disable-next-line new-cap
                const instance = new plugin(ctx, config)
                for (const hook of instance?.[symbols.initHooks] ?? []) {
                    hook()
                }
                await instance?.[symbols.setup]?.()
            } else {
                await plugin(ctx, config)
            }
        } catch (error: any) {
            const outerLines = outerError.stack!.split('\n')
            const innerLines = innerError.stack!.split('\n')

            // malformed error
            if (typeof error?.stack !== 'string') {
                outerLines[0] = `Error: ${error}`
                outerError.stack = outerLines.join('\n')
                throw outerError
            }

            // long stack trace
            const lines: string[] = error.stack.split('\n')
            const index = lines.indexOf(innerLines[2])
            if (index === -1) throw error

            lines.splice(index - 1, Infinity)
            // lines.push('    at Registry.plugin (<anonymous>)')
            lines.push(...outerLines.slice(2))
            error.stack = lines.join('\n')
            throw error
        }
    }, runtime)
}
  • 调用 resolve 判断是否合法插件形式;

  • 调用ctx.scope.assertActive() ,做一个检测;

  • _internal 中根据 plugin 获取对应的 scope,无则调用 Plugin.resolve 创建:

    export function resolve<C extends Context = Context>(plugin: Plugin<C>): Runtime<C> {
      let name = plugin.name
      if (name === 'apply') name = undefined
      const schema = plugin['Config'] || plugin['schema']
      const inject = Inject.resolve(plugin['using'] || plugin['inject'])
      return { name, schema, inject, plugin, scopes: new DisposableList() }
    }
    

    该对象中会除了 plugin 的基本属性,还有一个 scopes,实例化一个 DisposableList 对象。

  • 最后,实例化 EffectScope 对象,并返回;

createTraceable

实例化 EffectScope 时,会在 Registry 中调用 this.ctx,而 RegistrycreateTraceable 处理过,包上了一层 Proxy,现在来看看其机制的作用:

function createTraceable(ctx: Context, value: any, tracker: Tracker) {
  if (ctx[symbols.shadow]) {
    ctx = Object.getPrototypeOf(ctx)
  }
  const proxy = new Proxy(value, {
    get: (target, prop, receiver) => {
      if (prop === symbols.original) return target
      if (prop === tracker.property) return ctx
      if (typeof prop === 'symbol') {
        return Reflect.get(target, prop, receiver)
      }
      if (tracker.associate && ctx[symbols.internal][`${tracker.associate}.${prop}`]) {
        return Reflect.get(
          ctx,
          `${tracker.associate}.${prop}`,
          withProp(ctx, symbols.receiver, receiver)
        )
      }
      let shadow: any, innerValue: any
      const desc = getPropertyDescriptor(target, prop)
      if (desc && 'value' in desc) {
        innerValue = desc.value
      } else {
        shadow = createShadow(ctx, target, tracker.property, receiver)
        innerValue = Reflect.get(target, prop, shadow)
      }
      const innerTracker = innerValue?.[symbols.tracker]
      if (innerTracker) {
        return createTraceable(ctx, innerValue, innerTracker)
      } else if (!tracker.noShadow && typeof innerValue === 'function') {
        shadow ??= createShadow(ctx, target, tracker.property, receiver)
        return createShadowMethod(ctx, innerValue, receiver, shadow)
      } else {
        return innerValue
      }
    },
  })
  return proxy
}

对于 this.ctx,在 get 函数的第二行即返回了 ctx;

Comments

Leave a comment to join the discussion