前言
JavaScript
中的事件循环(Event Loop)
是 JavaScript
运行时的核心组成部分,它负责调度和执行 JavaScript
代码。事件循环在单线程环境中非常重要,因为它能够处理异步操作,使得 JavaScript
能够以非阻塞的方式执行。
以下是事件循环的基本流程:
- 调用栈(Call Stack) :当 JavaScript 代码执行时,它们会首先进入调用栈。调用栈是一个按照执行顺序排列的函数调用列表。如果一个函数调用了另一个函数,那么后一个函数会被推入调用栈。
- 任务队列(Task Queue) :当调用栈中的任务完成执行后,会将相应的回调函数放入任务队列中。这些回调函数通常是在异步操作(例如 setTimeout, setInterval, 网络请求等)完成时由浏览器添加到任务队列的。
- 微任务(Microtasks) :微任务是在当前同步代码块执行完毕后立即执行的任务。它们通常用于处理异步操作的结果,例如 Promise 的回调函数。当同步代码执行完毕后,微任务队列中的所有微任务都会立即执行,直到队列清空。
- 事件循环:事件循环的工作就是将任务队列中的任务和微任务队列中的微任务依次取出并执行。在每次循环中,事件循环会先检查微任务队列,执行所有的微任务,直到队列清空。然后,事件循环会检查任务队列,取出并执行一个任务。如果调用栈为空,事件循环会继续从微任务队列中取出微任务并执行,直到微任务队列也清空。
- 时间片(Event Loop Delay) :在事件循环中,每次循环的时间长度被称为时间片。时间片的作用是限制每个循环中执行的代码长度,防止一个任务或一组微任务无限期地占用 CPU 时间。当时间片用完时,事件循环会暂停当前的任务或微任务,然后回到调用栈中继续执行下一行代码。
- Promise 的处理:Promise 是一种常用的异步编程方式。当一个 Promise 被创建并添加到微任务队列时,它会在下一个事件循环中立即执行。如果 Promise 的回调函数中还有异步操作,那么这些异步操作会在下一个事件循环中处理。
正文
因为JavaScript
是单线程的,事件循环在JavaScript
中显的极为重要,它可以让一些耗时时间比较久的任务延时执行(异步执行),让我的感知就认为JavaScript
是多线程在工作一样。
但是在事件循环中也会有任务时长比较快的,因为有部分任务够快也不能完全让他们依次执行,所以事件循环中也大致分为了两块,宏任务(也有部分人称其为任务,这里以下以宏任务称呼)和微任务。宏任务的执行时间比较久,会在任务队列中执行靠后,也就是执行的优先级比较低。微任务执行时间一般都比较短,可以很快的执行,所以的它的执行优先级比较高。
前面一段话正常理解,随便怎么理解都可以看出微任务的优先级高先执行,宏任务的执行优先级比较低后执行。那么我为什么会说,我认为是宏任务先执行呢?
先看宏任务和微任务分别包含哪些:
宏任务: script(整体代码)、 setTimeout、 setInterval、 I/O、 UI交互事件、 postMessage、 MessageChannel、 setImmediate(Node.js 环境)。
微任务: Promise.then、 Object.observe、 MutationObserver、 process.nextTick(Node.js 环境)。
可以看到其中宏任务是包含script
整体代码的,也就是必须先执行整体代码(宏任务)中的同步任务,才能接着执行实现循环,事件循环中先执行优先级比较高的微任务,紧接在当微任务执行完毕之后,再次执行宏任务,然后产看是否有微任务,如果有紧接着再次执行微任务,再次循环,如果没有,则任务循环中的代码执行完毕。
事件循环本身也是很好理解的,就是将异步任务循环处理,另外就是循环处理的时机不同罢了。
示例:
js
setTimeout(() => {
console.log(1);
}, 0);
//Promise里的内容是同步任务,只有then里的内容才是异步任务
new Promise((resolve) => {
console.log(2);
//这里也是同步执行的,所以在上面那个定时器之下
setTimeout(() => {
//这里执行之后,then内才有数(也就是微任务队列中才会添加微任务)
resolve(3);
}, 0);
}).then((v) => {
console.log(v);
});
console.log(4);
很简单,最后的结果为:2 4 3 1
那么再稍微加大点难度上一遍
js
//这里是的内容是同步任务
const promis = () =>
new Promise((resolve) => {
console.log(1);
setTimeout(() => {
resolve(2);
}, 1000);
});
const init = async () => {
//多个时间不同定时器
setTimeout(() => {
console.log(3);
}, 0);
setTimeout(() => {
console.log(4);
}, 1000);
console.log(5);
//await一下的内容都是微任务,同在then里的内容相同,但是有了then之后,promis不会再赋值了
const p = await promis().then(() => {
console.log(6);
});
console.log(p);
setTimeout(() => {
console.log(7);
}, 0);
};
//先调用函数
init();
console.log(8);
这里稍微思考一下,结果也能得出来:5 1 8 3 4 6 undefined 7 ,这里有undefined
得原因上面也说得,在.then
之后不会再重新赋值,所以得到了一个undefined
。
另外在JavaScript
中提供了微任务函数queueMicrotask
,函数内所有的内容均会在微任务中执行。
js
setTimeout(() => {
console.log(1);
}, 0);
queueMicrotask(() => {
console.log(2);
});
console.log(3);
结果:3 2 1
根据这个来展示最后一个示例,看看能否答对
js
const promis = (s = 1000) =>
new Promise((resolve) => {
console.log(1);
setTimeout(() => {
resolve(2);
}, s);
});
const fn = () => {
console.log(3);
return new Promise((resolve) => {
console.log(4);
queueMicrotask(() => {
resolve(5);
console.log(6);
});
});
};
const init = async () => {
queueMicrotask(() => {
console.log(7);
setTimeout(() => {
fn().then((v) => {
console.log(v);
setTimeout(() => {
console.log(8);
}, 0);
});
console.log(9);
}, 200);
});
promis(100).then(() => {
console.log(10);
queueMicrotask(() => {
console.log(11);
});
setTimeout(() => {
console.log(12);
}, 100);
});
setTimeout(async () => {
console.log(13);
const p = await fn();
console.log(p);
}, 0);
};
init();
console.log(14);
new Promise((resolve) => {
console.log(15);
setTimeout(() => {
resolve(16);
}, 0);
}).then((v) => {
console.log(v);
});
setTimeout(() => {
console.log(17);
}, 0);
console.log(18);
最终答案会在评论区
最后
以我个人角度来说,我就是认为事件循环就是先执行得宏任务 ,虽然说微任务得优先级比较高,看着是属于微任务先进行循环的,但是作为整个事件循环的开始,JavaScript
整体代码也应该算上。
其实就是每个人的理解不同,有的会认为是从异步开始事件循环开始,这个时候微任务必定是在宏任务前面执行的,那么每次循环都是微任务先执行。反正事件循环一定是微任务和宏任务交替执行的,不是微任务先执行就是先执行。
当然面试的时候认定宏任务或微任务先执行也许都不对,因为不知道面试官属于哪个流派。最好的情况就是将两个情况都说出来,如果认为JavaScript
整体代码在循环里,那就是宏任务先执行,否者就是微任务先执行。
你认为是宏任务限制性还是微任务先执行?