写框架的框架—— Cordis 源码解析 (个人向)
前言
拉取代码
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.yaml
and.yml
files.
学习源码时,刚拿到一个全新的框架项目,不像常规的业务代码项目,往往抽象程序时是很高的,我的经验是,首先是略读一遍项目文档:介绍 | 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
回调执行结果; - 然后将
dispose
push 到disposables
数组中,返回dispose
函数,即为leak
的参数;
整体上来看,on
方法执行时,会调用 bail
做一个前置操作,包括:
- 触发
internal/event
事件,以及internal/listener
事件; - 如果有返回值,那就终止
on
后续事件注册的register
方法执行;
// TODO trace bind
1 | class ReflectService { |