普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月29日首页

滑动窗口详解:原理+分类+场景+模板+例题(视频贼清晰)

作者 颜酱
2025年12月29日 18:28

滑动窗口详解:原理+分类+场景+模板+例题

📺 推荐视频滑动窗口算法详解 - 视频解释非常清晰,建议先看视频再阅读本文!

在算法面试中,子串、子数组相关的问题频繁出现,暴力枚举往往因 O(n²) 时间复杂度超时。而滑动窗口算法,凭借其 O(n) 的高效性能,成为解决这类问题的"神兵利器"。本文将从原理本质出发,梳理滑动窗口的分类、适用场景,提炼通用模板,并结合经典例题实战拆解,帮你彻底掌握这一核心算法。

一、滑动窗口核心原理:用单调性压缩遍历维度

滑动窗口的本质,是利用区间的单调性,将原本需要嵌套遍历(O(n²))的连续区间问题,转化为单轮双指针遍历(O(n))。其核心逻辑基于对“窗口状态”的精准把控,通过两个指针(left 左边界、right 右边界)的协同移动,跳过无效区间(剪枝),实现高效枚举。

1.1 先搞懂:暴力枚举的痛点

以“无重复字符的最长子串”为例,暴力思路是枚举所有子串的起点 i 和终点 j(i≤j),检查子串 s[i..j] 是否无重复,最终记录最长长度。这种方式需要遍历所有 i、j 组合,时间复杂度 O(n²),且存在大量无效计算:比如当 s[0..3] 存在重复时,s[0..4]、s[0..5] 等包含该区间的子串必然也重复,无需再检查。

1.2 滑动窗口的核心洞察:区间单调性

滑动窗口能优化的关键,是抓住了「窗口状态的单调性」—— 窗口的状态(如是否含重复、和/积是否满足条件)会随窗口的扩展/缩小呈现单向变化,具体可总结为两条核心规律:

  • 规律1(坏状态的包含性):若窗口 [left, right] 处于“坏状态”(如含重复字符、和≥target、积≥K),则所有包含该窗口的更大窗口 [left, right+1]、[left, right+2]... 必然也是“坏状态”。此时无需继续扩展 right,应移动 left 缩小窗口,跳过无效区间。

  • 规律2(好状态的被包含性):若窗口 [left, right] 处于“好状态”(如无重复、和<target、积<K),则所有被该窗口包含的更小窗口 [left+1, right]、[left+2, right]... 必然也是“好状态”。此时无需缩小窗口,应继续扩展 right 寻找更优解。

1.3 一句话总结原理

滑动窗口通过 right 指针“扩窗口”探索新的区间,通过 left 指针“缩窗口”剔除无效区间,每个元素最多被加入窗口(right 移动)和移出窗口(left 移动)各一次,最终以 O(n) 时间完成所有有效区间的枚举。

二、滑动窗口的分类:按目标场景划分

滑动窗口的核心逻辑一致,但根据问题目标(求最长、求最短、求计数)的不同,缩窗口的条件和更新答案的时机会有差异。按目标可分为三大类,覆盖绝大多数经典场景:

分类 核心目标 缩窗口条件 更新答案时机 典型问题
类型1:求最长/最大区间 找到满足“好状态”的最长连续区间 窗口进入“坏状态”时,缩 left 至回到“好状态” 缩窗口完成后,每次扩展 right 后更新 无重复字符的最长子串、最长重复子数组
类型2:求最短/最小区间 找到满足“好状态”的最短连续区间 窗口进入“好状态”时,缩 left 至回到“坏状态”(尽可能缩小窗口) 缩窗口过程中,每次缩小 left 后更新 长度最小的子数组、最小覆盖子串
类型3:求计数/统计区间 统计所有满足“好状态”的连续区间个数 窗口进入“坏状态”时,缩 left 至回到“好状态” 缩窗口完成后,累加当前 right 对应的有效区间数(right-left+1) 乘积小于 K 的子数组、找到字符串中所有字母异位词

