什么是 Zone ?
如果你接触过 Angular,大概率听说过 zone.js 这个名字。不过,Angular 为什么要引入它?zone.js 到底能解决什么问题?今天这篇文章,我们先单独把 zone.js 拎出来聊聊。至于它在 Angular 框架里具体扮演什么角色,咱们放到下一篇文章细说。

先回答最核心的问题:什么是 Zone?官方文档的定义是:Zone 是一个跨多个异步任务的执行上下文。翻译乘人话就是——Zone 在拦截或追踪异步任务上,有着极其强大的能力。
工作原理剖析
光说不练假把式,我们直接拿一个具体的例子来看看它到底能做些什么,顺便拆解一下背后是怎么实现的。
这个例子很简单:页面加载后,第一个按钮的点击事件会去绑定第二个按钮的事件,第二个按钮的点击则会抛出一个异常。如果我们先后点击“Bind Error”和“Cause Error”这两个按钮,控制台会输出类似这样的错误信息:
(索引):26 Uncaught Error: aw shucks
at HTMLButtonElement.throwError ((索引):26:13)
从报错信息看,只知道第二个按钮的点击函数出了问题,但它是怎么来的、谁绑定的,完全看不到。
zone.js启动
那如果我们用 zone.js 来运行呢?换一下启动代码试试:
Zone.current.fork(
{
name: 'error',
onHandleError: function (parentZoneDelegate, currentZone, targetZone, error) {
console.log(error.stack);
}
}
).fork(Zone.longStackTraceZoneSpec).run(main);
这次控制台输出的内容丰富得多:
Error: aw shucks
at HTMLButtonElement.throwError ((索引):26:13)
at ZoneDelegate.invokeTask (zone.js:406:31)
at Zone.runTask (zone.js:178:47)
at ZoneTask.invokeTask [as invoke] (zone.js:487:34)
at invokeTask (zone.js:1600:14)
at HTMLButtonElement.globalZoneAwareCallback (zone.js:1626:17)
at ____________________Elapsed_571_ms__At__Mon_Jan_31_2022_20_09_09_GMT_0800_________ (localhost)
at Object.onScheduleTask (long-stack-trace-zone.js:105:22)
at ZoneDelegate.scheduleTask (zone.js:386:51)
at Zone.scheduleTask (zone.js:221:43)
at Zone.scheduleEventTask (zone.js:247:25)
at HTMLButtonElement.addEventListener (zone.js:1907:35)
at HTMLButtonElement.bindSecondButton ((索引):23:10)
at ZoneDelegate.invokeTask (zone.js:406:31)
at Zone.runTask (zone.js:178:47)
at ____________________Elapsed_2508_ms__At__Mon_Jan_31_2022_20_09_06_GMT_0800_________ (localhost)
at Object.onScheduleTask (long-stack-trace-zone.js:105:22)
at ZoneDelegate.scheduleTask (zone.js:386:51)
at Zone.scheduleTask (zone.js:221:43)
at Zone.scheduleEventTask (zone.js:247:25)
at HTMLButtonElement.addEventListener (zone.js:1907:35)
at main ((索引):20:10)
at ZoneDelegate.invoke (zone.js:372:26)
at Zone.run (zone.js:134:43)
两相对比,差异很明显。没有 zone.js 时,我们只能看到异常是按钮2的点击函数抛出的。而引入 zone.js 之后,不仅能定位到异常本身的抛出处,还能看到这个事件函数是由按钮1的点击函数绑定的,甚至能追溯到整个应用的入口 main 函数。这种跨多个异步任务持续追踪的能力,在大型复杂项目中简直就是救命稻草。那么 zone.js 到底是怎么做到的呢?
秘密在于 zone.js 接管了浏览器提供的原生异步 API,比如点击事件、定时器等。正是通过这种“狸猫换太子”的方式,它获得了对异步操作的更强的控制力。我们拿点击事件来举例,看看它是怎么动手脚的。
proto[ADD_EVENT_LISTENER] = makeAddListener(nativeAddEventListener,..)
这里的 proto 指向的是 EventTarget.prototype,也就是说,这行代码重新定义了 addEventListener 函数。
makeAddListener函数
那 makeAddListener 内部又做了什么呢?
function makeAddListener() {
......
// 关键代码1
nativeListener.apply(this, arguments);
......
// 关键代码2
const task = zone.scheduleEventTask(source, ...)
......
}
核心逻辑其实就两件事:第一,在自定义的包装函数里,仍然调用了浏览器原生的 addEventListener;第二,为每个点击函数安排了一个事件任务——也就是执行了 zone.scheduleEventTask。这第二步,才是 Zone 能实现这一切的秘密武器。
回到开头的示例,控制台之所以能输出那么完整的调用栈,靠的正是 onScheduleTask 这个钩子:
onScheduleTask: function (..., task) {
const currentTask = Zone.currentTask;
let trace = currentTask && currentTask.data && currentTask.data[creationTrace] || [];
trace = [new LongStackTrace()].concat(trace);
task.data[creationTrace] = trace;
}
前面控制台里那串长长的调用栈,实际上就存储在 currentTask.data[creationTrace] 中,它是一个由 LongStackTrace 实例组成的数组。每次有新的异步任务被安排时,onScheduleTask 都会把当前的函数调用栈记录下来。至于这个栈是怎么来的,看看 LongStackTrace 的构造器就明白了:
class LongStackTrace {
constructor() {
this.error = getStacktrace();
this.timestamp = new Date();
}
}
function getStacktraceWithUncaughtError() {
return new Error(ERROR_TAG);
}
this.error 保存的正是函数调用栈。看到 new Error 出现,相信大家也大概猜到它获取调用栈的机制了——其实就是利用 Error 对象的栈信息。
这篇文章只是展示了 zone.js 能力的一个侧面。如果你还想深入了解其他功能,建议直接查阅官方文档。但通过这个例子,希望你对 zone.js 已经有了一个直观的认识。毕竟,它是 Angular 变更检测机制中不可或缺的基石。
