异步任务管理神器-zone.js

angular2使用了zone.js,这篇文章将介绍一下zone.js是干什么的,以及zone.jsangular2中是怎么应用的。

先看一下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可作为多个异步任务执行的上下文,并能够控制这些异步任务。

为了讲明白这句话,我分下面几步来说明:

  1. 怎么创建一个zone
  2. 一个zone会作为哪些异步任务执行的上下文?
  3. 一个zone怎么控制异步任务?

1. 怎么创建一个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就是这棵树的根。


2. 一个zone会作为哪些异步任务执行的上下文?

先看一段代码

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


上个例子里我们的代码都是同步执行的,看起来也没什么大不了。
我们加点异步任务:setTimeoutaddEventListener

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,无限的嵌套,都是如此。
想要改变一段代码执行的上下文,可以使用另一个zonerun方法去执行这段代码。
我这里只讲用法,对zone.js是怎么实现的 有兴趣可以去看官方文档,查看monkey patch相关内容。

结论:
zoneA.run(fn) fn的上下文,以及fn触发的异步任务回调函数执行的上下文都将是zoneA


3. 一个zone怎么控制异步任务?

通过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方法参数对象添加了两个新属性:onScheduleTaskonInvokeTask,这两个属性的值都是方法。

解释一下这两个方法的参数:

  • task:代表异步任务。zone.js将异步任务的信息封装到task对象中。
  • parentZoneDelegate
    先说parent,之前说过所有zone生成了一个树形结构,有父子关系,这里的parent就是指zoneparent
    再说delegate,每个zone都有一个ZoneDelegate,可理解为真正做实事的是这个ZoneDelegate
    那么parentZoneDelegate.scheduleTask就是让父zone去创建异步任务,会进入父zoneonScheduleTask父zone接着让父zone的父zone处理……直到给了RootZoneRootZone最终生成了异步任务。parentZoneDelegate.invokeTask也是同样的道理。
  • targetZone currentZone:由于异步任务的处理会一级级的向上传递,targetZone指的就是异步任务真正是在哪个zone触发创建的,而currentZone指的是目前处理这个异步任务的zone是谁。类似event里的targetcurrentTarget

通过onScheduleTask onInvokeTask,我们能在异步任务创建的前后,异步任务回调函数执行的前后添加一些需要的操作。在这两个钩子方法中,我们也可以选择不调用parentZoneDelegate的方法,而直接在当前zone处理异步任务。


4. zone.js在angular中的应用

angularzone.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,那么可以理解为整个应用的上下文是ngZonengZone可以拦截应用里异步任务的回调函数。


看看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的属性innerouterZone,他们初始值为Zone.currentRootZone
NgZone.run实质就是inner.run

再看一下NgZoneforkInnerZoneWithAngularBehavior

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在这里,被赋值成一个nameangularzone,是RootZone子zone
inner添加了钩子函数onInvokeTask,所有以inner为上下文的异步任务回调函数执行前都要进这个方法。结合前面应用启动的代码,可以说应用的所有异步任务回调函数都会被这个方法拦截。
这个方法里的delegate.invokeTask会触发回调函数的执行,之后的_this.onLeave()会引起视图更新检查。这里就不展开说了,会涉及到zone.js对异步任务的分类,以及rxjs相关方面内容。


最后推荐大家阅读一下官方文档,希望看完这篇文章对你阅读官方文档有帮助。

猜你喜欢

转载自blog.csdn.net/github_39212680/article/details/73410009