三、适用场景:3个核心判断标准

并非所有子串/子数组问题都能用滑动窗口,需满足以下 3 个核心条件,缺一不可:

  1. 问题对象是连续区间:滑动窗口仅适用于“连续子串”或“连续子数组”问题,非连续区间(如子序列)不适用。

  2. 窗口状态具有单调性:需满足前文提到的两条规律之一,即扩展/缩小窗口时,状态变化是单向的。反例:“找和为 target 的子数组(含负数值)”,窗口 [left, right] 和为 target 时,扩展 right 可能因负数导致和变小,打破单调性,无法用滑动窗口。

  3. 状态可快速更新:加入 right 元素或移出 left 元素时,窗口的状态(如和、积、字符频率)能在 O(1) 时间内更新,无需重新计算整个窗口状态。

四、通用模板:3类场景统一框架

基于上述分类,提炼出通用模板,只需根据目标调整「缩窗口条件」和「更新答案时机」即可。模板核心步骤:初始化变量 → 扩窗口 → 缩窗口 → 更新答案。

4.0 快速参考表

类型 初始 ans 缩窗口条件 更新答案时机 关键代码
类型1:求最长 0 进入坏状态 缩窗口后,每次扩展 right 后 ans = Math.max(ans, right - left + 1)
类型2:求最短 Infinity 进入好状态 缩窗口过程中 ans = Math.min(ans, right - left + 1)
类型3:求计数 0 进入坏状态 缩窗口后 ans += right - left + 1

4.1 通用模板(TypeScript/JavaScript)

function slidingWindowTemplate<T>(data: T[], targetParam: any): number {
  // 1. 初始化变量
  let left = 0; // 左窗口边界
  let ans = 初始值; // 答案变量(最长→0,最短→Infinity,计数→0)
  let status = 初始状态; // 如对象(字符频率)、sum=0、prod=1

  // 2. 扩窗口:right 遍历所有元素
  for (let right = 0; right < data.length; right++) {
    const rightVal = data[right];
    // 加入右元素,更新状态
    // status.update(rightVal); // 根据具体类型更新

    // 3. 缩窗口:根据目标和当前状态判断是否缩左
    while (缩窗口条件) {
      // 核心差异点:不同类型场景条件不同
      const leftVal = data[left];
      // 移出左元素,更新状态
      // status.remove(leftVal); // 根据具体类型更新
      left++; // 缩小窗口
    }

    // 4. 更新答案:根据类型调整时机
    // 答案更新逻辑
    // 核心差异点:不同类型场景时机不同
  }

  // 5. 处理边界情况(如无满足条件的窗口)
  return 处理后的 ans;
}

4.2 分类型模板细化

类型1:求最长/最大区间

function maxLengthTemplate<T>(data: T[], param: any): number {
  let left = 0;
  let ans = 0; // 最长初始为0
  const status: Record<string, number> = {}; // 对象:记录字符频率

  for (let right = 0; right < data.length; right++) {
    const rightVal = data[right];
    // 更新状态
    status[rightVal as string] = status[rightVal as string] ? status[rightVal as string] + 1 : 1;

    // 缩窗口条件:进入坏状态
    while (坏状态判断) {
      // 如 status[rightVal] > 1(重复字符)
      const leftVal = data[left];
      status[leftVal as string]--;
      left++;
    }

    // 更新答案:缩窗口后,当前窗口是有效最长窗口
    ans = Math.max(ans, right - left + 1);
  }

  return ans;
}

类型2:求最短/最小区间

function minLengthTemplate(data: number[], param: any): number {
  let left = 0;
  let ans = Infinity; // 最短初始为无穷大
  let status = 0; // 如 sumWindow = 0

  for (let right = 0; right < data.length; right++) {
    const rightVal = data[right];
    status += rightVal; // 更新状态

    // 缩窗口条件:进入好状态(尽可能缩小窗口)
    while (好状态判断) {
      // 如 status >= target(和≥目标)
      // 缩窗口时更新答案
      ans = Math.min(ans, right - left + 1);
      const leftVal = data[left];
      status -= leftVal;
      left++;
    }
  }

  // 处理边界:无满足条件的窗口返回0
  return ans !== Infinity ? ans : 0;
}

