从0到1实现通用微任务调度器:理解前端异步调度核心

从0到1实现通用微任务调度器:理解前端异步调度核心

微任务(Microtask)是前端异步编程的核心概念,广泛应用于Vue/react的更新、Promise回调、事件循环优化等场景。手动实现一个通用的微任务调度器,不仅能加深对事件循环的理解,还能解决「批量执行异步任务、统一控制执行时机」的实际开发需求。

本文将从「最小可用版」到「生产级优化版」,一步步拆解微任务调度器的实现思路,最终给出可直接复用的完整代码。

一、核心需求与设计思路

1. 要解决的问题

在前端开发中,频繁触发的异步任务(如多次更新DOM、批量数据处理)如果直接执行,会导致多次微任务创建,增加性能开销。我们需要:

  • 批量管理任务,去重且统一在微任务队列执行;

  • 支持任务执行前/后的回调(如初始化/清理逻辑);

  • 外部可通过await等待所有任务执行完成;

  • 兼容浏览器/Node.js环境(处理queueMicrotask API兼容性)。

2. 核心设计原则

  • 去重执行 :用Set存储任务,避免重复添加;

  • 防重复调度:标记是否已将执行逻辑放入微任务队列,避免多次触发;

  • 状态隔离 :用私有属性封装内部状态(如isFlushing),对外暴露可控的API;

  • 兼容降级 :无queueMicrotask时,用Promise.resolve().then()兜底。

二、Step 1:实现最小可用版调度器

先搭建核心骨架,完成「添加任务、批量执行」的基础能力,暂不处理回调、异常、兼容等细节。

1. 定义基础类型

先明确核心类型,让代码语义更清晰:

typescript 复制代码
// 待执行的任务类型(无参无返回值函数)
export type TaskToQueue = () => void;

// 取消回调的函数类型
export type Unsubscribe = () => void;

2. 核心类实现(最小版)

typescript 复制代码
/**
 * 最小可用版微任务调度器
 * 核心能力:添加任务、批量在微任务执行、防重复调度
 */
export class MicroScheduler {
  // 任务队列:Set自动去重
  private readonly taskQueue = new Set<TaskToQueue>();
  // 标记是否已调度微任务执行(防重复)
  private isFlushScheduled = false;
  // 标记是否正在执行任务(避免嵌套执行)
  private isFlushing = false;

  /**
   * 添加任务到队列,并触发调度
   */
  enqueue(task: TaskToQueue): void {
    this.taskQueue.add(task);
    this.scheduleFlush();
  }

  /**
   * 调度任务执行到微任务队列(核心:防重复调度)
   */
  private scheduleFlush(): void {
    // 已调度/正在执行,直接返回
    if (this.isFlushScheduled || this.isFlushing) return;

    this.isFlushScheduled = true;
    // 核心:将执行逻辑放入微任务队列
    queueMicrotask(() => {
      this.isFlushScheduled = false;
      this.executeTasks();
    });
  }

  /**
   * 批量执行所有任务
   */
  private executeTasks(): void {
    if (this.isFlushing) return;
    this.isFlushing = true;

    // 遍历执行所有任务,执行后从队列移除
    this.taskQueue.forEach((task) => {
      task();
      this.taskQueue.delete(task);
    });

    this.isFlushing = false;
  }
}

3. 测试最小版功能

typescript 复制代码
// 实例化调度器
const scheduler = new MicroScheduler();

// 添加2个任务(故意重复添加,测试去重)
const task1 = () => console.log('执行任务1');
scheduler.enqueue(task1);
scheduler.enqueue(task1); // 重复添加,Set会自动去重
scheduler.enqueue(() => console.log('执行任务2'));

// 输出结果(微任务阶段执行):
// 执行任务1
// 执行任务2

关键逻辑说明

  • scheduleFlush:核心防重复逻辑,确保多次调用enqueue仅触发一次微任务;

  • isFlushing:避免执行任务时嵌套调用executeTasks(如任务中再次enqueue);

  • Set存储任务:天然解决重复添加问题,比数组更高效。

三、Step 2:扩展核心能力

在最小版基础上,添加「同步执行、前后回调、外部等待」能力,逐步完善功能。

1. 添加同步执行方法flushSync

支持手动同步执行任务(不等待微任务):

typescript 复制代码
/**
 * 同步触发任务执行(立即执行,不等待微任务)
 */
flushSync(): void {
  this.executeTasks();
}

2. 添加执行前/后回调

