在JavaScript中,事件循环(Event Loop)
是一个重要的概念,特别是在一些公司的面试中,这是一道必考题。如果你搞不懂事件循环(Event Loop)的概念,那你绝对算不上一个优秀的程序员,本篇博文,希望能帮助大家搞懂浏览器的 Event Loop。
什么是事件循环
由于 JavaScript 是单线程运行的语言,为了能够处理异步事件 ,引入了事件循环机制。它在JavaScript运行环境中负责管理和调度事件和任务的机制。换句话说,它决定了代码中各个部分的执行顺序。事件循环的主要目标是保证 JavaScript 单线程
的特性,即一次只处理一个任务,而不会造成阻塞。
任务队列
在事件循环中,JS引擎会不断地从任务队列中取出任务执行,直到任务队列中没有任务为止。这里就涉及到了 JS 中另外一个知识点:任务队列(Task Queue)
。
当异步操作完成时,会将对应的回调函数放入任务队列中。而事件循环负责从任务队列中取出任务,并执行它们。这样,JavaScript就可以在等待某个操作完成的同时,继续处理其他任务。
宏任务和微任务
任务队列分为两种类型:宏任务(Macrotask)
和微任务(Microtask)
。在每次执行完一个宏任务后,都会按照特定的顺序执行微任务队列中的所有任务,然后才会进入下一个宏任务。
常见的宏任务(Macrotask):
- setTimeout
- setInerval
- setImmediate
- requestAnimationFrame
- I/O
常见的微任务(Microtask):
- process.nextTick(Node独有)
- Promise
- Object.observe
- MutationObserver
每次只能执行一个宏任务,但可以连续执行多个微任务。
执行过程
JavaScript 事件循环机制可以分为以下几个阶段:
1.执行同步代码
当 JavaScript 引擎开始执行脚本时,会先执行当前线程的同步代码,即按照顺序执行脚本中的每一行代码,直到遇到耗时较长的操作或者异步操作为止。
2.执行宏任务
执行完同步代码后,JavaScript 引擎会从宏任务队列中取出一个任务执行。例如,我们可以使用 setTimeout 函数来延迟一段时间之后执行回调函数:
js
console.log('Start');
setTimeout(function() {
console.log('我是 Async Task ----');
}, 1000);
console.log('End');
在这段代码中,Start
和 End
是同步代码,会先执行完毕。接着,setTimeout 函数是一个宏任务
,会被放入宏任务队列中等待执行。1 秒钟后,setTimeout
函数所设置的回调函数就会被取出来执行。
3. 执行微任务
在执行完一个宏任务之后,JavaScript 引擎会按照顺序执行微任务队列中的所有任务。常见的微任务包括
例如,我们可以使用 Promise 构造函数来创建一个异步任务:
js
console.log('Start');
Promise.resolve().then(function() {
console.log('我是 Micro Task ----');
});
console.log('End');
在这段代码中,Start
和 End
是同步代码,会先执行完毕。接着,Promise.resolve
函数是一个微任务
,会被放入微任务队列中等待执行。当宏任务执行完成后,JavaScript 引擎会立即按照顺序执行微任务队列中的所有任务。
Node.js 中的事件循环
Node 的 Event Loop 分为6个阶段,它们会按照顺序反复运行。
- timers:定时器任务,setTimeout 和 setInterval 等定时器回调函数将会被添加到该队列。
- pending callbacks:处理一些系统级别的回调函数,例如 TCP 或 UDP 连接上异常关闭时,会产生此类回调函数。
- idle, prepare:内部使用的任务队列,我们不需要关注。
- Poll轮询:轮询任务队列,执行 I/O 相关回调,例如文件 I/O、网络 I/O 等,几乎所有的回调都将在此排队等待。
- check:setImmediate 回调函数的执行队列。
- close callbacks:close 事件的回调队列,例如 socket.on('close')。
在浏览器事件循环中,每执行完一个宏任务,便要检查并执行微任务队列;而node事件循环中则是在"上一阶段"执行完,"下一阶段"开始前执行微任务队列中的任务。
最后,留一道面试题给大家,看看你有没有真的搞懂JavaScript 的事件循环机制。
js
console.log(1);
setTimeout(() => {
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise((resolve) => {
console.log(4);
resolve();
}).then(() => {
console.log(5);
});
});
new Promise((resolve) => {
console.log(7);
resolve();
}).then(() => {
console.log(8);
});
process.nextTick(() => {
console.log(6);
});
setTimeout(() => {
console.log(9);
process.nextTick(() => {
console.log(10);
});
new Promise((resolve) => {
console.log(11);
resolve();
}).then(() => {
console.log(12);
});
});
请问上面代码中,输出顺序是什么? 答案&解析
总结
JavaScript事件循环是实现异步编程的重要机制。通过合理地利用宏观任务和微观任务以及事件循环的特性,我们可以编写出高效、响应迅速的JavaScript代码。
了解事件循环的工作原理对于开发者来说是非常重要的,它有助于我们更好地理解JavaScript的运行机制,并能够更好地调试和优化我们的代码。
往期文章: