凭借什么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,以使汝能全然领悟其内在机制。
要是你认为今儿的“外卖调度”阐述得清晰易懂,那就点个赞以便让更多人得以瞧见。要是存有疑问,那就去评论区相见。最后,咱们明天再会!
