前言
拉取代码
1
$ git clone git@github.com:cordiverse/cordis.git
拉取依赖
1
2# 仓库默认使用 yarn 作为包管理器,不折腾
$ yarn预编译工作区子包
1
2# 对应命令为 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.yamland.ymlfiles.
学习源码时,刚拿到一个全新的框架项目,不像常规的业务代码项目,往往抽象程序时是很高的,我的经验是,首先是略读一遍项目文档:介绍 | Cordis,之后针对代码细节回顾文档内容。
当前 Cordis 文档尚未完善,不过已有的内容已经够我理解一段时间了。
扫一眼项目结构和依赖后,可以聚焦到项目的单元测试代码,能最快的了解到整个项目,各个模块单元的情况或者说作用责任。
注意到,在 package.json 确实存在多条用于测试的命令,其中:yarn yakumo mocha --import tsx,执行后发现,只打印了:
1 | ⚡Mahoo12138 ❯❯ yarn test |
好像有什么不对劲的地方,保持好奇心,继续试验了其他几条:
shx rm -rf coverage && c8 -r text yarn test...
执行后都打印了 unknown command: mocha,但是也如期输出了测试覆盖率,代码逻辑是正常运行的。
接着我们直接对每个用例进行测试:
1 | $ npx mocha ./**/tests/*.spec.ts --require esbuild-register |
也都正常输出,那么项目的基本配置应该没问题,可以针对每个测试项进行分析和学习了。
section: Association
case: service injection
1 | // packages\core\tests\associate.spec.ts |
Context
首先创建了一个 Context 对象:
1 | export class Context { |
- 接收一个可选的
config配置参数,并调用了resolveConfig,且这里是把 Context 当作一个插件传入,解析其Config和schema,生成一个config; - 然后创建一些内部存储对象,如
store、isolate、internal和intercept,使用Object.create(null),确保了对象的纯洁性,是没有原型链的。 - 接着,它创建一个
Proxy对象,且传入this,也就是拦截属性访问,交由ReflectService.handler处理。 - 执行它初始化
ReflectService、Registry和Lifecycle,并将它们挂载到context实例上,这里其实就已经在执行ReflectService.handler中的set逻辑了 。 - 其中
self.root = self这一行很重要,对于使用new创建出来的Context会有该属性标记,即 root Context; - 最后,递归调用
attach函数,附加内部服务来完成上下文对象的初始化。
可以看看 ReflectService.handler 内的逻辑:
1 | class ReflectService { |
ReflectService.resolveInject 用于获取ctx[symbols.internal]中传入的name 属性,针对internal?.type === 'alias' 做了递进获取;
ReflectService.handler.set 根据props和internal处理了属性赋值的多种情况,根据已有的代码信息还不好理解其作用。
ReflectService
1 | class ReflectService { |
构造函数中,通过调用 this._mixin 方法,对 reflect,scope,registry,lifecycle 这几个对象中的多个方法调用了 this._accessor 方法:
- 获取了
this.ctx.root[symbols.internal]; - 将
internal[name]设置为type=accessor的get/set的对象;
// TODO get/set
其中这里的 get/set 也就是 ReflectService.handler的 get/set 进行 internal.set/get.call调用时的函数。
Registry
Context 构造时,也初始化了一个 Registry 对象:
1 | class Registry<C extends Context = Context> { |
创建了一个 MainScope 对象赋值到 ctx.scope,初始化 MainScope 的状态,然后 null 这个特殊键及 MainScope 存入 _internal 中。
MainScope
Registry 构造时,plugin 传入的是 null,
1 | export class MainScope<C extends Context = Context> extends EffectScope<C> { |
MainScope 继承自 EffectScope:
1 | export abstract class EffectScope<C extends Context = Context> { |
parent.extend({ scope: this })调用后,执行的代码量非常多,整体看下来,是根据 ctx[symbols.shadow]和[symbols.tracker]返回 context 或基于其的一个 Proxy 对象,以及还有 { scope: this } 的 MainScope ,需要理解 Traceable 这个概念才能搞懂这里代码的作用。
1 | export class Context { |
实际调试中发现,对于 root 级 Context ,getTraceable 倒数第二行返回了, createTraceable 并未调用到,也不考虑其逻辑。
Lifecycle
1 | class Lifecycle { |
Lifecycle 构造函数中调用了this.on 注册了多个事件监听,且也都将解除绑定的 dispose 函数传入了ctx.scope.leak 方法;
1 | export abstract class EffectScope<C extends Context = Context> { |
leak 方法的逻辑很简单,就是在销毁函数 dispose(able) 上挂载 MainScope 。
1 | class Lifecycle { |
再来看看 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回调执行结果; - 然后将
disposepush 到disposables数组中,返回dispose函数,即为leak的参数;
整体上来看,on 方法执行时,会调用 bail 做一个前置操作,包括:
- 触发
internal/event事件,以及internal/listener事件; - 如果有返回值,那就终止
on后续事件注册的register方法执行;
// TODO trace bind
1 | class ReflectService { |