代码改变世界

Zone.js 简介 & 抛砖引玉

2016-09-12 11:10  Justany_WhiteSnow  阅读(3472)  评论(0编辑  收藏  举报

Zone.js是angular团队参照NodeJS的Domain,Dart的Zone,为angular 2开发的核心组件。

 screenshot

一开始,我对Zone.js是拒绝的。我们知道类似的 Domain 模块,主要是为了解决异步错误跟踪问题。所以,当我没有太强烈的错误跟踪需求的时候,Zone.js有啥用?

然而execution context不仅仅可以用来跟踪异步错误,还可以做一些猥琐而实用的事情。

先来理解一下 execution context

Zone.current.fork({}).run(function () {
    Zone.current.inTheZone = true;

    setTimeout(function () {
        console.log('in the zone: ' + !!Zone.current.inTheZone); // 'in the zone: true'
    }, 0);
});

console.log('in the zone: ' + !!Zone.current.inTheZone); // 'in the zone: false'

execution context,我们可以理解成只与当前 fork 出来的Zone实例相关的上下文。

上面的例子很明显,因为只有在 fork 中 Zone 的实例我们才设置了 Zone.current.inTheZone 为 true,所以在外面打印出来的结果是 false。

好神奇,这个怎么做到的呢?

我们想像上面那个过程是同步的,那么发生了什么呢?

const defaultZone = Zone.current
// 生成一个新的Zone
const zone = new Zone()
// 设置当前zone
Zone.current = zone
// 对当前zone设值
Zone.current.inTheZone = true
console.log('in the zone: ' + !!Zone.current.inTheZone)
// 退出当前zone
Zone.current = defaultZone
console.log('in the zone: ' + !!Zone.current.inTheZone)

很好,同步没有什么问题,那么异步怎么办呢?其实很简单,就是在每一个异步入口加一个看门人,就可以了。

const defaultZone = Zone.current
// 生成一个新的Zone
const zone = new Zone()
// 设置当前zone
Zone.current = zone
// 对当前zone设值
Zone.current.inTheZone = true
const anonymousA = function () {
  console.log('in the zone: ' + !!Zone.current.inTheZone); // 'in the zone: true'
}
// 给进入异步的函数配发令牌映射到对应zone
anonymousA._zone = zone
// 退出当前zone
Zone.current = defaultZone
setTimeout(() => {
  // 函数重新回来,设置当前 zone
  Zone.current = anonymousA._zone
  anonymousA.call(this)
  // 退出当前zone
  Zone.current = defaultZone
}, 0)
console.log('in the zone: ' + !!Zone.current.inTheZone)

当然Zone.js实现比上面复杂得多,有兴趣的同学可以看看源代码。

在同一个项目使用不同版本的 jQuery

从上面的例子看,我们可以看到,我们可以在 zone 实例上保存只有该 zone 使用的属性。那么我们在利用Object.defineProperty就可以达成我们的目标了。

  • 我们先简单写一个模块执行器(意思是我才不想管加载的事情):
// 写的巨简单,不要吐槽
!function (win, Zone) {
    var map = {};
    var noop = {};
    var dependence = {};
    var alias = {};
    var hasSet = {};

    // 因为懒,仅支持 define(name, factory),反正只是 demo
    function define(name, factory) {
        if (typeof factory === 'function') {
            map[name] = {
                factory: factory,
                exports: noop 
            };
        } else {
            map[name] = {
                exports: factory
            };
        }
    }

    function require(name) {
        var module = map[name]
        if (module.exports !== noop) return module.exports;
        if (dependence[name]) {
            var properties = {};
            // 利用Object.defineProperty 组装 window.xxx -> require('xxx') 的映射
            Object.keys(dependence[name]).forEach(function (key) {
                var res;
                if (alias[key]) res = alias[key];
                else res = key;
                properties[res] = require(key + '@' + dependence[name][key]);
                if (!hasSet[res]) {
                    hasSet[res] = true;
                    Object.defineProperty(window, res, {
                        get: function () {
                            return Zone.current.get(res)
                        }
                    });
                }
            });
            // 对每个模块,fork 一个 Zone 实例进行执行
            Zone.current.fork({
                properties: properties
            }).run(function () {
                module.exports = module.factory()
            });
        } else {
            module.exports = module.factory();
            return module.exports;
        }
    }

    function config(opt) {
        Object.assign(dependence, opt.dep);
        Object.assign(alias, opt.alias);
    }

    require.config = config;
    window.define = define;
    window.require = require;
}(window, Zone)
  • 试用一下:
// 模拟两个jQuery
define('jquery@1.4', {
    version: '1.4',
    bind: function () {
        console.log('call bind');
    }
})
define('jquery@1.8', {
    version: '1.8',
    on: function () {
        console.log('call on');
    }
})
// 仅仅打印版本,不做任何事情 
function logVersion() {
    console.log('version === ', $.version)
}
// 要运行的第一段代码
define('module1', function module1() {
    // 使用1.8版本
    $.on();
    // 证明即使异步调用,这里面的 $ 依然指向正确
    setTimeout(logVersion, 100)
})
// 要运行的第二段代码
define('module2', function module2() {
    // 使用1.4版本
    $.bind();
    // 证明即使异步调用,这里面的 $ 依然指向正确
    setTimeout(logVersion, 300)
})
// 载入依赖
require.config({
    dep: {
        module1: {
            'jquery': '1.8'
        },
        module2: {
            'jquery': '1.4'
        }
    },
    alias: {
        'jquery': '$'
    }
})
require('module1')
require('module2')

具体实现参见:(two-different-jquery)[https://github.com/miniflycn/async-technique-you-may-do-not-know/tree/master/two-different-jquery]

更进一步

其实我们可以基于 Zone.js 做一个 Sandbox,则在大型重历史包袱的应用中,可以很好地将多个技术体系共存而不产生恶心的冲突问题。

或者做一个对任意模块依赖注入的方案,对模块之间做完全解耦。