Delegate是个好货色,看看孙啸达 同学对ZoneDelegate的介绍吧,这是他对于zone.js系列文章的第三篇~
zone.js系列往期文章
- zone.js由入门到放弃之一——通过一场游戏意识zone.js
- zone.js由入门到放弃之二——zone.js API大练兵
zone.js源码剖析
接下来的全是干货,从头到尾,一干到底
一点前置:Zone 和 ZoneDelegate
在前文中,咱们始终在回避解说Zone和ZoneDelegate之间的区别。尤其在上篇文章讲API的时候,我甚至让大家把这两者当成一回事。其实这两者并不是齐全相等的。单从Delegate这个单词你也能看出,尽管Zone和ZoneDelegate的API很像,然而真正干活的是ZoneDelegate。我简略节选几段Zone的源码,大家不难发现,大多数Zone的API都间接或间接通过代理中绝对应的API实现的。
public fork(zoneSpec: ZoneSpec): AmbientZone { // 此处省略成吨源码 return this._zoneDelegate.fork(this, zoneSpec);}public run<T>(callback: (...args: any[]) => T, applyThis?: any, applyArgs?: any[], source?: string): T { // 此处省略成吨源码 return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);}runTask(task: Task, applyThis?: any, applyArgs?: any): any { // 此处省略成吨源码 return this._zoneDelegate.invokeTask(this, task, applyThis, applyArgs);}
我把上篇文章讲到的API和ZoneDelegate之间的调用关系简略梳理了一下。下文在剖析源码的时候,会有大量Zone、ZomeDelegate、ZomeTask三者之间互相调用的场景,切实理不清的中央能够返回这里看下。
尽管ZoneDelegate理论承当了大量的工作,然而Zone也不是甩手掌柜,啥活儿也不干。在我集体看来,Zone其实次要只负责两件事:
- 保护Zone的上下文栈:咱们晓得Zone是个具备继承关系的链式构造。zone.js在全局会保护一个Zone栈帧,每当咱们在某个Zone中执行代码时,Zone要负责将以后的Zone上下文置于栈帧中;当代码执行结束,又要负责将Zone栈帧复原回去。
public run<T>(callback: (...args: any[]) => T, applyThis?: any, applyArgs?: any[], source?: string): T { // 将以后的Zone上下文置于栈帧中 _currentZoneFrame = {parent: _currentZoneFrame, zone: this}; try { ... } finally { // 复原Zone栈帧 _currentZoneFrame = _currentZoneFrame.parent!; }}
- Zone还负责ZoneTask的状态切换。上文说过,Zone能够对宏工作、微工作、事件进行治理。那么每个工作在Zone中处于何种阶段、何种状态也是由Zone负责的。Zone会在适当时候调用ZoneTask的_transitionTo办法切换ZoneTask的状态。
- *
接下来会把zone.js对setTimeout的Patch过程进行具体的阐明,为了不便了解,其中波及的大量源码都是我简化之后。
第一阶段:zone.js打包setTimeout
Patch第一站
zone.js提供一个静态方法用于Patch咱们常见的API,对setTimeout的Patch位于zone.js/lib/browser/browser.ts
下:其中这个patchTimer(global, set, clear, 'Timeout');
就是本次源码剖析的终点。
代码传送门
Zone.__load_patch('timers', (global: any) => { const set = 'set'; const clear = 'clear'; patchTimer(global, set, clear, 'Timeout'); patchTimer(global, set, clear, 'Interval'); patchTimer(global, set, clear, 'Immediate');});
战术式阉割patchTimer
尽管patchTimer是打包setTimeout的要害代码,然而为了先理清框架,我先把一些当下没那么重要的代码都省略掉。通过上面的代码咱们发现,patchTimer中最外围的一句就是:
setNative = patchMethod(...)
setNative从命名上不难理解,其实就是用来保留原生的setTimeout。除了保留原生setTimeout之外,咱们在下一节中一起看下patchMethod对setTimeout还做了什么。
代码传送门
export function patchTimer(window: any, setName: string, cancelName: string, nameSuffix: string) { let setNative: Function|null = null; function scheduleTask(task: Task) { // 战术式疏忽 } setNative = patchMethod(window, setName, (delegate: Function) => function(self: any, args: any[]) { // 战术式疏忽 });}
只会甩锅的patchMethod
上面是简化后的代码,不难发现patchMethod就做了两件事:
- 将原生setTimeout办法保存起来,保留在windiw.__zone_symbol__setTimeout中
- 通过patchFn办法打包setTimeout,并替换原windiw.setTimeout
patchFn
这个函数看起来有点繁琐,其实这是对函数柯里化的利用,是一种高阶函数。如果对这块常识不理解的能够简略了解为它就是一个返回函数的函数。patchFn
的执行会返回一个打包后的setTimeout,而对patchFn
的定义来自于上一节的patchTimer
办法中。所以我说patchMethod
甩锅,说好的要打包setTimeout办法,后果打包工具还得patchTimer
函数提供。
代码传送门
export function patchMethod( ..., patchFn: (delegate: Function, delegateName: string, name: string) => (self: any, args: any[]) => any): Function|null { let delegate: Function|null = null; // __zone_symbol__xxx 是 zone.js 的特色产物,专门用来保留原生API的 delegate = windiw.__zone_symbol__setTimeout = windiw.setTimeout; const patchDelegate = patchFn(delegate!, delegateName, name); windiw.setTimeout = function() { return patchDelegate(this, arguments as any); }; return delegate;}
看看zone.js对setTimeout到底干了什么:
再回到patchTimer
办法中,patchTime在调用patchMethod的时候传入了一个patchFn
办法。这个办法对setTimeout干了两件事:
- 通过timer办法把实在回调包装了一下,实际上就是想保留this指针
- 调用scheduleMacroTaskWithCurrentZone办法封装出一个task 【重点】
看到这里是不是有点似曾相识的感觉,这个task会不会是ZoneTask?scheduleMacroTaskWithCurrentZone会不会和scheduleMacroTask有关系?
这里能够很负责的通知你,两个的问题的答案都是必定的哈!至于scheduleMacroTaskWithCurrentZone的源码剖析,咱们稍作调整再持续剖析。
代码传送门
const patchFn = (delegate: Function) => function(self: any, args: any[]) { const options = { delay: args[1] || 0, args: args }; const callback = args[0]; // 封装timer办法,保留this指针 args[0] = function timer(this: unknown) { return callback.apply(this, arguments); }; // 通过调用scheduleMacroTask封装异步Task const task = scheduleMacroTaskWithCurrentZone(setName, args[0], options, scheduleTask, clearTask); return task;};
第一阶段小结:
我把第一阶段称作为打包阶段,此处个别都在利用初始化的时候执行的,zone.js正是利用这段时间对各式各样的API进行了Monkey Patch操作。截止目前为止,zone.js对setTimeout的Patch操作其实并没有什么特地。最外围的函数是patchTimer,尽管在这个阶段中,该函数大部分性能都被战术性阉割了,然而它将setTimeout的原生实现替换成了patchFn
。从patchFn
的实现咱们能够看出,每当咱们触发window.setTimeout时,就会有一个名为task的工作被创立进去。上一遍文章说过,zone.js能够把诸多异步操作封装成ZoneTask,而后就能够对每个异步工作的生命周期进行监控、跟踪。看到这里,是不是大抵有点轮廓了。
上面这个图,是我依据zone.js第一阶段的动作形容的,不便大家配合源码进行了解。
我看很多文章都说过zone.js的Patch过程如何残忍,光听他人说有什么意思,不如本人来看看
第二阶段:触发setTimeout
上一阶段中,zone.js强势hack了setTimeout,让setTimeout被调用时创立一个task。接下来,咱们看下,当一个打包的setTimeout被调用后的流程。
创立Task
先填个坑,上一节我说scheduleMacroTaskWithCurrentZone和scheduleMacroTask有关系,此处以源码为证哈:代码传送门
export function scheduleMacroTaskWithCurrentZone(source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,customCancel?: (task: Task) => void): MacroTask { return Zone.current.scheduleMacroTask(source, callback, data, customSchedule, customCancel); }
scheduleMacroTask
非常简单,创立一个ZoneTask后帅锅给scheduleTask
函数。
代码传送门
scheduleMacroTask(source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,customCancel?: (task: Task) => void): MacroTask { return this.scheduleTask( new ZoneTask(macroTask, source, callback, data, customSchedule, customCancel));}
在这里,这个新建的ZoneTask十分重要,它除了一些初始化操作以外,有3个值得大家留神的中央(其它作用临时不大的代码曾经被省略):
- scheduleFn是zoneTask调度的要害代码,这里具体的代码在
patchTimer
中。但在之前被我战术性阉割了,后续用到的时候我再开展解释。这里先记住,task有个scheduleFn办法,办法来自patchTimer
,请死记! - ZoneTask有个invoke办法,该办法理论是对zone.runTask的调用。zone.runTask前面会介绍,然而这里是ZoneTask和Zone之间分割的一个桥梁,请死记!
_transitionTo
是ZoneTask状态切换函数,Zone就是通过这个函数来扭转Task的状态,并对Task施行跟踪监控的,还是 bi~~~ 请死记!
代码传送门
class ZoneTask<T extends TaskType> implements Task { // 战术性省略 constructor( type: T, source: string, callback: Function, options: TaskData|undefined, scheduleFn: ((task: Task) => void)|undefined, cancelFn: ((task: Task) => void)|undefined) { this.type = type; this.source = source; this.data = options; this.scheduleFn = scheduleFn; this.cancelFn = cancelFn; this.callback = callback; const self = this; // invoke最总会被封装成setTimeout的回调函数 this.invoke = function() { return this.zone.runTask.call(global, self, this, <any>arguments); }; } // Task的状态切换函数 _transitionTo(toState: TaskState, fromState1: TaskState, fromState2?: TaskState) { // 战术性省略 }}
调度Task
Task创立后,Zone会通过代理执行scheduleTask实现对Task的调度。Zone只在ZoneDelegate调度前后别离去批改一下Task的状态而已,真的是干啥全凭一张嘴。
代码传送门
scheduleTask<T extends Task>(task: T): T { // 赵立冬:情侣大巷这个我的项目给你了 (task as any as ZoneTask<any>)._transitionTo(scheduling, notScheduled); // 高启强:撸起袖子干 task = this._zoneDelegate.scheduleTask(this, task) as T; // 赵立冬:这个我的项目做得不错 if ((task as any as ZoneTask<any>).state == scheduling) { (task as any as ZoneTask<any>)._transitionTo(scheduled, scheduling); } return task;}
ZoneDelegate.scheduleTask次要工作:
- 上篇文章中咱们讲到的onScheduleTask这个勾子会在此时被调用,这是zone.js跟踪异步工作时触发的第一个勾子。代码中
this._scheduleTaskZS.onScheduleTask
的执行就是这块的体现。因为Zone有着一层层的继承关系,所以源码中其实还有很多父级代理中onScheduleTask勾子的调用逻辑。我为了不便了解,在上面代码中把这部分代码省略了,实际上scheduleTask这个办法会在这个过程中被递归调用屡次。 - 调度的外围是调用了task.scheduleFn办法,在上文中,我说这里是重点,要死记的。
代码传送门
scheduleTask(targetZone: Zone, task: Task): Task { // 战术性省略:此处代码跟源码略有出入,这么做只是为了不便了解 this._scheduleTaskZS.onScheduleTask !( this._scheduleTaskDlgt !, this._scheduleTaskCurrZone !, targetZone, task) as ZoneTask<any>; task.scheduleFn(task); return returnTask;}
scheduleTask
函数的代码不多,然而要理解它须要后面很多的铺垫:
- setNative办法被调用,后面讲了这个办法是原生的setTimeout,也就是说执行到这里,真正的setTimeout办法才刚被调用。
- setTimeout的回调被从新封装,封装当前变成了task.invoke。从这一刻,zone.js正式改写了setTimeout的回调,并开始正式接管setTimeout。
- task.invoke这个办法之前强调了要死记的,因为它会间接调用zone.runTask办法。通过这样的方法,zone.js能够将setTimeout的回调办法限定在Zone的上下文中执行。别看这里只有几行,这是zone跨调用栈维持上下文对立的外围所在!
代码传送门
function scheduleTask(task: Task) { const data = <TimerOptions>task.data; data.args[0] = function() { // 将setTimeout回调替换成task.invoke return task.invoke.apply(this, arguments); }; // 执行原生setTimeout data.handleId = setNative!.apply(window, data.args); return task;}
第二阶段小结:
对接上一阶段,当setTimeout被触发后,zone会依据patch后的setTimeout新建一个Task(MacroTask)。这个task有个三个重要知识点:
- 保留了该task的调度办法
scheduleFn
- 定义task的invoke办法
- 存在一个切换task状态的办法
_transitionTo
。
接下来Zone把调度Task的工作承包给高启强,啊不对不对,是承包给ZoneDelegate,而后ZoneDelegate通过调用ZoneTask中scheduleFn实现任务调度。
scheduleFn
这个函数实际上hack掉了原生setTimeout办法上的回调函数,将回调函数改写成task的invoke办法。到此造成一个逻辑上的闭环,一句话总结:setTimeout的回调理论调用的是task.invoke函数。
下图是到目前为止的调用关系图:
第三阶段:回调执行
因为原生的setTimeout被触发,所以改写后的回调被送进循环队列的Timer队列中,待计时器计算延时达到后,将改写后的回调放入执行队列期待执行。这部分内容是V8引擎的循环队列的常识,这里就不开展讲了。咱们最关怀的是,当执行栈开始执行这个回调的时候又会产生什么?
Task运行
回调函数执行的时候,理论执行的是task.invkoe办法;又因为task.invkoe绑定Zone.runTask。当然,一看到Zone上办法,那咱们能够毫无波澜地判断,此时Zone除了改一改Task状态之外又又又把活承包给ZoneDelegate,而这次的承办单位是ZoneDelegate.invokeTask
。
ZoneDelegate.invokeTask
绝对比较简单,我就不阉割它了。别看后面一堆判断逻辑,都是虚张声势(也不全是,至多onInvokeTask这个勾子是此时被调用的)。ZoneDelegate.invokeTask
最重要的理论就是最初这句task.callback.apply(applyThis, applyArgs)
。这里的callback是setTimeout实在的回调函数,从此出咱们能够看出,这个回调函数的确是执行在Zone的上下文中的。
代码传送门
invokeTask(targetZone: Zone, task: Task, applyThis: any, applyArgs?: any[]): any { return this._invokeTaskZS ? this._invokeTaskZS.onInvokeTask! (this._invokeTaskDlgt!, this._invokeTaskCurrZone!, targetZone, task, applyThis, applyArgs) : task.callback.apply(applyThis, applyArgs);}
你认为这就完了?
最初,这篇文章还差一个坑没有填,那就是第三个要死记的_transitionTo
办法。之前只是说_transitionTo
能够扭转Task的状态,那么一个Task到底有些状态呢?都是什么时候扭转的?上面这些是Task所有可能的状态,那咱们对下面讲的封装逻辑只波及到其中的几个。
const notScheduled: 'notScheduled' = 'notScheduled', scheduling: 'scheduling' = 'scheduling', scheduled: 'scheduled' = 'scheduled', running: 'running' = 'running', canceling: 'canceling' = 'canceling', unknown: 'unknown' = 'unknown';
- Task在刚刚初始化的时候是
notScheduled
的 - scheduleFn调度函数执行之前,Task状态会被改为
scheduling
- scheduleFn调度函数执行之后,Task状态会被改为
scheduled
- 当回调函数被置于调用栈中筹备执行时,Task状态会被改为
running
- 回调函数执行结束后,Task状态会被改为
notScheduled
调用关系图
最初,奉上我对源码剖析的调用关系图
总结
剖析zone.js源码的过程是苦楚的,光从思维图上就能够看出,zone.js的绝大多数逻辑都是围绕Zone、ZoneDelegate、ZoneTask开展的。这兄弟三个之间互相援用、相互依赖,即便在我省略掉很多代码之后还是存在很多盘根错节的调用关系。如果你是一个颈椎病患者,那么倡议你能够深度体验一下,你的脖子大概率会问候一下zone.js的整体研发团队。
明天这篇文章其实只剖析setTimeout的Patch逻辑,zone.js其实对很多其它API也都下手了。setTimeout只是一个宏工作的代表,后续心愿能够再选一个微工作和事件持续剖析一下zone.js的打包流程。以后,前提是if necessary
最近曾经梳理完NgZone的源码逻辑,集体感觉可能会更贴近大家的理论开发,剖析过程也更乏味。喜爱的能够持续蹲个后续~~~
OpenTiny 社区招募贡献者啦
OpenTiny Vue 正在招募社区贡献者,欢送退出咱们
你能够通过以下形式参加奉献:
- 在 issue 列表中抉择本人喜爱的工作
- 浏览贡献者指南,开始参加奉献
你能够依据本人的爱好认领以下类型的工作:
- 编写单元测试
- 修复组件缺点
- 为组件增加新个性
- 欠缺组件的文档
如何奉献单元测试:
- 在
packages/vue
目录下搜寻it.todo
关键字,找到待补充的单元测试 - 依照以上指南编写组件单元测试
- 执行单个组件的单元测试:
pnpm test:unit3 button
如果你是一位经验丰富的开发者,想承受一些有挑战的工作,能够思考以下工作:
- ✨ [Feature]: 心愿提供 Skeleton 骨架屏组件
- ✨ [Feature]: 心愿提供 Divider 分割线组件
- ✨ [Feature]: tree树形控件能减少虚构滚动性能
- ✨ [Feature]: 减少视频播放组件
- ✨ [Feature]: 减少思维导图组件
- ✨ [Feature]: 增加相似飞书的多维表格组件
- ✨ [Feature]: 增加到 unplugin-vue-components
- ✨ [Feature]: 兼容formily
参加 OpenTiny 开源社区奉献,你将播种:
间接的价值:
- 通过参加一个理论的跨端、跨框架组件库我的项目,学习最新的
Vite
+Vue3
+TypeScript
+Vitest
技术 - 学习从 0 到 1 搭建一个本人的组件库的整套流程和方法论,包含组件库工程化、组件的设计和开发等
- 为本人的简历和职业生涯添彩,参加过优良的开源我的项目,这自身就是受面试官青眼的亮点
- 结识一群优良的、酷爱学习、酷爱开源的小伙伴,大家一起打造一个平凡的产品
久远的价值:
- 打造集体品牌,晋升集体影响力
- 造就良好的编码习惯
- 取得华为云 OpenTiny 团队的荣誉和定制小礼物
- 受邀加入各类技术大会
- 成为 PMC 和 Committer 之后还能参加 OpenTiny 整个开源生态的决策和长远规划,造就本人的治理和布局能力
- 将来有更多机会和可能
对于 OpenTiny
OpenTiny 是一套企业级组件库解决方案,适配 PC 端 / 挪动端等多端,涵盖 Vue2 / Vue3 / Angular 多技术栈,领有主题配置零碎 / 中后盾模板 / CLI 命令行等效率晋升工具,可帮忙开发者高效开发 Web 利用。
外围亮点:
跨端跨框架
:应用 Renderless 无渲染组件设计架构,实现了一套代码同时反对 Vue2 / Vue3,PC / Mobile 端,并反对函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强。组件丰盛
:PC 端有100+组件,挪动端有30+组件,蕴含高频组件 Table、Tree、Select 等,内置虚构滚动,保障大数据场景下的晦涩体验,除了业界常见组件之外,咱们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP地址输入框、Calendar 日历、Crop 图片裁切等配置式组件
:组件反对模板式和配置式两种应用形式,适宜低代码平台,目前团队曾经将 OpenTiny 集成到外部的低代码平台,针对低码平台做了大量优化周边生态齐全
:提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供蕴含 10+ 实用功能、20+ 典型页面的 TinyPro 中后盾模板,提供笼罩前端开发全流程的 TinyCLI 工程化工具,提供弱小的在线主题配置平台 TinyTheme
分割咱们:
- 官网公众号:
OpenTiny
- OpenTiny 官网:https://opentiny.design/
- OpenTiny 代码仓库:https://github.com/opentiny/
- Vue 组件库:https://github.com/opentiny/tiny-vue (欢送 Star)
- Angluar组件库:https://github.com/opentiny/ng (欢送 Star)
- CLI工具:https://github.com/opentiny/tiny-cli (欢送 Star)
更多视频内容也能够关注OpenTiny社区,B站/抖音/小红书/视频号。