You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
JavaScript 是单线程,基于事件循环的,而通过 Zone.js,我们可以把处理事件的回调函数放到不同的 zone 里面执行,而且在当前回调函数内触发的异步事件也会在当前 zone 里面得到处理,即我们给事件的回调函数提供了执行环境。而且,Zone.js 还提供了钩子,允许我们在回调函数执行前后执行额外一些代码(还有其他的一些钩子)。
onHasTask:
(delegate: ZoneDelegate,current: Zone,target: Zone,hasTaskState: HasTaskState)=>{delegate.hasTask(target,hasTaskState);if(current===target){// We are only interested in hasTask events which originate from our zone// (A child hasTask event is not interesting to us)if(hasTaskState.change=='microTask'){zone.hasPendingMicrotasks=hasTaskState.microTask;checkStable(zone);}elseif(hasTaskState.change=='macroTask'){zone.hasPendingMacrotasks=hasTaskState.macroTask;}}},
Angular 源码解析系列。这篇文章有关于 Zone.js 的用途,实现和 NgZone 的实现,以及 Angular 如何使用 Zone.js 实现自动变更检测。
这篇文章是 Angular 源码解析系列的第一篇,分为以下三个小节:
阅读这篇文章你需要熟悉 JavaScript 的事件循环 (Event Loop) 机制。
现在 Zone.js 的源码已经被放到 Angular 底下以 mono repo 的形式管理,你可以通过下面的链接阅读 Zone.js 的源码。
angular/angular
为什么 Angular 需要 Zone.js?
所有前端框架需要解决的一个共同问题就是:应该何时将应用状态的变化反映到视图中,即变更检测。React 的方案是交给用户自行决定,即让用户通过
setState
方法告诉 React 应用的状态发生了改变;Vue 通过拦截对象的赋值操作来监测状态改变(即所谓响应式);而 Angular 的方案就是 Zone.js。Zone.js 通过给一些会触发异步事件 API 打补丁(monkey patch),比如 XHR、DOM event、定时器等来监听异步事件的编排(比如调用setTimeout
)和触发(比如setTimeout
到时),而应用状态的变化一定是某个异步事件的结果,这样 Angular 就可以借助 Zone.js 实现变更检测。体现在代码中:Angular 应用会在 zone 的
onMicrotaskEmpty
回调中调用tick
方法,而tick
方法会调用顶层组件的detectChanges
方法执行变更检测,就是下面这行代码:可以通过看代码注释来了解官方对 Zone.js 作用的描述。
Zone.js 如何工作?
编程模型
把 Zone.js 中的 zone 想象成 JavaScript VM 线程里的 mini 线程。
JavaScript 是单线程,基于事件循环的,而通过 Zone.js,我们可以把处理事件的回调函数放到不同的 zone 里面执行,而且在当前回调函数内触发的异步事件也会在当前 zone 里面得到处理,即我们给事件的回调函数提供了执行环境。而且,Zone.js 还提供了钩子,允许我们在回调函数执行前后执行额外一些代码(还有其他的一些钩子)。
总而言之——
核心代码
核心部分实现了 Zone.js 的机制,而不关心各种 patch 该如何实现,代码都在 zone.ts 当中,前面几百行都是接口声明,请自行阅读,本文主要聚焦于其实现。
重要类型和方法
这个文件主要声明和实现了如下几个类:
Zone
,JavaScript 事件的执行环境,和线程一样,它们可以带一些数据,并且可能拥有父子 zone。ZoneTask
,包装后的异步事件,这些 task 有三种子类:MicroTask
,由Promise
创建,我们知道 native 的Promise
是在当前事件循环结束前就要执行的,所以打过补丁的 Promise 也应该在事件循环结束前执行。MacroTask
,由setTimeout
等创建,native 的setTimeout
会在将来某个时间被处理,而且会被处理一到多次。EventTask
,由addEventListener
等创建,这些 task 可能被触发多次,也可能一直不会被触发。ZoneSpec
,创建一个 zone 时给它提供的参数,除了name
是必须的外,还可以传入如下的钩子:onFork
,创建新 zone 的钩子。onIntercept
,包装某个回调函数时触发的钩子。onInvoke
,调用某个回调函数时触发的钩子。onHandleError
,调用某个回调函数出错时触发的钩子。onScheduleTask
,ZoneTask
被安排时触发的钩子,比如调用了setTimeout
。onInvokeTask
,ZoneTask
被触发时触发的钩子,比如setTimeout
到时。onCancelTask
,ZoneTask
被取消时触发的钩子,比如用clearTimeout
取消了计时器。onHasTask
,检测到有或无ZoneTask
时触发的钩子(即对第一个 schedule 的 zone 和最后一个 invoke 或 cancel 的 task 触发)。ZoneDelegate
,负责调用钩子。Zone
类这些代码是对 Zone 的实现。
有几个值得关注的静态方法:
get root()
,该方法返回根 zone,所有其他 zone 都是该 zone 的子孙,类似于操作系统的第一个进程。根 zone 是 Zone.js 初始化时自行创建的,相关代码在这里。根 zone 确保了所有的异步函数都在 Zone.js 的机制内运行。get current()
,返回当前 zone,类似于单线程 CPU 中正在占用 CPU 的进程,它本质上是返回闭包内的一个变量_currentZoneFrame
的引用。get currentTask
,返回当前正被 invoke 的 task。__load_patch
,这是 Zone.js 加载补丁的方法,后面讲解 patch 的加载时会详细说明。这个类的构造函数,要点如下:
parent
变量保存了父 zone,这样 zone 就可以形成一个树型结构。_properties
上作为函数运行的上下文,而get()
和getZoneWith()
方法分别用于取得某个 key 所对应的变量和上下文中有某个 key 的 zone。ZoneDelegate
并赋值给_zoneDelegate
属性。Zone
类还有如下的实例方法:fork
,创建一个子 zone,相当于 fork 一个进程。wrap
,包裹一个函数,这个被包裹的函数执行时,首先会通过runGurad
把该函数运行的上下文置换为原来包裹它的 zone,然后通过ZoneSpec
去执行钩子。run
,立即在当前 zone 执行函数,并调用ZoneSpec
执行invoke
钩子。runGuarded
,和 run 做的事情基本相同,不同点在于如果执行过程出错,错误会被ZoneSpec
注册的 error 钩子先处理,如果ZoneSpec
error 钩子不能处理,再抛出。runTask
,运行一个ZoneTask
:ZoneTask
类的实现。MacroTask
和EventTask
两种类型的 task 是不需要执行的。ZoneDelegate
的方法来执行 task。reEntryGuard
进行状态保护。scheduleTask
,用来安排一个 task 的执行环境。ZoneDelegate
类代码在这里。
要点:
ZoneDelegate
的(当然父级也有可能是空的)。这样父级 zone 可以通过 delegate 干预子 zone 的行为。scheduleTask()
,该方法对于MicroTask
会调用scheduleMicroTask
。ZoneTask
这个类的代码在这里。
要点:
_state
,记录了这个 task 的状态。task
为当前 task。static invokeTask()
,该函数执行一个 task:_numberOfNestedTaskFrames
计数器的值。drainMicroTaskQueue
尝试清空所有的MicroTask
。_transitionTo
,改变 task 的状态,task 可以从两个源状态转移到一个目标状态,当 zone 的状态不属于两个源状态中的任何一个时,这个方法会抛出错误。除了上面几个重要的类,下面两个方法也值得关注:
scheduleMicroTask
,这个方法是对 Promise 这样的所谓 micro task 的处理。drainMicroTaskQueue
。drainMicroTaskQueue
。该方法内容十分简单,即尝试对_microTaskQueue
中的每一个 MicroTask run 一下。_numberOfNestedTaskFrames === 1
时。补丁的实现
我们之前提到了
__load_patch
方法是 Zone.js 用来加载补丁的,这一小节我们将以setTimeout
为例介绍 Zone.js 如何加载补丁。同时还会结合上一小节的内容,讲解setTimeout
在 Zone.js 执行的全过程。因为各个 JavaScript runtime 对异步函数的支持情况不尽相同(比如在 Node.js 环境里不可能有 DOM 事件相关的异步函数,如addEventListener
),所以 Zone.js 会给不同的 runtime 提供不同的 dist 包,patch 不同的异步函数。好在不管在任何环境中,setTimeout
都是存在的。patch 的具体实现在这里,下面我们将会仔细讲解这部分代码。
首先准备好要 patch 的函数的名称:
接下来,调用
patchMethod
方法,传入的三个参数分别是目标对象(被 patch 后的函数应当挂载在目标对象上,因为setTimeout
其实是window
的一个属性,所以这里的形参叫做window
),被 patch 函数的名字,以及一个回调函数,请注意这个回调函数直接返回了另外一个函数(如果你了解什么叫做柯里化,应该很容易理解,其实就是让该函数的执行时能够访问到delegate
参数):来看
patchMethod
方法:首先,该方法从
target
的原型链上找到 name 代表的方法的具体位置(不要忘记 JavaScript 访问对象属性是通过原型链机制进行的),如果找不到这个方法,就直接在target
上创建 patch 过的方法.然后,检查 patch 过的方法是否存在,不存在才进行 patch。
在进行 patch 时:
delegate
先指向原生方法。PropertyDescriptor
的writable
字段),不然就用原来的,相当于没有 patch。patchFn
(这里是一个类似于柯里化的过程),将patchDelegate
变量指向 patch 过后的函数,然后再将proto[name]
指向一个新的函数,这里同时确保了this
能够绑定在正确的对象上(对于setTimeout
的例子就是window
)。用户执行
setTimeout
后会发生什么?我在这里给读者准备了一个简单的例子,通过给
console.log('z')
这一行打断点,就能够看到整个调用栈。setTimeout
时,实际上执行的是这一行的匿名函数,最终执行了这一行的function(self: any, args: any[])
scheduleMacroTaskWithCurrentZone
方法,这个函数直接将参数透传给Zone.current.scheduleTaskMacroTask
Zone.current.scheduleTaskMacroTask
,最终是调用了ZoneDelegate
的scheduleTask
方法。scheduleTask
方法将被调用,而这一句task.scheduleFn(task);
实际上调用了scheduleTask
函数,最终调用原生的setTimeout
。handle
,即原生setTimeout
的返回值。setTimeout
触发时,task.invoke
被调用,最终ZoneDelegate.invokeTask
被调用。Angular 如何使用 Zone.js?
Angular 应用初始化过程中,实例化了一个 NgZone 然后将所有逻辑都跑在该对象的
_inner
对象中,该_inner
即为 Angular zone。Angular 创建该 zone 的过程中传入的
ZoneSpec
的部分如下所示:在
checkStable
这个方法中你可以看到这样一行:这就联系到了开篇提到的代码:
当然
checkStable
方法还有可能在其他时机被调用,主要是通过 Zone.js 的onInvokeTask
和onInvoke
两个钩子,即在异步事件触发时,交给读者自行验证,这里就不赘述了。结论
这篇文章介绍了 Zone.js 的实现,包括 Zone.js 核心和 patch 的实现,还讲解了 Angular 对 Zone.js 的使用。
bootstrapModule
方法的第二个参数传参{ ngZone: 'noop' }
来使用一个在钩子里不做任何事情的ZoneSpec
。参考资料
The text was updated successfully, but these errors were encountered: