angular2
使用了zone.js
,这篇文章将介绍一下zone.js
是干什么的,以及zone.js
在angular2
中是怎么应用的。
先看一下zone.js
的官方说明
A Zone is an execution context that persists across async tasks, and allows the creator of the zone to observe and control execution of the code within the zone.
说通俗点:一个zone
可作为多个异步任务执行的上下文,并能够控制这些异步任务。
为了讲明白这句话,我分下面几步来说明:
zone
?zone
会作为哪些异步任务执行的上下文?zone
怎么控制异步任务?需要通过已存在的zone
来创建新的zone
。
假设存在一个alreadyExistedZone
,通过调用它的fork
方法来创建一个新的zone
。
fork
方法传入的参数用来配置新的zone
,参数中的name
属性用来给zone
设置一个名称,方便我们调试时知道当前zone
是谁。
var newZone = alreadyExistedZone.fork({
name: 'new-zone'
});
console.log(newZone.name); // new-zone
newZone
被称为alreadyExistedZone
的子zone
。
newZone.parent
可以访问到alreadyExistedZone
,但没有提供从alreadyExistedZone
访问newZone
的方法。
console.log(newZone.parent === alreadyExistedZone); // true
一个zone
可以有多个子zone
,每次调用fork
方法,都增加一个新的子zone
。
你是不是有个疑问:第一个zone哪来的?
zone.js
初始化时候生成好了,通过Zone.root
来访问:
var rootZone = Zone.root;
console.log(rootZone.name); // <root>
可以看出最终所有的zone
会生成一个树状结构,Zone.root
就是这棵树的根。
先看一段代码
console.log(`begin----current zone is ${Zone.current.name}`);
var zoneA = Zone.current.fork({
name: 'ZoneA'
});
zoneA.run(() => {
console.log(`run------current zone is ${Zone.current.name}`);
});
console.log(`end------current zone is ${Zone.current.name}`);
输出
begin----current zone is <root>
run------current zone is ZoneA
end------current zone is <root>
解释一下Zone.current
:
Zone.current
返回当前zone
,即当前上下文Zone.current
总是有值的,不在任何上下文中,它也会返回RootZone
官方原文
RootZone is ambient and it is indistinguishable from no Zone.
可以认为RootZone和没有Zone一样。
可以看出zoneA.run(fn)
fn
执行的上下文为zoneA
。
上个例子里我们的代码都是同步执行的,看起来也没什么大不了。
我们加点异步任务:setTimeout
和addEventListener
。
console.log(`begin----current zone is ${Zone.current.name}`);
var zoneA = Zone.current.fork({
name: 'ZoneA'
});
zoneA.run(() => {
testSetTimeout();
testAddEventListener();
});
function testSetTimeout() {
setTimeout(() => {
console.log(`setTimeout callback----current zone is ${Zone.current.name}`);
}, 1000)
}
function testAddEventListener() {
document.body.addEventListener('click', event => {
console.log(`mouseclick handler----current zone is ${Zone.current.name}`);
});
}
console.log(`end------current zone is ${Zone.current.name}`);
点击几下鼠标,输出:
begin----current zone is <root>
end------current zone is <root>
setTimeout callback----current zone is ZoneA
mouseclick handler----current zone is ZoneA
mouseclick handler----current zone is ZoneA
mouseclick handler----current zone is ZoneA
mouseclick handler----current zone is ZoneA
可以看出setTimeout
回调函数执行的上下文是ZoneA
,鼠标事件监听函数执行的上下文也是ZoneA
。你还可以尝试其他异步任务,如Promise setInterval requestAnimationFrame ajax
,都会如此。
说明一下:
回调函数执行的上下文是ZoneA
,回调函数中再触发异步任务,新触发的异步任务其回调函数执行的上下文仍是ZoneA
,无限的嵌套,都是如此。
想要改变一段代码执行的上下文,可以使用另一个zone
的run
方法去执行这段代码。
我这里只讲用法,对zone.js
是怎么实现的 有兴趣可以去看官方文档,查看monkey patch
相关内容。
结论:
zoneA.run(fn)
fn
的上下文,以及fn
触发的异步任务回调函数执行的上下文都将是zoneA
。
通过zone.run(fn)
能让fn
触发的异步任务都在同一个上下文【zone
】执行。
现在看看zone
怎么控制fn
触发的异步任务。
zone
提供了一些钩子方法控制异步任务,这里我主要介绍下面两个。
onScheduleTask
onInvokeTask
看下面这段程序,异步任务创建时打印日志,异步任务回调开始、结束时打印日志。
var parent = Zone.current;
var child = parent.fork({
name: 'child',
onScheduleTask: function (parentZoneDelegate, currentZone, targetZone, task) {
console.log(`schedule task at: ${new Date().getTime()}`);
return parentZoneDelegate.scheduleTask(targetZone, task);
},
onInvokeTask: function (parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs) {
console.log(`start executing callback at: ${new Date().getTime()}`);
parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
console.log(`callback execution over at: ${new Date().getTime()}`);
}
});
child.run(() => {
setTimeout(() => {
var sum = 0;
for (var i = 0; i < 100000000; i++) {
sum += i;
}
}, Math.random() * 10000);
});
输出:
schedule task at: 1495371621045
start executing callback at: 1495371628109
callback execution over at: 1495371628237
这次调用zone.fork
方法参数对象添加了两个新属性:onScheduleTask
和onInvokeTask
,这两个属性的值都是方法。
解释一下这两个方法的参数:
task
:代表异步任务。zone.js
将异步任务的信息封装到task
对象中。 parentZoneDelegate
: parent
,之前说过所有zone
生成了一个树形结构,有父子关系,这里的parent
就是指zone
的parent
。 delegate
,每个zone
都有一个ZoneDelegate
,可理解为真正做实事的是这个ZoneDelegate
。 parentZoneDelegate.scheduleTask
就是让父zone
去创建异步任务,会进入父zone
的onScheduleTask
,父zone
接着让父zone的父zone
处理……直到给了RootZone
,RootZone
最终生成了异步任务。parentZoneDelegate.invokeTask
也是同样的道理。targetZone
currentZone
:由于异步任务的处理会一级级的向上传递,targetZone
指的就是异步任务真正是在哪个zone
触发创建的,而currentZone
指的是目前处理这个异步任务的zone
是谁。类似event
里的target
和currentTarget
。通过onScheduleTask
onInvokeTask
,我们能在异步任务创建的前后,异步任务回调函数执行的前后添加一些需要的操作。在这两个钩子方法中,我们也可以选择不调用parentZoneDelegate
的方法,而直接在当前zone
处理异步任务。
angular
用zone.js
来判断什么时候需要更新视图。
angular
认为视图需要更新都是由异步任务导致的,如鼠标事件交互,ajax请求,timer等,那么只需要在异步任务回调执行完后来检查组件视图是否需要更新就可以了。
分析一下@angular/core
的几段代码:
应用启动的入口:
PlatformRef_.prototype._bootstrapModuleFactoryWithZone = function (moduleFactory, ngZone) {
var _this = this;
if (!ngZone)
ngZone = new NgZone({ enableLongStackTrace: isDevMode() });
return ngZone.run(function () {
// 应用启动
......
});
};
假如ngZone
是一个zone
,那么可以理解为整个应用的上下文是ngZone
,ngZone
可以拦截应用里异步任务的回调函数。
看看NgZone
到底是什么,下面是NgZone
的构造函数。
function NgZone(_a) {
......
this.outer = this.inner = Zone.current;
......
this.forkInnerZoneWithAngularBehavior();
}
NgZone.prototype.run = function (fn) { return this.inner.run(fn); };
NgZone
不是Zone
,但NgZone
的属性inner
和outer
是Zone
,他们初始值为Zone.current
即RootZone
。
NgZone.run
实质就是inner.run
。
再看一下NgZone
的forkInnerZoneWithAngularBehavior
。
NgZone.prototype.forkInnerZoneWithAngularBehavior = function () {
var _this = this;
this.inner = this.inner.fork({
name: 'angular',
properties: /** @type {?} */ ({ 'isAngularZone': true }),
onInvokeTask: function (delegate, current, target, task, applyThis, applyArgs) {
try {
_this.onEnter();
return delegate.invokeTask(target, task, applyThis, applyArgs);
}
finally {
_this.onLeave();
}
},
......
});
};
inner
在这里,被赋值成一个name
为angular
的zone
,是RootZone
的子zone
。
inner
添加了钩子函数onInvokeTask
,所有以inner
为上下文的异步任务回调函数执行前都要进这个方法。结合前面应用启动的代码,可以说应用的所有异步任务回调函数都会被这个方法拦截。
这个方法里的delegate.invokeTask
会触发回调函数的执行,之后的_this.onLeave()
会引起视图更新检查。这里就不展开说了,会涉及到zone.js
对异步任务的分类,以及rxjs
相关方面内容。
最后推荐大家阅读一下官方文档,希望看完这篇文章对你阅读官方文档有帮助。