• 彻底搞懂 JavaScript 微任务:带你玩转事件循环
  • 发布于 1周前
  • 54 热度
    0 评论
事件循环:一根“橡皮筋”撑起整个 JS 世界
JS 是单线程的,但它还想不阻塞,于是祭出了祖传神器——事件循环。
把它想成一家烤串店:
角色 类比
调用栈 Call Stack 烤串师傅(一次只能烤一串)
宏任务队列 Macrotask Queue 外卖订单(setTimeout、I/O、UI 渲染)
微任务队列 Microtask Queue 加急小票(Promise、queueMicrotask、MutationObserver)
事件循环“烤串流程”
1.师傅把手上所有串烤完(同步代码)。
2.先处理所有加急小票(清空微任务)。
3.擦桌子、摆盘(浏览器渲染)。
4.烤下一单外卖串(取一个宏任务)。
5.重复 1-4,直到打烊。
微任务之所以“香”,就在于它插队在渲染之前,能保证 UI 不会“花屏”。

举个栗子:点击保存按钮到底干了啥?
document.getElementById('saveButton').addEventListener('click', async () => {
  updateLoadingState(true);          // 1. 立刻转菊花
  try {
    await saveUserData(userData);    // 2. 异步保存
    showSuccessMessage('保存成功');  // 3. 微任务里更新 UI
    const data = await fetchUser();  // 4. 再拿新数据
    updateUI(data);                  // 5. 再更新 UI
  } catch (e) {
    showErrorMessage(e.message);     // 6. 出错兜底
  } finally {
    updateLoadingState(false);       // 7. 关闭菊花
  }
});
菊花之所以能立刻转起来,是因为 updateLoadingState(true) 是同步的。
所有 await 后面的活儿,都会在微任务里排队,所以 UI 不会半吊子。
queueMicrotask():官方插队券
浏览器新 API,作用就是往微任务队列里塞一张“加急小票”。
queueMicrotask(() => {
  console.log('我是加急,宏任务都靠边!');
});
与 setTimeout(fn, 0) 的区别:setTimeout 是宏任务,得等渲染;queueMicrotask 是微任务,马上跑。

实战:ThemeManager 只刷一次屏
class ThemeManager {
  constructor() {
    this.theme = { darkMode: false, contrast: 'normal', fontSize: 16 };
    this.isRendering = false;
  }
  // 堆代码 duidaima.com
  updateTheme(updates) {
    // 1. 改数据
    this.theme = { ...this.theme, ...updates };
    // 2. 只排一次队,避免多次重排
    if (!this.isRendering) {
      this.isRendering = true;
      queueMicrotask(() => {
        this.applyThemeToDOM();
        this.isRendering = false;
      });
    }
  }

  applyThemeToDOM() {
    document.documentElement.style.setProperty(
      '--bg',
      this.theme.darkMode ? '#1a1a1a' : '#fff'
    );
  }
}

const tm = new ThemeManager();
tm.updateTheme({ darkMode: true });
tm.updateTheme({ contrast: 'high' });
tm.updateTheme({ fontSize: 20 });
// 最终只调用一次 applyThemeToDOM,性能拉满!
FormController:表单提交不抖屏
class FormController {
  constructor(formEl) {
    this.form = formEl;
    this.isSubmitting = false;
    this.form.addEventListener('submit', async (e) => {
      e.preventDefault();
      if (this.isSubmitting) return;

      this.setSubmitting(true);
      try {
        const res = await fetch('/api/submit', { method: 'POST', body: new FormData(this.form) });
        if (!res.ok) throw new Error(res.statusText);

        // 用微任务确保状态更新后再提示用户
        queueMicrotask(() => {
          this.showSuccess();
          this.resetForm();
        });
      } catch (err) {
        this.showError(err);
      } finally {
        this.setSubmitting(false);
      }
    });
  }

  setSubmitting(flag) {
    this.isSubmitting = flag;
    this.form.querySelector('button[type="submit"]').disabled = flag;
  }
}
微任务 vs Promise:谁先谁后?
console.log('Script start');

setTimeout(() => console.log('setTimeout'), 0);

queueMicrotask(() => {
  console.log('Microtask 1');
  queueMicrotask(() => console.log('Nested Microtask 1'));
});

Promise.resolve()
  .then(() => {
    console.log('Promise 1');
    queueMicrotask(() => console.log('Nested Microtask 2'));
  })
  .then(() => console.log('Promise 2'));

queueMicrotask(() => console.log('Microtask 2'));

console.log('Script end');

/*
输出顺序:
Script start
Script end
Microtask 1
Promise 1
Microtask 2
Nested Microtask 1
Promise 2
Nested Microtask 2
setTimeout
*/
口诀:“清完微任务,才轮到宏任务”。
Promise 的 .then 每挂一次就塞一次微任务,层层嵌套也按 FIFO 排队。
踩坑指南
1. 无限递归 → 页面卡死
function infiniteMicrotask() {
  queueMicrotask(infiniteMicrotask); // 卡死循环,浏览器直接罢工
}
2. 微任务里干重活 → 阻塞渲染
queueMicrotask(() => {
  // 千万别算大斐波那契,UI 会冻成狗
});
3. 与宏任务混用 → 顺序谜之混乱
用 setTimeout + Promise 时要时刻提醒自己:
“宏任务等渲染,微任务插渲染”。

🆚 微任务 vs Go 的 defer
特性 JS 微任务 Go defer
触发时机 当前同步栈清空后 函数 return 前
执行顺序 FIFO(队列) LIFO(栈)
异步? ✅ 是 ❌ 否
典型用途 UI 批处理/状态同步 文件关闭/锁释放

DevTools 调试三板斧

1.Performance 面板:录一段,看微任务耗时。
2.console.trace():在微任务里打栈,一眼定位来源。

3.断点:直接在 .then 或 queueMicrotask 回调里打断点,看闭包变量。


推荐用法与黑名单
👍 建议用
.批处理 UI 状态(React setState 合并、Vue nextTick)
.高优操作:打日志、埋点
.配合 MutationObserver 做 DOM 同步
🚫 别乱用
.CPU 密集计算
.深层递归

.时间敏感动画(交给 requestAnimationFrame)


🎤 结束彩蛋
微任务就像插队的小黄人——
它们个头小,但一拥而上就能把宏任务挤到门口。
用好了页面丝滑,用炸了浏览器罢工。
记住口诀:“同步烤串 → 微任务插队 → 宏任务收尾”,包你横行事件循环!
Happy coding!评论区等你分享翻车现场~
用户评论