支持注册/取消「执行前/后」的回调,满足初始化、清理等场景:

typescript 复制代码
// 新增:执行前/后回调队列
private readonly beforeFlushCallbacks = new Set<() => void>();
private readonly afterFlushCallbacks = new Set<() => void>();

/**
 * 注册「任务执行前」的回调
 * @returns 取消回调的函数
 */
onBeforeFlush(callback: () => void): Unsubscribe {
  this.beforeFlushCallbacks.add(callback);
  return () => this.beforeFlushCallbacks.delete(callback);
}

/**
 * 注册「任务执行后」的回调
 */
onAfterFlush(callback: () => void): Unsubscribe {
  this.afterFlushCallbacks.add(callback);
  return () => this.afterFlushCallbacks.delete(callback);
}

// 改造executeTasks:执行前后回调
private executeTasks(): void {
  if (this.isFlushing) return;
  this.isFlushing = true;

  // 1. 执行前置回调
  this.beforeFlushCallbacks.forEach((cb) => cb());

  // 2. 执行任务
  this.taskQueue.forEach((task) => {
    task();
    this.taskQueue.delete(task);
  });

  // 3. 执行后置回调
  this.afterFlushCallbacks.forEach((cb) => cb());

  this.isFlushing = false;
}

3. 支持外部await等待执行完成

添加tick属性,基于固定的Promise实现外部等待(核心:用初始化的Promise做「时间锚点」,避免每次创建新Promise导致等待失效):

typescript 复制代码
// 新增:固定的微任务Promise(仅初始化一次)
private readonly microtaskPromise = Promise.resolve<void>(undefined);

// 公开只读属性:外部可await
readonly tick = this.microtaskPromise;

测试扩展能力

typescript 复制代码
const scheduler = new MicroScheduler();

// 注册前后回调
const unsubBefore = scheduler.onBeforeFlush(() => console.log('任务执行前'));
scheduler.onAfterFlush(() => console.log('任务执行后'));

// 添加任务
scheduler.enqueue(() => console.log('执行任务'));

// 外部等待任务执行完成
async function test() {
  await scheduler.tick;
  console.log('所有任务执行完毕');
  unsubBefore(); // 取消前置回调
}

test();

// 输出顺序:
// 任务执行前
// 执行任务
// 任务执行后
// 所有任务执行完毕

四、Step 3:兼容与容错优化(生产级)

完成核心功能后,添加「环境兼容、参数校验、异常捕获」,让代码更健壮。

1. 兼容queueMicrotask API

Node.js和部分老浏览器可能未实现queueMicrotask,需降级到Promise

typescript 复制代码
// 声明全局queueMicrotask类型(兼容TS)
declare const queueMicrotask: ((callback: () => void) => void) | undefined;

// 新增:兼容后的微任务执行函数
private readonly queueMicrotaskFn: (callback: () => void) => void;

// 改造构造函数:初始化兼容函数
constructor() {
  this.queueMicrotaskFn =
    typeof queueMicrotask !== 'undefined'
      ? queueMicrotask
      : (cb) => this.microtaskPromise.then(cb);
}

// 改造scheduleFlush:使用兼容后的函数
private scheduleFlush(): void {
  if (this.isFlushScheduled || this.isFlushing) return;
  this.isFlushScheduled = true;
  this.queueMicrotaskFn(() => {
    this.isFlushScheduled = false;
    this.executeTasks();
  });
}

2. 添加参数校验

避免传入非函数类型的任务/回调:

typescript 复制代码
// 改造enqueue
enqueue(task: TaskToQueue): void {
  if (typeof task !== 'function') {
    throw new TypeError('enqueue 必须传入函数类型的任务');
  }
  this.taskQueue.add(task);
  this.scheduleFlush();
}

// 封装回调注册逻辑,添加校验
private registerCallback(callbacks: Set<() => void>, callback: () => void): Unsubscribe {
  if (typeof callback !== 'function') {
    throw new TypeError('回调必须是函数类型');
  }
  callbacks.add(callback);
  return () => callbacks.delete(callback);
}

// 改造onBeforeFlush/onAfterFlush,复用校验逻辑
onBeforeFlush(callback: () => void): Unsubscribe {
  return this.registerCallback(this.beforeFlushCallbacks, callback);
}

onAfterFlush(callback: () => void): Unsubscribe {
  return this.registerCallback(this.afterFlushCallbacks, callback);
}

3. 异常捕获(容错)

