从这里我们能看到,zone.js对浏览器中的setTimeout、setInterval、setImmediate、以及事件、promise、地理信息geolocation都做了特殊处理。那么这些处理是怎么处理的呢?下面是关于fnPatch.patchSetClearFunction的实现代码,来自zone.js中functions.ts(https://github.com/angular/zone.js/blob/master/lib/patch/functions.ts)的代码片段:
export function patchSetClearFunction(window, Zone, fnNames) { function patchMacroTaskMethod(setName, clearName, repeating, isRaf) { //浏览器原生方法留存 var setNative = window[setName]; var clearNative = window[clearName]; var ids = {}; if (setNative) { var wtfSetEventFn = wtf.createEvent('Zone#' + setName + '(uint32 zone, uint32 id, uint32 delay)'); var wtfClearEventFn = wtf.createEvent('Zone#' + clearName + '(uint32 zone, uint32 id)'); var wtfCallbackFn = wtf.createScope('Zone#cb:' + setName + '(uint32 zone, uint32 id, uint32 delay)'); // 对浏览器原生方法的包裹封装 window[setName] = function () { return global.zone[setName].apply(global.zone, arguments); }; // 对浏览器原生方法的包裹封装 window[clearName] = function () { return global.zone[clearName].apply(global.zone, arguments); }; // 创建自己包裹方法,由上面的wind[setName]转移到这里执行. Zone.prototype[setName] = function (fn, delay) { var callbackFn = fn; if (typeof callbackFn !== 'function') { // force the error by calling the method with wrong args setNative.apply(window, arguments); } var zone = this; var setId = null; // wrap the callback function into the zone. arguments[0] = function() { var callbackZone = zone.isRootZone() || isRaf ? zone : zone.fork(); var callbackThis = this; var callbackArgs = arguments; return wtf.leaveScope( wtfCallbackFn(callbackZone.$id, setId, delay), callbackZone.run(function() { if (!repeating) { delete ids[setId]; callbackZone.removeTask(callbackFn); } return callbackFn.apply(callbackThis, callbackArgs); }) ); }; if (repeating) { zone.addRepeatingTask(callbackFn); } else { zone.addTask(callbackFn); } setId = setNative.apply(window, arguments); ids[setId] = callbackFn; wtfSetEventFn(zone.$id, setId, delay); return setId; }; ...... } } fnNames.forEach(function(args) { patchMacroTaskMethod.apply(null, args); }); };在上面的代码中,首先会将浏览器的原生方法保存在setNative中以便将会重用。紧接着zone.js就开始了它的暴力行为,覆盖window[setName]和window[clearName]然后将对setName的调用转到自身的zone[setName]的调用,zone.js就是如此暴力的对浏览器原生对象实现了拦截转移。然后它会在Task执行的前后调用自身的addRepeatingTask、addTask以及wtf事件来应用注册上的所有钩子函数。
到这里相信作为读者的你已经明白了zone.js的实现机制了,是不是和笔者一样有种“简单粗暴”的感觉?但是它真的很强大,为我们实现了对异步Task的跟踪、分析等。
zone.js应用场景zone.js能实现异步Task跟踪,分析,错误记录、开发调试跟踪等,这些都是zone.js场景的应用场景。你也可以在https://github.com/angular/zone.js/tree/master/example看见更多的示例代码,以及Brian在ng-conf 2014关于zone.js的演讲视频: https://www.youtube.com/watch?v=3IqtmUscE_U.
当然对于一些特定的业务分析zone.js也有它很好的运用场景。如果你使用过Angular1的开发,那么也许你还能记忆犹新的想起:使用第三方事件或者ajax却忘记$scope.$apply的场景吧。在Angular1中如果在非Angular的上下文改变数据Model,Angular是无法预知的,因此也不会触发界面的更新。所以我们不得不显示的调用$scope.$apply或者$timeout来触发界面的更新。Angular框架为了更多的获知变化的事件,不得不为封装了一整套框架内置的服务和指令,如ngClick、ngChange、$http,$timeout等,这也增加了Angular1的学习成本。
也是为了解决Angular1的这一些列问题,Angular2团队引入了zone.js,放弃自定义这类服务和指令,相反而是拥抱浏览器的原生对象和方法。所以在Angular2中可以使用浏览器的任何事件了,只需要括号模板语法的标识:(eventName),等价于on-eventName;也可以直接使用浏览器的原生对象了,如setTimeout,addEventListener、promise、fetch等。
当然,zone.js也能应用于Angular1的项目之中。示例代码如下(?html,js,output):
angular.module("com.ngbook.demo", []) .controller("DemoController", ['$scope', function($scope){ zone.fork({ afterTask: function(){ var phase = $scope.$root.$$phase; if(['$apply', '$digest'].indexOf(phase) === -1) { $scope.$apply(); } } }).run(function(){ setTimeout(function(){ $scope.fromZone = "I am from zone with setTimeout!"; }, 2000); }); }]);在示例代码中,在每次Task的完成后都会尝试$scope.$apply,强制将Model数据的改变更新到UI界面。对于在Angular1中使用zone.js更多的地方应该是在Directive中,同时也可以将zone的创建过程封装为服务(工厂方法,每次返回一个全新的zone对象)。在Angular2中也有同样zone的封装,它被称为ngZone(https://github.com/angular/angular/blob/master/modules/angular2/src/core/zone/ng_zone.ts)。