JS事件循环机制和Vue的nextTick原理

247 阅读4分钟

一、先看个生活场景

假设银行柜台只有一个窗口(JS主线程),来办业务的人分三种:

  1. 普通客户(同步任务):直接冲到窗口办业务(立即执行)
    console.log('取号排队'); // 立即办理
    
  2. VIP客户(微任务) :拿金卡的客户,当前业务办完后必须立刻服务
    Promise.resolve().then(() => console.log('VIP窗口办理')); // 插队办理
    
  3. 预约客户(宏任务) :取了号在等候区等待叫号
    setTimeout(() => console.log('预约客户办理'), 0); // 最后办理
    

二、事件循环执行口诀

  1. 处理完所有普通客户(同步代码全执行)
  2. VIP客户立刻插队处理(清空微任务队列)
  3. 叫一个预约客户办理(执行一个宏任务)

重复这个流程直到关门(程序结束)。

常见宏任务和微任务

宏任务(Macro-task)微任务(Micro-task)
setTimeout/setIntervalPromise.then/catch/finally
I/O 操作(如文件读取)MutationObserver
DOM 事件回调(如点击)queueMicrotask
requestAnimationFrame

三、看几个实战案例

案例1:基础流程

console.log('普通客户1号');
setTimeout(() => console.log('预约客户1号'), 0);
Promise.resolve().then(() => console.log('VIP客户1号'));
console.log('普通客户2号');

输出结果

普通客户1号
普通客户2VIP客户1号
预约客户1

案例2:VIP中还有VIP

Promise.resolve().then(() => {
  console.log('VIP客户1号');
  Promise.resolve().then(() => console.log('VIP的家属'));
});

输出结果

VIP客户1VIP的家属

(微任务队列必须清空到一滴都不剩)

案例3:宏任务嵌套

setTimeout(() => {
  console.log('预约客户1号');
  setTimeout(() => console.log('预约客户的家属'), 0);
}, 0);

输出结果

预约客户1号
预约客户的家属

(每次只处理一个宏任务)


四、async/await 的猫腻

async函数其实就是披着马甲的Promise:

async function 买奶茶() {
  console.log('1. 付钱');
  await 等制作(); // 这里会生成微任务
  console.log('3. 拿奶茶');
}

function 等制作() {
  return new Promise(resolve => {
    console.log('2. 开始制作');
    resolve();
  });
}

买奶茶();

输出结果

1. 付钱
2. 开始制作
3. 拿奶茶

五、实际开发三大坑

坑1:以为点击事件能抢跑

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('统计点击量'));
  console.log('按钮被点了');
});

点击后输出:

按钮被点了
统计点击量

(整个点击回调都是宏任务)

坑2:requestAnimationFrame 乱入

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

可能输出:

RAF
setTimeout

(动画回调在渲染前执行,算特殊宏任务)

坑3:微任务无限循环

function 死循环() {
  Promise.resolve().then(死循环);
}
死循环();

(页面直接卡死,因为微任务队列永远清不完)

拓展 : nextTick 的作用

在 Vue 中,当你修改数据后,DOM 并不会立即更新,而是进入一个异步更新队列。nextTick 的作用就是让你在 DOM 更新完成后执行一些操作。

1. 核心思想

  • 利用微任务:Vue 会优先使用微任务(如 Promise.then)来调度 nextTick 回调。
  • 降级策略:如果当前环境不支持微任务(如 IE),则降级为宏任务(如 setTimeout)。

2、nextTick 的执行时机

  • 当你修改 Vue 实例的数据时,Vue 会将这些更新操作放入一个队列中。
  • 在下一个事件循环中,Vue 会清空这个队列并更新 DOM。
  • nextTick 的回调会在 DOM 更新完成后执行。

3、nextTick 与事件循环的关系

微任务优先

  • Vue 默认使用微任务(如 Promise.then)来实现 nextTick
  • 微任务会在当前事件循环的同步代码执行完毕后立即执行,确保 DOM 更新后回调立即触发。

降级为宏任务

  • 在不支持微任务的环境中(如 IE),Vue 会降级为宏任务(如 setTimeout)。
  • 宏任务会在下一个事件循环中执行,延迟稍高。

总结

  • 利用微任务:优先使用 Promise.then 确保回调在 DOM 更新后立即执行。
  • 降级策略:在不支持微任务的环境中降级为 setTimeout
  • 与事件循环的关系nextTick 的回调会在当前事件循环的微任务阶段执行。
  • 微任务的优先级高
    指的是 在每一轮事件循环中,微任务会在下一个宏任务之前被强制执行,且必须清空队列。
  • 宏任务的优先级低
    必须等待所有微任务执行完毕后,才能执行下一个宏任务。