ZoneJs 源码解析
ZoneJs是什么,它能干什么,它是怎么做到的?
- Zone是为js的执行提供了一个共享的数据上下文。为js函数执行维护了一个逻辑上的调用栈。
- 同时提供了对于函数执行方法的拦截,在函数执行前后,添加一些通用的逻辑(例如日志,异常处理)。
- 统一的任务模型,提供对于宏任务/微任务/事件任务的调度执行。
- Zone提供领域的克隆操作,提供基于父领域的继承复写。
从源码角度来分析Zone的具体实现。以下是依据对于源码的理解,手撸的一个简易的版本,关键的变量方法保持与源码一致。
- ZoneJs中的基本数据类型。
- ZoneTask,是对于Task的封装,记录了Zone,调度,等信息
- ZoneDelegate 它里面的方法是Zone里面方法的镜像,Zone里面的方法负责干活的都委托给了这个实例
- Zone, 它里面包含的逻辑主要是Zone状态的维护,Task状态的维护,具体的干活都委托给了上面这两个。它是一个容器。
- ZoneSpec,这个是定义拦截的地方,如果想对于Zone中方法的执行进行拦截,可以在这个里面定义
// Zone中的Task 分为
class ZoneTask{
type:'MicroTask'|'MacroTask'|'EventTask';
// Task的Zone在创建的时候就决定了
_zone:Zone;
// 这个是Task的逻辑,所以的逻辑就是这个
callback:Function;
// 这个对象是对于callback或者Task本身的一些描述,例如是不是周期性的任务,是否需要延迟,也可以存放SetTimeout返回的句柄
data:TaskData;
// 这个是这个任务的调度函数,对于微任务,是由Zone负责调度,对于MacroTask|EventTask,需要由客户端代码提供调度的方法。
scheduleFn:((task:Task)=>void);
// 这个是用来取消MacroTask|EventTask,客户端代码需要提供。
cancelFn:(task:Task)=>void;
// 这个方法其实是给客户端代码直接调用的。例如宏任务,EventTask。
public invoke: Function;
constructor(
type: T, source: string, callback: Function, options: TaskData|undefined,
scheduleFn: ((task: Task) => void)|undefined, cancelFn: ((task: Task) => void)|undefined) {
this.type = type;
this.source = source;
this.data = options;
this.scheduleFn = scheduleFn;
this.cancelFn = cancelFn;
if (!callback) {
throw new Error('callback is not defined');
}
this.callback = callback;
const self = this;
// TODO: @JiaLiPassion options should have interface
if (type === eventTask && options && (options as any).useG) {
this.invoke = ZoneTask.invokeTask;
} else {
this.invoke = function() {
return ZoneTask.invokeTask.call(global, self, this, <any>arguments);
};
}
}
// 这个静态方法会被放到实例方法里面,Task的执行逻辑就在这里
static invokeTask(task:Task,target:any,args:any[]):any{
//全局的一个计数器,标记运行的task数量
_numberOfNestedTaskFrames++;
try {
task.runCount++;
// 将task放置到它的zone中去运行
return task.zone.runTask(task, target, args);
} finally {
// 这个是为了让MicroTask对立里面的任务全部执行完,因为当前没有其他的任务了,所以这个时候应该去将MicroTask队列里面的任务全部执行。
if (_numberOfNestedTaskFrames == 1) {
drainMicroTaskQueue();
}
_numberOfNestedTaskFrames--;
}
}
}
class _ZoneDelegate{
// 这个标记当前的委托是为哪个Zone服务的
public zone:Zone;
// 第一个参数是为哪个Zone服务的,第二个参数是父级Zone的委托,如果当前的Zone没有提供对操作的拦截,就使用父级的。
// 第三个参数是当前的zone对于操作的拦截逻辑
constructor(zone:Zone,parentDelegate:_ZoneDelegate|null,zoneSpec:ZoneSpec){
}
//基于当前的Zone复刻一个新的Zone出来,这个函数里面会先检查有没有复写的拦截操作,如果没有则直接实例化一个Zone实例。
fork(targetZone:Zone,zoneSpec:ZoneSpec){
return this._forkZS?this._forkZS.onFork(this._forkDlgt,this.zone,targetZone,zoneSpec):
new Zone(targetZone,zoneSpec)
}
// 这个是基于 Zone 里面的Run方法的封装,套路是一样的,本质上就是一个apply方法的调用
invoke(targetZone:Zone,callback:Function,applyThis:any,applyArgs:any[],source?:string):any{
return this._invokeZS?this._invokeZS(targetZone,callback,applyThis,applyArgs,source):
callback.apply(applyThis,applyArgs);
}
// 这个任务调度的逻辑,对于微任务则由Zone调度,插入到任务队列里面,对于其他任务应该由客户端代码提供调度逻辑
scheduleTask(targetZone:Zone,task:Task):Task{
let returnTask:Task=task;
if(this._scheduleTaskZS){
returnTask=this._scheduleTaskZS.onScheduleTask(this._scheduleTaskDglt,this._scheduleTaskCurrentZone,targetZone,task);
}else{
if(task.scheduleFn){
task.scheduleFn(task);
}else if(task.type==='MicroTask'){
scheduleMicroTask(task);
}else{
throw new Error('Task is missing scheduleFn.');
}
}
}
// 这个是执行任务的逻辑,就是直接调用apply
invokeTask(targetZone:Zone,task:Task,applyThis?:any,applyArgs:any){
return this._invokeTaskZS?this._invokeTaskZS.onInvokeTask(targetZone,task,applyThis,applyArgs):
task.callback.apply(applyThis,applyArgs);
}
}
class Zone{
//这个就是共享的数据上下文
_properties:{[key:string]:any};
//每个子Zone保留了对于父级的引用。
_parent:Zone|null;
// 每个zone得有一个名字,这样可以方便日志输出,知道异常是从哪个zone里面发出来的。
_name:string;
//Zone中对于任务的执行等各种方法都是委托到这个实例中的,Zone本身其实就是个壳,干活的是这个实例。
// 为什么要这样的,这个其实是为了继承用的,当某个方法,子Zone没有提供复写,那默认会去使用父级的定制逻辑,直到根部。如果直接使用父级Zone的方法,这样就变成了父级Zone的执行了。
// 所以采用了这个方法进行变通,在当前的Zone中,但是使用了父级Zone里面的拦截逻辑。
_zoneDelegate:_ZoneDelegate;
// 这个是由当前的Zone创建一个子Zone
public fork(zoneSpec:ZoneSpec):Zone{
if (!zoneSpec) throw new Error('ZoneSpec required!');
return this._zoneDelegate.fork(this, zoneSpec);
}
// 这个是执行Task的逻辑,除了切换当前的Zone,具体的执行逻辑委托给了Delegate.
public runTask(task:Task,applyThis?:any,applyArgs:any):any{
const previousTask = _currentTask;
_currentTask=task;
_currentZoneFrame={parent:_currentZoneFrame,zone:this};
try{
this._zoneDelegate.invokeTask(this,task,applyThis,applyArgs);
}catch(err){
if(this._zoneDelegate.handleError(this,err)){
throw err;
}
}finally{
_currentTask=previousTask;
_currentZoneFrame=_currentZoneFrame.parent;
}
}
// 这个调度任务的方法,负责维护任务的状态变化,Zone的更新。具体的调度逻辑由委托负责。
scheduleTask(task:Task){
// 更新Task的状态
(task as any as ZoneTask<any>)._transitionTo(scheduling, notScheduled);
(task as any as ZoneTask<any>)._zone = this;
// 交由委托负责具体的调度逻辑
task = this._zoneDelegate.scheduleTask(this, task) as T;
// 更新Task的状态
if ((task as any as ZoneTask<any>).state == scheduling) {
(task as any as ZoneTask<any>)._transitionTo(scheduled, scheduling);
}
}
}
_microTaskQueue:Task[]=[];
_numberOfNestedTaskFrames:number=0;
_isDrainingMicrotaskQueue:false=false;
// 这个就是原生的调度微任务的方法,这个是一个内部方法,不会对外使用
function nativeScheduleMicroTask(callback:Function){
Promise.resolve(0).then(callback);
}
//这个是执行微任务的主线程,它会在每次插入微任务队列的时候开启,或者直接调用Task.invoke方法的时候也会开启。
function drainMicroTaskQueue(){
if(!_isDrainingMicrotaskQueue){
_isDrainingMicrotaskQueue=true;
// 一气呵成将所有的微任务都执行完,每个周期就是执行所有的任务。过程中产生的新的微任务会放到下一个循环中
while(_microTaskQueue.length>0){
//一个循环清空一次
const queue = _microTaskQueue;
//这个期间产生的新的任务,放到下一个循环里
_microTaskQueue=[];
for(let i=0;i<queue.length;i++){
const task=queue[i];
// 每个任务在zone里面去执行
try{
task.zone.runTask(task);
}catch(err){
_api.onUnhandledError(error as Error);
}
}
}
_isDrainingMicrotaskQueue=false;
}
}
// 这个就是Zone内部调用Task的方法。
function scheduleMicroTask(task:Task){
if(_numberOfNestedTaskFrames===0&&_microTaskQueue.length===0){
nativeScheduleMicroTask(drainMicroTaskQueue);
}
task&&_microTaskQueue.push(task);
}
- 在
timer.ts
中提供了patchTimer()
方法来对这些方法进行了拦截。拦截的方式非常暴力,直接将window.setTimeout
给换掉了。下面是对于这些实现的一个简化版
let nativeSettimeout:Funciton;
//调用原生的setTimeout来执行,原来的函数放到了Zone里面去执行,通过调用task.invoke方法。
function customSchedule(task){
task.data.args[0]=function(){
return task.invoke.apply(this,arguments);
}
task.data.handleId = nativeSettimeout.apply(window,data.args);
}
// 这个是将原来的setTimeout转换成Zone里面的setTimeout.
function patchFn(original:Fnction){
return function(args:any){
// args就是setTimeout(function,xxx); 这个args就是所有的参数,第一个参数就是处理函数
const callback=args[0];
const options={
isPeriodic:false,
args,
}
// 创建宏任务,它的调度是由我们自定义的,本质上是委托到原生的setTimeout
const task = Zone.current.scheduleMacroTask(source,callback,options,customSchedule,customCancel);
return task;
}
}
// 这个是个通用方法,就是将window.setTimeout=换成我们自定义的方法。
function patchMethod(target:any,name:string,patchFn:(original:Function)=>Function){
const delegate = target[name]
const patchedDelegate=patchFn(delegate);
target[name]=function(){
return patchDelegate(arguments);
}
return delegate;
}
ngZone 它在Zonejs的基础上创建了一个子zone,并对方法的执行进行了拦截。拦截的目的是为了维护当前hasPendingMicroTasks,hasPendingMacroTasks,isStable
,及时发布相应的事件,onMicrotaskEmpty,onStable
.对异常进行捕获处理。例如,它在Invoke前后,添加了onEnter,onLeave
方法来计算状态
zone._inner = zone._inner.fork({
name: 'angular',
properties: <any>{'isAngularZone': true},
// 拦截InvokeTask的方法
onInvokeTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
applyArgs: any): any => {
try {
// 任务计数
onEnter(zone);
return delegate.invokeTask(target, task, applyThis, applyArgs);
} finally {
// 变更检查聚合
if ((zone.shouldCoalesceEventChangeDetection && task.type === 'eventTask') ||
zone.shouldCoalesceRunChangeDetection) {
delayChangeDetectionForEventsDelegate();
}
// 任务计算
onLeave(zone);
}
},
onInvoke:
(delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
applyArgs?: any[], source?: string): any => {
try {
onEnter(zone);
return delegate.invoke(target, callback, applyThis, applyArgs, source);
} finally {
if (zone.shouldCoalesceRunChangeDetection) {
delayChangeDetectionForEventsDelegate();
}
onLeave(zone);
}
},
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;
updateMicroTaskStatus(zone);
checkStable(zone);
} else if (hasTaskState.change == 'macroTask') {
zone.hasPendingMacrotasks = hasTaskState.macroTask;
}
}
},
onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any): boolean => {
delegate.handleError(target, error);
zone.runOutsideAngular(() => zone.onError.emit(error));
return false;
}
});