【JS】Zone.js源码简读
简介
Angular 引入 Zone.js 以处理变更检测。Zone.js 使 angular 可以决定何时需要刷新UI。
Zone.js有Node和Web的不同版本,仅描述Web版本。
Zone.js采用Monkey-patch的方式对默认方法进行替换,目前有标准Api和非标准Api。
两种Patch方式:Wrap和Task
对于Api的Patch方式不同,控制的颗粒度是不同的:
Wrap方式:onInvoke
和onIntercept
Task方式:onScheduleTask(Zone内配置了Task就会触发)
, onInvokeTask
(Task任务触发前), onCancelTask
(Task任务取消前), onHasTask
(Zone内Task状态变化后触发)
如果想尝试更多的新功能,需要单独引入patch
解决了什么问题?
为异步任务保留了上下文环境,实现了生命周期钩子。
基本用法和示例
概念
- Zone区域,通过fork创建一个新的Zone
- Zone对平常会用得到的异步操作都做了“替换”,将他们纳入生命周期管理中,提供了颗粒度更细的钩子
实例
通过钩子函数模拟一个监控MacroTask耗时的函数
const perfomanceZone = (function (params) {let start = 0;
let timer = performance ?
performance.now.bind(performance) : Date.now.bind(Date);
return {
onInvokeTask: function (delegate, _, target, task) {
start = timer();
delegate.invokeTask(target, task);
},
onHasTask: function (delegate, _, target, hasTaskState) {
if (!hasTaskState.macroTask) {
console.log(timer() - start);
}
}
}
}())
function perfomanceFn(asyncFn) {
Zone.current.fork(perfomanceZone).run(asyncFn);
}
perfomanceFn(function name(params) {
setTimeout(() => {
// Do Something
}, 1000)
})
多个Zone的隔离与嵌套
const ZoneA = Zone.current.fork({name: 'ZoneA',
onScheduleTask: function (parentZoneDelegate, currentZone, targetZone, task) {},
onInvokeTask: function (parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs) {}
})
const ZoneB = ZoneA.fork({
name: 'ZoneA',
onScheduleTask: function (parentZoneDelegate, currentZone, targetZone, task) {},
onInvokeTask: function (parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs) {}
})
ZoneB.run(function () {
setTimeout(() => {
console.log(123);
}, 1000);
})
在异步任务之间传递上下文
每一个Zone都有一个properties
对象,在Zone内部可以通过Zone.current.get
方法得到
源码探索
前置准备
1.Performance
mark(): 标记
measure(): 测量两个mark的difference
2.Patch
暴力替换
timers
的方法,调用了patchTimer
patchTimer
里,调用了patchMethod
在patchMethod
里,先检测是否有该方法,然后检测该方法是否可被重新赋值(writable属性):
isPropertyWritable
返回true的话:重写proto的该方法,并且把原方法换一个新名字(加上symbol_name)保存起来。
这样就完成了patch的load工作。
以setTimeout为例,走一下内部流程
Zone
- 创建Zone时会定义的name和properties;
- 这里可以看到很多常用属性的查找逻辑,比如'root','current';
- Zone内部通过
_currentZoneFrame
实现当前Zone的状态保存和初始化的工作;
Delegate
Zone内部方法的调用都是通过它来定义和执行。
Delegate类定义了钩子函数的执行规则:冒泡。fork时传入ZoneSpec的话,parent的Delegate就会被保存。
(依照此规则的话,parentDelegate肯定存在,有root的Delegate兜底)
在ZoneSpec里定义onInvokeTask时,第一个参数delegate被传入的是父级的delegate。所以如果当前ZoneSpec没有定义或执行完毕,就执行嵌套父级的方法,直至rootZone执行默认方法,执行原方法内容。
默认的钩子函数定义如下:
Fork
调用Fork创建新Zone。这里的自定义配置称作zoneSpec
源码里,调用Zone的fork方式是通过调用delegate的fork方法:
delegate的fork方法定义如下:
一个三元判断,这里的_forkZS来源自zoneSpec
里是否定义了onFork
方法,就是你是否自定义了fork的实现方式。如果没有,走 new Zone(targetZone, zoneSpec)。Zone的fork方法传的this就是这里的targetZone,这样就形成了嵌套关系。
Run
Run方法会调整_currentZoneFrame的状态。
同时会调用delegate的invoke方法:
这里的_invokeZS同上面的_forkZS,当嵌套链上的zoneSpec
有定义onInvoke钩子函数时,就会先调用钩子函数,而是否要继续执行这个task是在钩子函数里我们自定义的。
onInvoke: function (delegate, _, target, task, applyThis, applyArgs) {delegate.invoke(target, task);
}
如果不手动执行 delegate.invokeTask(target, task)
,task不会执行。
这里是从整体上控制run方法的整体,而控制单个异步任务的思路是相同。
钩子
Zone.js
通过__load_patch
替换了默认方法的实现,比如常用的setTimeout
等:
1. onSchedule
patchTimer(触发patch) -> patchMethod(patch) -> scheduleMacroTaskWithCurrentZone(触发schedule)-> Zone.current.scheduleMacroTask(转化为ZoneTask) -> Zone.scheduleTask -> Delegate.scheduleTask
在Delegate.scheduleTask里会执行定义任务时的函数scheduleFn
:
还是以setTimeout为例,patchTimer
的方法体内是这样定义scheduleFn
:
在这里,以setTimeout为例,setNative就是原生的setTimeout方法,data.args包含两个内容:原方法体和原延迟时间。通过apply调用,传入原来的延迟时间,就会在相同的时间间隔后被替换为ZoneTask的task,进而触发task的invoke方法。
2. onInvoke
invoke
方法与上面的onSchedule
流程类似,会触发onInvoke
钩子:
如果我们在某一个父级的ZoneSpec里的onInvokeTask没有手动触发delegate.invokeTask
时,就可以阻止异步任务的执行了(当然,我们可以手动调用task.callback,主动跳出冒泡过程直接执行原方法体)。
总结
可以看到,在Zone.js内部,Zone,ZoneDelegate和ZoneTask三个类完成了所需的工作。通过分析setTimeout
的过程了解到Zone.js的思路大致是暴力替换了全局的方法,但是Zone之外我们调用这些被替换的方法时,并不会触发钩子。
整个流程简单分为两部分看:
- Run方法调整_currentZoneFrame(即Zone.current)的状态
- task-ZoneTask-Hooks-task的过程
Zone.js通过patch的方式实现了插件化,其封装和抽象逻辑值得好好研究和学习,例如可追踪的stack信息(需引入long-stack-trace-zone.js)
:
// 无Zonemain();
// 有Zone
Zone.current.fork({
name: 'error',
onHandleError: function (parentZoneDelegate, currentZone, targetZone, error) {
console.log(error.stack);
}
}).fork(Zone.longStackTraceZoneSpec).run(main);
有Zone:
无Zone:
资料
Github:https://github.com/angular/angular/tree/master/packages/zone.js
CDN:https://cdnjs.com/libraries/zone.js
博客教程:
https://www.cnblogs.com/whitewolf/p/zone-js.html;
https://www.imwhite.com.cn/2019/10/zone-js-tutorial/;
以上是 【JS】Zone.js源码简读 的全部内容, 来源链接: utcz.com/a/95027.html