单个任务/回调执行失败,不影响整体流程:

typescript 复制代码
private executeTasks(): void {
  if (this.isFlushing) return;
  this.isFlushing = true;

  try {
    // 1. 执行前置回调(捕获单个回调异常)
    const beforeCallbacks = Array.from(this.beforeFlushCallbacks);
    beforeCallbacks.forEach((cb) => {
      try {
        cb();
      } catch (e) {
        console.error('执行前置回调失败:', e);
      }
    });

    // 2. 执行任务(复制队列,避免迭代时修改)
    const tasks = Array.from(this.taskQueue);
    tasks.forEach((task) => {
      try {
        task();
      } catch (e) {
        console.error('执行任务失败:', e);
      }
      this.taskQueue.delete(task);
    });
  } finally {
    // 无论是否出错,都执行后置回调+重置状态
    const afterCallbacks = Array.from(this.afterFlushCallbacks);
    afterCallbacks.forEach((cb) => {
      try {
        cb();
      } catch (e) {
        console.error('执行后置回调失败:', e);
      }
    });
    this.isFlushing = false;
  }
}

五、完整生产级代码

整合所有优化,最终的完整代码如下(可直接复制复用):

typescript 复制代码
// 类型命名更精准,贴合场景
export type TaskToQueue = () => void;
// 取消回调的函数类型(语义更清晰)
export type Unsubscribe = () => void;

// 声明全局 queueMicrotask 类型(兼容 Node.js 和浏览器环境)
declare const queueMicrotask: ((callback: () => void) => void) | undefined;

/**
 * 微任务调度器类
 * 核心能力:批量管理任务,统一在微任务队列执行,支持执行前后回调
 */
export class MicroScheduler {
  // ========== 私有属性(封装,外部不可访问) ==========
  // 任务队列:Set 自动去重,避免重复执行
  private readonly taskQueue = new Set<TaskToQueue>();
  // 执行前回调队列
  private readonly beforeFlushCallbacks = new Set<() => void>();
  // 执行后回调队列
  private readonly afterFlushCallbacks = new Set<() => void>();
  // 标记是否正在执行任务
  private isFlushing = false;
  // 标记是否已将 flush 调度到微任务队列
  private isFlushScheduled = false;
  // 固定的微任务 Promise,用于外部 await 等待执行完成(仅初始化一次)
  private readonly microtaskPromise = Promise.resolve<void>(undefined);
  // 兼容后的微任务执行函数(适配无 queueMicrotask 的环境)
  private readonly queueMicrotaskFn: (callback: () => void) => void;

  // ========== 公开只读属性(外部可访问,不可修改) ==========
  /**
   * 用于等待微任务执行完成的 Promise
   * 外部可通过 await scheduler.tick 等待任务执行完毕
   */
  readonly tick = this.microtaskPromise;

  constructor() {
    // 兼容 queueMicrotask API(前端工程化必备)
    this.queueMicrotaskFn =
      typeof queueMicrotask !== 'undefined'
        ? queueMicrotask
        : (cb) => this.microtaskPromise.then(cb);
  }

  // ========== 公开方法(外部可调用) ==========
  /**
   * 添加任务到调度队列,并触发异步执行调度
   * @param task 待执行的微任务(无参无返回值函数)
   */
  enqueue(task: TaskToQueue): void {
    if (typeof task !== 'function') {
      throw new TypeError('enqueue 必须传入函数类型的任务');
    }
    this.taskQueue.add(task);
    this.scheduleFlush();
  }

  /**
   * 异步触发任务批量执行(放入微任务队列)
   * 多次调用仅会触发一次微任务,避免重复执行
   */
  flush(): void {
    this.scheduleFlush();
  }

  /**
   * 同步触发任务批量执行(立即执行,不等待微任务)
   */
  flushSync(): void {
    this.executeTasks();
  }

  /**
   * 注册「任务执行前」的回调
   * @param callback 执行前的回调函数
   * @returns 取消回调的函数(调用后不再触发该回调)
   */
  onBeforeFlush(callback: () => void): Unsubscribe {
    return this.registerCallback(this.beforeFlushCallbacks, callback);
  }

  /**
   * 注册「任务执行后」的回调
   * @param callback 执行后的回调函数
   * @returns 取消回调的函数(调用后不再触发该回调)
   */
  onAfterFlush(callback: () => void): Unsubscribe {
    return this.registerCallback(this.afterFlushCallbacks, callback);
  }

