ZoneJs 源码解析

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);
}

Zone原生对于window/node中的基本API都做了拦截,使得它们的执行能够被Zone接管到。下面就以最常用的setTimeout/setInterval/setImmediate 为例来看一下Zone的实现。

  • 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;
    }
  });

posted @ 2024-08-19 14:35  kongshu  阅读(13)  评论(0编辑  收藏  举报