H5W3
当前位置:H5W3 > JavaScript > 正文

【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方式:onInvokeonIntercept

Task方式:onScheduleTask(Zone内配置了Task就会触发)onInvokeTask(Task任务触发前), onCancelTask(Task任务取消前), onHasTask(Zone内Task状态变化后触发)

如果想尝试更多的新功能,需要单独引入patch

【JS】Zone.js源码简读

解决了什么问题?

为异步任务保留了上下文环境,实现了生命周期钩子。

基本用法和示例

概念

  1. Zone区域,通过fork创建一个新的Zone
  2. 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

暴力替换

【JS】Zone.js源码简读

timers的方法,调用了patchTimer

【JS】Zone.js源码简读

patchTimer里,调用了patchMethod

【JS】Zone.js源码简读

patchMethod里,先检测是否有该方法,然后检测该方法是否可被重新赋值(writable属性):

【JS】Zone.js源码简读

isPropertyWritable返回true的话:重写proto的该方法,并且把原方法换一个新名字(加上symbol_name)保存起来。

这样就完成了patch的load工作。

以setTimeout为例,走一下内部流程

Zone

【JS】Zone.js源码简读

  1. 创建Zone时会定义的name和properties;
  2. 这里可以看到很多常用属性的查找逻辑,比如’root’,’current’;
  3. Zone内部通过_currentZoneFrame实现当前Zone的状态保存和初始化的工作;

Delegate

Zone内部方法的调用都是通过它来定义和执行。

【JS】Zone.js源码简读

Delegate类定义了钩子函数的执行规则:冒泡。fork时传入ZoneSpec的话,parent的Delegate就会被保存。

(依照此规则的话,parentDelegate肯定存在,有root的Delegate兜底)

【JS】Zone.js源码简读

在ZoneSpec里定义onInvokeTask时,第一个参数delegate被传入的是父级的delegate。所以如果当前ZoneSpec没有定义或执行完毕,就执行嵌套父级的方法,直至rootZone执行默认方法,执行原方法内容。【JS】Zone.js源码简读

默认的钩子函数定义如下:【JS】Zone.js源码简读

Fork

调用Fork创建新Zone。这里的自定义配置称作zoneSpec

【JS】Zone.js源码简读

源码里,调用Zone的fork方式是通过调用delegate的fork方法:

【JS】Zone.js源码简读

delegate的fork方法定义如下:

【JS】Zone.js源码简读

一个三元判断,这里的_forkZS来源自zoneSpec里是否定义了onFork方法,就是你是否自定义了fork的实现方式。如果没有,走 new Zone(targetZone, zoneSpec)。Zone的fork方法传的this就是这里的targetZone,这样就形成了嵌套关系。

Run

Run方法会调整_currentZoneFrame的状态。

【JS】Zone.js源码简读

同时会调用delegate的invoke方法:

【JS】Zone.js源码简读

这里的_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等:

【JS】Zone.js源码简读

1. onSchedule

patchTimer(触发patch) -> patchMethod(patch) -> scheduleMacroTaskWithCurrentZone(触发schedule)-> Zone.current.scheduleMacroTask(转化为ZoneTask) -> Zone.scheduleTask -> Delegate.scheduleTask

【JS】Zone.js源码简读

【JS】Zone.js源码简读

【JS】Zone.js源码简读

在Delegate.scheduleTask里会执行定义任务时的函数scheduleFn

【JS】Zone.js源码简读

还是以setTimeout为例,patchTimer的方法体内是这样定义scheduleFn

【JS】Zone.js源码简读

在这里,以setTimeout为例,setNative就是原生的setTimeout方法,data.args包含两个内容:原方法体原延迟时间。通过apply调用,传入原来的延迟时间,就会在相同的时间间隔后被替换为ZoneTask的task,进而触发task的invoke方法。

2. onInvoke

invoke方法与上面的onSchedule流程类似,会触发onInvoke钩子:

【JS】Zone.js源码简读

如果我们在某一个父级的ZoneSpec里的onInvokeTask没有手动触发delegate.invokeTask时,就可以阻止异步任务的执行了(当然,我们可以手动调用task.callback,主动跳出冒泡过程直接执行原方法体)。

总结

可以看到,在Zone.js内部,Zone,ZoneDelegate和ZoneTask三个类完成了所需的工作。通过分析setTimeout的过程了解到Zone.js的思路大致是暴力替换了全局的方法,但是Zone之外我们调用这些被替换的方法时,并不会触发钩子。

整个流程简单分为两部分看:

  1. Run方法调整_currentZoneFrame(即Zone.current)的状态
  2. task-ZoneTask-Hooks-task的过程

【JS】Zone.js源码简读

Zone.js通过patch的方式实现了插件化,其封装和抽象逻辑值得好好研究和学习,例如可追踪的stack信息(需引入long-stack-trace-zone.js)

// 无Zone
main();
// 有Zone
Zone.current.fork({
name: 'error',
onHandleError: function (parentZoneDelegate, currentZone, targetZone, error) {
console.log(error.stack);
}
}).fork(Zone.longStackTraceZoneSpec).run(main);

有Zone:

【JS】Zone.js源码简读

无Zone:

【JS】Zone.js源码简读

资料

Github:https://github.com/angular/angular/tree/master/packages/zone.js

CDN:https://cdnjs.com/libraries/zone.js

博客教程:

  1. https://www.cnblogs.com/whitewolf/p/zone-js.html;
  2. https://www.imwhite.com.cn/2019/10/zone-js-tutorial/;

本文地址:H5W3 » 【JS】Zone.js源码简读

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址