简介

Angular 引入 Zone.js 以处理变更检测。Zone.js 使 angular 可以决定何时需要刷新UI。

Zone.js有Node和Web的不同版本,仅描述Web版本。

Zone.js采用Monkey-patch的方式对默认方法进行替换,目前有标准Api和非标准Api。

两种Patch方式:Wrap和Task

There are several patch mechanisms

  • wrap: makes callbacks run in zones, and makes applications able to receive onInvoke and onIntercept callbacks
  • Task: just like in the JavaScript VM, applications can receive onScheduleTask, onInvokeTask, onCancelTask and onHasTask callbacks
  1. MacroTask
  2. MicroTask
  3. EventTask

Some APIs which should be treated as Tasks, but are currently still patched in the wrap way. These will be patched as Tasks soon.

对于Api的Patch方式不同,控制的颗粒度是不同的:

Wrap方式:onInvokeonIntercept

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

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

image.png

解决了什么问题?

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

基本用法和示例

概念

  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

暴力替换

image.png

timers的方法,调用了patchTimer

image.png

patchTimer里,调用了patchMethod

image.png

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

image.png

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

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

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

Zone

image.png

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

image.png

Delegate

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

image.png

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

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

image.png

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

默认的钩子函数定义如下:image.png

Fork

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

image.png

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

image.png

delegate的fork方法定义如下:

image.png

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

Run

Run方法会调整_currentZoneFrame的状态。

image.png

同时会调用delegate的invoke方法:

image.png

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

image.png

1. onSchedule

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

image.png

image.png

image.png

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

image.png

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

image.png

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

2. onInvoke

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

image.png

如果我们在某一个父级的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的过程

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:

image.png

无Zone:

image.png

资料

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/

JiuAS
1 声望0 粉丝