引入
JavaScript 是一种单线程的语言,意味着它一次只能执行一个任务。然而,它却能够处理许多并发任务,这得益于它的事件驱动模型和事件循环机制,今天将来给大家介绍一下事件循环相关知识
什么是事件循环?
事件循环是 JavaScript 运行时环境中的一个重要组成部分,负责管理执行任务的顺序。它允许 JavaScript 运行时在等待异步任务完成时执行其他任务,以保持程序的响应性。
宏任务(Macro Tasks)
宏任务是由 JavaScript 运行时环境(如浏览器或 Node.js)安排的任务,它们被添加到任务队列中。典型的宏任务包括定时器回调、事件监听器回调、I/O 操作等。在每次事件循环迭代中,宏任务队列中的任务会被依次执行,直到队列为空。
微任务(Micro Tasks)
微任务是在宏任务执行完毕后立即执行的任务,它们不会被添加到任务队列中,而是被添加到微任务队列中。典型的微任务包括 Promise 的回调函数、MutationObserver 回调等。微任务队列的任务会在下一个事件循环迭代开始前执行。
事件循环的过程:
- 执行全局同步代码:首先,执行全局同步代码,将同步任务添加到执行栈中,并开始执行它们。
- 处理宏任务:当遇到宏任务(例如定时器回调、I/O 操作、事件监听器回调等)时,将其添加到相应的宏任务队列中。
- 处理微任务:在执行栈为空时,会首先处理微任务队列中的任务(例如 Promise 的回调函数)。微任务队列的任务会在下一个事件循环迭代开始前执行。
- 从宏任务队列中获取任务:如果微任务队列为空,事件循环会从宏任务队列中获取一个宏任务,并将其相关的回调函数推入执行栈中执行。
- 重复:重复以上步骤,直到所有任务队列都为空。
代码示例:
console.log('Start');
// 微任务
Promise.resolve().then(function() {
console.log('Microtask 1');
});
// 宏任务
setTimeout(function() {
console.log('Timeout 1');
// 微任务
Promise.resolve().then(function() {
console.log('Microtask 2');
});
}, 0);
// 宏任务
setTimeout(function() {
console.log('Timeout 2');
}, 0);
console.log('End');
初始状态
- 执行栈: 空
- 宏任务队列: 空
- 微任务队列: 空
第1步
执行 console.log('Start');
,输出 "Start"。
- 执行栈:
console.log('Start');
- 宏任务队列: 空
- 微任务队列: 空
第2步
执行 Promise.resolve().then(function() { console.log('Microtask 1'); });
,微任务入队,但尚未执行。
- 执行栈:
Promise.resolve().then(...)
- 宏任务队列: 空
- 微任务队列:
console.log('Microtask 1');
第3步
执行第一个 setTimeout(function() { console.log('Timeout 1'); ... }, 0);
,宏任务入队。
- 执行栈:
setTimeout(...)
- 宏任务队列:
function() { console.log('Timeout 1'); ... }
- 微任务队列:
console.log('Microtask 1');
第4步
执行第二个 setTimeout(function() { console.log('Timeout 2'); }, 0);
,宏任务入队。
-
执行栈:
setTimeout(...)
-
宏任务队列:
function() { console.log('Timeout 1'); ... }
function() { console.log('Timeout 2'); }
-
微任务队列:
console.log('Microtask 1');
第5步
执行 console.log('End');
,输出 "End"。
-
执行栈:
console.log('End');
-
宏任务队列:
function() { console.log('Timeout 1'); ... }
function() { console.log('Timeout 2'); }
-
微任务队列:
console.log('Microtask 1');
第6步
执行栈清空,开始处理微任务队列,执行 console.log('Microtask 1');
,输出 "Microtask 1"。
-
执行栈: 空
-
宏任务队列:
function() { console.log('Timeout 1'); ... }
function() { console.log('Timeout 2'); }
-
微任务队列: 空
第7步
微任务队列清空,事件循环检查宏任务队列,取出并执行第一个宏任务,执行 console.log('Timeout 1');
,输出 "Timeout 1"。
- 执行栈:
console.log('Timeout 1');
- 宏任务队列:
function() { console.log('Timeout 2'); }
- 微任务队列: 空
第8步
在执行 "Timeout 1" 的宏任务中,再次添加微任务 Promise.resolve().then(function() { console.log('Microtask 2'); });
,微任务入队。
- 执行栈:
Promise.resolve().then(...)
- 宏任务队列:
function() { console.log('Timeout 2'); }
- 微任务队列:
console.log('Microtask 2');
第9步
宏任务 "Timeout 1" 执行完毕,执行栈清空,开始处理微任务队列,执行 console.log('Microtask 2');
,输出 "Microtask 2"。
- 执行栈: 空
- 宏任务队列:
function() { console.log('Timeout 2'); }
- 微任务队列: 空
第10步
微任务队列清空,事件循环检查宏任务队列,取出并执行第二个宏任务,执行 console.log('Timeout 2');
,输出 "Timeout 2"。
- 执行栈:
console.log('Timeout 2');
- 宏任务队列: 空
- 微任务队列: 空
第11步
所有任务执行完毕,执行栈清空。
最终输出顺序是:
Start
End
Microtask 1
Timeout 1
Microtask 2
Timeout 2
结语
JavaScript 事件循环机制是理解异步编程的关键。通过深入了解事件循环的工作原理以及宏任务与微任务的区别,我们可以更好地处理异步任务,提高程序的性能和响应性。希望本文能够帮助你更深入地理解 JavaScript 的事件循环机制!
作者的小猜想(未必准确,有错必改)
js中定时器我理解是回调定时加入宏任务队列,且没有先后这一说,如果队列里面有其他宏任务,新的回调也是加入到队尾,所以说“定时未必准时”,因为处理定时回调前可能微任务队列,宏任务队列还有其他的任务要处理,当然一般情况下应该都是准时的。