首页 纸飞机账号购买内容详情

事件循环:JS 的“外卖调度系统”大揭秘

2026-03-31 4 纸飞机账号购买

凭借什么setTimeout明明设定为0毫秒,然而却并非立即去执行呢?凭借什么Promise.then比起setTimeout会优先执行呢?今日我们要去揭开JS异步执行的深藏底层的秘密——事件循环。读完这一篇,你便能够如同背诵口诀一般记住各类异步任务的执行顺序,面试之时再也不用担心会被问到“输出顺序”了。前言。

难道你忘却了昨天咱们所提及的异步之事吗?JS会将那些耗费时间的任务抛掷给浏览器去予以执行,自身则持续朝着下方运行。然而存在的问题在于:当异步任务得以完成之后,回调函数究竟是怎样被调用起来的?又是由谁来判定先去执行哪一个回调?

这便要提及今日的主角,是事件循环,也就是Event Loop。它好似一个外卖调度中心,负责管理全部的“订单”,此“订单”即异步任务,还管理着“配送员”,这“配送员”指的是回调函数。弄明白它,你便弄清楚了JS的异步执行机制。

一、为什么需要事件循环?

JS具备单线程特性,这表明仅有一个“执行员”负责处理代码,倘若这个执行员遭遇卡顿状况,那么整个页面便会出现故障。

却浏览器给予了JS一组“外挂”,那便是Web APIs,像setTimeout、DOM事件、网络请求这般,JS碰到这些任务之时,会交由浏览器转至后台予以处理,自身接着去执行同步代码。

出现问题了:在后台任务结束之后,回调函数究竟是怎样回来开展执行的呢?这就要求事件循环去进行调度了——它的职责是留意着所谓的“任务队列”,只要主线程已经处于空闲状态了,便会将队列当中的任务提取出来予以执行。

二、事件循环的三大角色

事件循环就像一家外卖店,有三个核心角色:

1. 主线程:厨师

厨艺之人(处于主流程线程之中)仅仅从事一项事务:依照先后顺序去料理菜品(也就是执行具备同步特性的代码)。其在同一时刻仅能够着手处理一道菜品,只有当完成一道菜品之后才能够展开下一道菜品的制作。

2. 任务队列:待处理订单

从顾客那儿下的订单,(此订单是异步回调形式的),被送到了这个地方来排队。厨师把手里忙乎的活给弄完了之后,就会来到这个地方去取下一个订单。

3. 事件循环:调度员

调配人员(事件循环体)所承担的职责极为简易:持续目不转睛地注视着厨师,留意其是否已完成手头事务。一旦厨师完成了工作,便从任务序列之中选取一份订单递交给厨师。此番进程在不断重复,故而被称作“事件循环”。

三、宏任务 vs 微任务:VIP通道和普通通道

任务队列并不是只有一个,而是分两条通道:

调度员存在这样一个规则,每次从宏任务队列当中选取一个任务,将其执行完毕之后,要把当前所有的微任务全部执行完,之后再去取下一个宏任务。

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出顺序:1,4,3,2

为什么是这个顺序?

同步代码会先行执行(1,4),待其执行完毕之后,微任务队列当中存在Promise.then(3),全部予以执行,随后再去取下一个宏任务setTimeout(2),这便是事件循环的完整流程。

用代码来模拟一下事件循环的执行过程:

// 循环流程示意
while (true) {
  // 1. 执行一个宏任务(从宏任务队列取)
  const macroTask = macroQueue.shift();
  execute(macroTask);
  
  // 2. 执行所有微任务
  while (microQueue.length > 0) {
    const microTask = microQueue.shift();
    execute(microTask);
  }
  
  // 3. 可能进行一次UI渲染(浏览器)
  // 4. 回到步骤1
}

这个循环,乃是事件循环的核心逻辑所在。记住这般顺序,便能够推断出任何异步代码的执行顺序了。

五、经典面试题:输出顺序大挑战

来几个经典题目,看看你掌握了没有。

题目一:基础版

setTimeout(() => console.log('A'), 0);
Promise.resolve().then(() => console.log('B'));
console.log('C');
// 输出:C B A

题目二:嵌套版

setTimeout(() => {
  console.log('A');
  Promise.resolve().then(() => console.log('B'));
}, 0);
Promise.resolve().then(() => console.log('C'));
setTimeout(() => console.log('D'), 0);
console.log('E');
// 输出:E C A B D

分析:

同步代码,E是微任务,C是第一个宏任务也就是第一个setTimeout,它里面有A,然后有它的微任务B,第二个宏任务是第二个setTimeout,里面有D,题目三是async/await版。

async function test() {
  console.log('1');
  await console.log('2');
  console.log('3');
}
console.log('4');
test();
console.log('5');
// 输出:4 1 2 5 3