类型3:求计数/统计区间

function countTemplate(data: number[], param: any): number {
  let left = 0;
  let ans = 0; // 计数初始为0
  let status = 1; // 如 prod = 1

  for (let right = 0; right < data.length; right++) {
    const rightVal = data[right];
    status *= rightVal; // 更新状态

    // 缩窗口条件:进入坏状态
    while (坏状态判断) {
      // 如 status >= K(乘积≥K)
      const leftVal = data[left];
      status /= leftVal;
      left++;
    }

    // 更新答案:当前right对应的有效区间数 = right-left+1
    ans += right - left + 1;
  }

  return ans;
}

五、经典例题实战:逐行拆解

结合模板,拆解 3 类场景的经典例题,帮你理解如何将模板落地到具体问题。

例题1:无重复字符的最长子串(类型1:求最长)

题目描述

给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。

解题思路

  • 窗口状态(坏):窗口内存在重复字符;
  • 状态统计:用对象记录窗口内字符的出现次数;
  • 缩窗口条件:当前加入的字符出现次数>1(进入坏状态);
  • 更新答案:缩窗口完成后,计算当前窗口长度,更新最大值。

代码实现

function lengthOfLongestSubstring(s: string): number {
  let left = 0;
  let ans = 0; // 最长子串长度初始为0
  const window: Record<string, number> = {}; // 对象:记录窗口内字符出现次数

  for (let right = 0; right < s.length; right++) {
    const rightChar = s[right];
    // 加入右字符,更新状态
    window[rightChar] = window[rightChar] ? window[rightChar] + 1 : 1;

    // 缩窗口:当当前字符出现次数>1(坏状态),缩左直到无重复
    while (window[rightChar] > 1) {
      const leftChar = s[left];
      window[leftChar]--; // 移出左字符,更新状态
      left++;
    }

    // 更新答案:当前窗口是无重复的有效窗口,计算长度
    ans = Math.max(ans, right - left + 1);
  }

  return ans;
}

复杂度分析

时间复杂度 O(n):每个字符被 right 加入、left 移出各一次;空间复杂度 O(min(m, n))):m 是字符集大小,窗口内字符数不超过 min(m, n)。

例题2:长度最小的子数组(类型2:求最短)

题目描述

给定一个含有 n 个正整数的数组和一个正整数 target,找出该数组中满足其和 ≥ target 的长度最小的 连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

解题思路

  • 窗口状态(好):窗口和≥target;
  • 状态统计:用 sumWindow 记录窗口内元素和;
  • 缩窗口条件:sumWindow≥target(进入好状态),缩左以寻找更短窗口;
  • 更新答案:缩窗口过程中,每次缩小后计算窗口长度,更新最小值。

代码实现

function minSubArrayLen(target: number, nums: number[]): number {
  let left = 0;
  let ans = Infinity; // 最短长度初始为无穷大
  let sumWindow = 0; // 窗口内元素和

  for (let right = 0; right < nums.length; right++) {
    sumWindow += nums[right]; // 加入右元素,更新和

    // 缩窗口:和≥target时,尽可能缩小窗口
    while (sumWindow >= target) {
      // 缩窗口时更新答案:当前窗口是有效最短窗口候选
      ans = Math.min(ans, right - left + 1);
      sumWindow -= nums[left]; // 移出左元素,更新和
      left++;
    }
  }

  // 处理边界:无满足条件的窗口返回0
  return ans !== Infinity ? ans : 0;
}

复杂度分析

时间复杂度 O(n):每个元素最多被遍历两次;空间复杂度 O(1):仅用常数级变量。

例题3:乘积小于 K 的子数组(类型3:求计数)

题目描述