  // ========== 私有方法(内部逻辑,对外隐藏) ==========
  /**
   * 调度 flush 到微任务队列(核心:防重复调度)
   */
  private scheduleFlush(): void {
    if (this.isFlushScheduled || this.isFlushing) return;

    this.isFlushScheduled = true;
    this.queueMicrotaskFn(() => {
      this.isFlushScheduled = false;
      this.executeTasks();
    });
  }

  /**
   * 核心执行逻辑:执行前后回调 + 批量执行任务
   */
  private executeTasks(): void {
    if (this.isFlushing) return;
    this.isFlushing = true;

    try {
      // 1. 执行前置回调(复制后遍历,避免迭代时修改)
      const beforeCallbacks = Array.from(this.beforeFlushCallbacks);
      beforeCallbacks.forEach((cb) => {
        try {
          cb();
        } catch (e) {
          console.error('调度器执行前置回调失败:', e);
        }
      });

      // 2. 执行所有任务(复制后遍历,避免迭代时修改队列)
      const tasks = Array.from(this.taskQueue);
      tasks.forEach((task) => {
        try {
          task();
        } catch (e) {
          // 捕获单个任务异常,不影响其他任务执行(前端容错最佳实践)
          console.error('调度器执行任务失败:', e);
        }
        this.taskQueue.delete(task);
      });
    } finally {
      // 无论是否出错,都重置状态 + 执行后置回调
      const afterCallbacks = Array.from(this.afterFlushCallbacks);
      afterCallbacks.forEach((cb) => {
        try {
          cb();
        } catch (e) {
          console.error('调度器执行后置回调失败:', e);
        }
      });
      this.isFlushing = false;
    }
  }

  /**
   * 注册回调并返回取消函数(复用逻辑)
   * @param callbacks 回调队列
   * @param callback 要注册的回调函数
   * @returns 取消回调的函数
   */
  private registerCallback(callbacks: Set<() => void>, callback: () => void): Unsubscribe {
    if (typeof callback !== 'function') {
      throw new TypeError('回调必须是函数类型');
    }
    callbacks.add(callback);
    return () => callbacks.delete(callback);
  }
}

export default MicroScheduler;

六、使用场景与扩展方向

1. 典型使用场景

  • 批量DOM更新:多次修改DOM的任务加入调度器,避免频繁重排重绘;

  • 状态管理库:如Vue的响应式更新,批量执行依赖收集后的回调;

  • 异步数据处理:批量处理接口返回的数据,统一执行后续逻辑。

2. 扩展方向

  • 添加「任务优先级」:区分高/低优先级任务,按优先级执行;

  • 支持「任务超时」:设置任务执行超时时间,超时后抛出异常;

  • 增加「任务统计」:记录任务执行数量、耗时,便于性能监控。

七、总结

实现微任务调度器的核心是「理解事件循环+控制执行时机」:

  1. Set管理任务,解决重复执行问题;

  2. isFlushScheduled/isFlushing控制调度时机,避免重复执行;

  3. 用固定的Promise做时间锚点,支持外部可靠等待;

  4. 兼容环境+异常捕获,保证生产环境可用。

从「最小可用版」到「生产级优化版」的实现过程,不仅能掌握微任务的核心逻辑,还能学习到前端工程化的最佳实践(封装、兼容、容错)。希望本文能帮助你理解异步调度的本质,也能在实际项目中落地这个通用的微任务调度器。

相关推荐
hashiqimiya2 小时前
JavaScript的object的使用和监控打印日志
前端·javascript·vue.js
C_心欲无痕3 小时前
vue3 - useId生成唯一标识符
前端·javascript·vue.js·vue3
KoalaShane3 小时前
El-slider 增加鼠标滚动滑块事件
开发语言·前端·javascript
栀秋6663 小时前
Tailwind CSS:用“类名编程”重构你的前端开发体验
前端·css
C_心欲无痕3 小时前
vue3 - watchSyncEffect同步执行的响应式副作用
开发语言·前端·javascript·vue.js·vue3
用泥种荷花3 小时前
【前端学习AI】FewShotPromptTemplate
前端
小魔女千千鱼3 小时前
在 Vue 中,this 的行为在箭头函数和普通函数中是不同的
前端·javascript·vue.js
霍理迪4 小时前
CSS盒模型布局规则
前端·javascript·css
千寻girling4 小时前
面试官: “ 说一下 JS 中什么是事件循环 ? ”
前端·javascript