ZoneJs源码解析

4 阅读1分钟

Zone.js 源码分析

第一节:猴子补丁

猴子补丁 = 运行时替换原生 API,在调度时和执行时插入自定义逻辑。

核心代码

const _setTimeout = window.setTimeout;

window.setTimeout = function (callback, delay, ...args) {
  // === 调度时:插入的代码 ===
  const zone = Zone.current;  // 保存当前 Zone
  
  const wrapped = function () {
    // === 执行时:插入的代码 ===
    return zone.run(callback, this, args);
  };
  
  return _setTimeout(wrapped, delay, ...args);
};

作用

  • 调度时:把 Zone.current 存进闭包
  • 执行时:用 zone.run() 恢复 Zone,再执行真正的 callback

第二节:作用域

作用域 = 让异步回调能访问「调度时」的上下文。

Zone 类

class Zone {
  constructor(parent, spec) {
    this._parent = parent;
    this._properties = spec.properties || {};
  }
  
  // 在 Zone 中执行代码
  run(callback) {
    const prev = _currentZoneFrame;
    _currentZoneFrame = { zone: this, parent: prev };
    try {
      return callback();
    } finally {
      _currentZoneFrame = prev;  // 恢复
    }
  }
  
  // 创建子 Zone,继承父 Zone 的 properties
  fork(spec) {
    return new Zone(this, {
      ...spec,
      properties: { ...this._properties, ...spec.properties }
    });
  }
  
  // 获取 Zone 上挂载的数据
  get(key) {
    if (this._properties.hasOwnProperty(key)) {
      return this._properties[key];
    }
    return this._parent ? this._parent.get(key) : undefined;
  }
}

// 全局 getter
Object.defineProperty(Zone, 'current', {
  get() { return _currentZoneFrame.zone; }
});

使用

const zone = Zone.current.fork({
  properties: { requestId: 'req-123' }
});

zone.run(() => {
  Zone.current.get('requestId');  // 同步能拿到
  setTimeout(() => {
    Zone.current.get('requestId');  // 异步也能拿到
  }, 1000);
});

第三节:判断异步是否全部完成

原理:猴子补丁给 setTimeout 前后增加代码,每次调度 +1,每次执行完 -1,当从 1→0 时表示全部完成。

核心代码

class Zone {
  constructor() {
    this._macroTaskCount = 0;
    this._onHasTask = null;
  }
  
  _changeTaskCount(delta) {
    const wasEmpty = this._macroTaskCount === 0;
    this._macroTaskCount += delta;
    const isEmpty = this._macroTaskCount === 0;
    
    if (this._onHasTask) {
      if (wasEmpty && !isEmpty) {
        this._onHasTask({ macroTask: true });   // 有任务进入
      } else if (!wasEmpty && isEmpty) {
        this._onHasTask({ macroTask: false });  // 全部完成 ✓
      }
    }
  }
}

// 猴子补丁:前后增加计数代码
window.setTimeout = function (callback, delay, ...args) {
  const zone = Zone.current;
  
  // 调度时 +1
  zone._changeTaskCount(1);
  
  const wrapped = function () {
    try {
      return zone.run(callback, this, args);
    } finally {
      // 执行完 -1(finally 确保无论成功/抛错都执行)
      zone._changeTaskCount(-1);
    }
  };
  
  return _setTimeout(wrapped, delay, ...args);
};

流程

调用 setTimeout  →  count: 01  → 触发 onHasTask({ macroTask: true })
                     ↓
               事件循环等待
                     ↓
回调执行完        →  count: 10  → 触发 onHasTask({ macroTask: false }) 全部完成

使用

const zone = Zone.current.fork({
  onHasTask(state) {
    if (!state.macroTask) {
      console.log('全部异步执行完了');
    }
  }
});

zone.run(() => {
  setTimeout(() => {}, 500);
  setTimeout(() => {}, 800);
});