给你一个整数数组 nums 和一个整数 k,统计并返回该数组中乘积小于 k 的连续子数组的个数。

解题思路

  • 窗口状态(坏):窗口乘积≥k;
  • 状态统计:用 prod 记录窗口内元素乘积;
  • 缩窗口条件:prod≥k(进入坏状态),缩左直到乘积<k;
  • 更新答案:缩窗口完成后,当前 right 对应的有效子数组数为 right-left+1(即 [left,right]、[left+1,right]...[right,right])。

代码实现

function numSubarrayProductLessThanK(nums: number[], k: number): number {
  // 边界条件:k≤1时,所有正整数乘积≥1,无满足条件的子数组
  if (k <= 1) {
    return 0;
  }
  let left = 0;
  let ans = 0; // 计数初始为0
  let prod = 1; // 窗口内元素乘积

  for (let right = 0; right < nums.length; right++) {
    prod *= nums[right]; // 加入右元素,更新乘积

    // 缩窗口:乘积≥k时,缩左直到乘积<k
    while (prod >= k) {
      prod /= nums[left]; // 移出左元素,更新乘积
      left++;
    }

    // 累加当前right对应的有效子数组数
    // 当窗口 [left, right] 的乘积 < k 时,以 right 结尾的所有子数组都满足条件
    // 即 [left,right]、[left+1,right]...[right,right] 共 right-left+1 个
    ans += right - left + 1;
  }

  return ans;
}

复杂度分析

时间复杂度 O(n):每个元素最多被遍历两次;空间复杂度 O(1):仅用常数级变量。

六、新手避坑指南

  1. 窗口边界统一:建议全程使用「左闭右闭」或「左闭右开」边界定义,不要混用。本文所有例题均采用「左闭右闭」,窗口长度为 right-left+1。

  2. 状态更新顺序:缩窗口时,需先更新状态(如减 sum、除 prod),再移动 left 指针,避免漏算或多算。

  3. 边界条件处理

    • 求最短时,初始 ans 设为无穷大,最后需判断是否更新过(未更新则返回 0);

    • 乘积问题需注意 k≤1 的情况(正整数乘积最小为 1,直接返回 0);

    • 空字符串/空数组需提前返回 0。

  4. 单调性验证:遇到子串/子数组问题时,先手动模拟 2-3 个案例,确认状态是否满足单调性,再决定是否用滑动窗口。

七、总结

滑动窗口的核心是「用单调性压缩遍历维度」,剪枝只是优化手段。掌握它的关键在于:

  1. 判断问题是否满足「连续区间+状态单调性+状态可快速更新」;

  2. 根据目标(最长/最短/计数)确定「缩窗口条件」和「更新答案时机」;

  3. 套用通用模板,灵活调整状态统计工具(哈希表/和/积)。

只要抓住这三点,无论是简单的“无重复子串”,还是复杂的“最小覆盖子串”,都能按此逻辑拆解。建议多做几道经典例题,固化模板思维,面试时就能快速反应。

练习题推荐

按难度和类型分类,建议按顺序练习:

基础题(必做)

进阶题(推荐)

扩展题(挑战)

昨天以前首页

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

作者 颜酱
2025年12月27日 20:41

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

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

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

一、核心需求与设计思路

1. 要解决的问题

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

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

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

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

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

2. 核心设计原则

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

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

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

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

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

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

1. 定义基础类型

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

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

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

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

/**
 * 最小可用版微任务调度器
 * 核心能力:添加任务、批量在微任务执行、防重复调度
 */
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. 测试最小版功能

// 实例化调度器
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

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

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

2. 添加执行前/后回调

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

// 新增:执行前/后回调队列
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导致等待失效):

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

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

测试扩展能力

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

// 声明全局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. 添加参数校验

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

// 改造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. 异常捕获(容错)

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

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

五、完整生产级代码

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

// 类型命名更精准,贴合场景
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. 兼容环境+异常捕获,保证生产环境可用。

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

❌
❌