稍等一下,为何会是这样的一种排列顺序呢?等待之后的那部分代码,其实际上等同于被包裹在了承诺的后续处理函数里面,这属于微任务范畴。所以呀:

同步代码,4进入test函数,1await console.log('2'),先执行console.log('2'),然后await后面的代码(3)变成微任务,继续同步,5同步结束,执行微任务:3题目四:复杂混合。

setTimeout(() => console.log('A'), 0);
Promise.resolve()
  .then(() => {
    console.log('B');
    setTimeout(() => console.log('C'), 0);
  })
  .then(() => console.log('D'));
console.log('E');
// 输出:E B D A C

分析:

同步方面:存在E微任务,其关联Promise.then链,第一个then会进行输出B的操作,同时将setTimeout(C)添加到宏任务中,之后第二个then会输出D,这是因为第一个then返回的Promise会立刻触发第二个then,宏任务中有A,下一个宏任务是C,且UI渲染的位置在哪呢。

浏览器会于恰当的时候开展UI渲染,一般而言,是在一项宏任务执行完毕,并且所有微任务执行完毕之后,同时在下一个宏任务开启之前。

setTimeout(() => {
  document.body.style.background = 'red';
});
Promise.resolve().then(() => {
  document.body.style.background = 'blue';
});

置于上方的那段代码,其背景会率先转变为蓝色,紧接着再转变成红色。缘由在于,微任务会先行得以执行,继而是UI渲染,随后下一个宏任务才会被执行。

七、Node.js的事件循环不一样

Node.js的事件循环跟浏览器不一样,它存在着六个阶段,timers、pending、idle/prepare、poll、check、close。简单来讲,Node之中的setImmediate以及process.nextTick有着自身的顺序:

setImmediate(() => console.log('setImmediate'));
setTimeout(() => console.log('setTimeout'), 0);
process.nextTick(() => console.log('nextTick'));
// 输出:nextTick, setTimeout/setImmediate(顺序不一定)

在Node当中,setTimeout的执行顺序,取决于事件循环的当前阶段,它和setImmediate的执行顺序,并不一定是谁先执行,存在不确定性。

八、实际开发中的应用

知道事件循环究竟能起到什么样的作用呢?并非仅仅是为了能通过面试,其实在实际的项目开发过程当中它也是具备着相当大的效用的:

1. 用setTimeout分割长任务

要是存在一个极为耗费时间的循环,它会致使页面出现阻塞情况,那么能够采用setTimeout进行分片执行。

function processLargeArray(items, chunkSize = 100) {
  let index = 0;
  function process() {
    const chunk = items.slice(index, index + chunkSize);
    chunk.forEach(item => {
      // 处理每个item
    });
    index += chunkSize;
    if (index < items.length) {
      setTimeout(process, 0); // 让出主线程,让页面有机会渲染
    }
  }
  process();
}

2. 用微任务做“刚刚好”的异步

有时,你期望代码能够以异步的方式执行,然而同时又希望它能够尽可能迅速地执行,在这种情况下,可以运用微任务。

function doSomethingAsync(callback) {
  // 确保回调是异步执行的
  queueMicrotask(callback);
  // 或者 Promise.resolve().then(callback)
}

3. 用Promise优化“同步变异步”

要是存在某个操作,它有可能是同步的,也存在着是异步的这种情况,那么运用Promise进行包装,就能够确保执行顺序保持一致。

function fetchData() {
  if (cachedData) {
    // 用微任务确保异步执行
    return Promise.resolve(cachedData);
  }
  return fetch('/api/data');
}
// 调用方不需要担心是同步还是异步
fetchData().then(data => console.log(data));

九、总结:外卖调度员的日常

事件循环就是JS的“外卖调度员”,它的工作流程是:

由厨师,也就是主线程,完成一道菜,此为同步代码,之后调度员查看 VIP 通道,即微任务队列,看是否有菜需要上桌,若存在则全部上完,接着从普通通道,也就是宏任务队列,选取一道菜品给予厨师,如此循环重复。

牢记这般口诀:先是同步,接着是微任务,最后为宏任务。于宏任务里所嵌套的微任务,于下一回微任务循环之际予以执行。

倘若掌握了事件循环,那么你便掌握了 JS 异步的执行规律,那些看上去错综复杂的异步面试题,只不过是这个规则的排列组合罢了。

翌日,吾等即将步入JavaScript之又一至关重要之论题——Promise源码之达成,亲手撰写一个契合Promise/A+规格的Promise,以使汝能全然领悟其内在机制。

要是你认为今儿的“外卖调度”阐述得清晰易懂,那就点个赞以便让更多人得以瞧见。要是存有疑问,那就去评论区相见。最后,咱们明天再会!

事件循环:JS 的“外卖调度系统”大揭秘

相关标签: # 事件循环 # JS异步 # 宏任务 # 微任务 # 面试准备