JavaScript宏任务、微任务和事件循环
JavaScript是一门单线程的语言,也就是说,在同一时间只能执行一个任务,其他的任务必须等待当前任务完成后才能执行。那么,JavaScript是如何处理异步操作的呢?答案就是事件循环(Event
Loop)。
什么是事件循环?
事件循环是JavaScript的执行机制,它可以让JavaScript在执行一个任务的同时,监听其他异步事件的发生,并在合适的时机将它们加入到执行队列中。事件循环有两种类型的任务:宏任务(MacroTask) 和微任务(MicroTask) 。它们有以下特点:
- 宏任务:可以理解为主线程上的一个个独立的工作单元,例如:script(整体代码)、setTimeout、setInterval、setImmediate、requestAnimationFrame、I/O、UI rendering等。
- 微任务:可以理解为在当前主线程上需要优先执行的一些小工作单元,例如:Promise.then()、MutationObserver、process.nextTick等。
也就是说,在每一次事件循环中,只会从宏任务队列中取出一个宏任务来执行,然后检查并清空所有微任务队列中的所有微任务,然后再从宏任务队列中取出下一个宏任务来执行,如此循环往复。这就是事件循环的基本流程。
js代码的执行机制
- 首先,js代码在执行之前需要先编译,进行变量提升和函数声明。
- 然后,js代码按照顺序进入主线程(主执行栈)执行,遇到同步任务就立即执行,遇到异步任务就放入到事件队列(Event Queue) 中等待。
- 事件队列中的异步任务又分为宏任务(macro task) 和微任务(micro task) ,宏任务包括整体代码script、setTimeout、setInterval等,微任务包括Promise.then()、MutationObserver等。
- 在每一次事件循环(Event Loop) 中,只会从宏任务队列中取出一个宏任务来执行,然后检查并清空所有微任务队列中的所有微任务并依次执行。
- 然后再从宏任务队列中取出下一个宏任务来执行,如此循环往复。
- 也就是说,js代码的执行顺序是:同步代码 > 微任务 > 宏任务。这样可以保证一些优先级较高或者依赖于当前状态或者DOM结构的异步操作能够正确地执行。
为什么要区分宏任务和微任务?
你可能会问,为什么要区分宏任务和微任务呢?为什么不把所有的异步操作都放到一个队列里呢?这样不是更简单吗?
其实,区分宏任务和微任务是有一定意义的。首先,这样可以保证一些优先级较高的异步操作能够尽快被执行,例如Promise.then()中的回调函数就是一个典型的例子。如果把Promise.then()放到宏任务队列中,那么就需要等待当前的宏任务以及其他排在前面的宏任务都执行完毕后才能执行,这样可能会导致一些不必要的延迟。而如果把Promise.then()放到微任务队列中,那么就可以在当前的宏任务结束后立即执行,这样可以提高响应速度和用户体验。
其次,这样可以保证一些依赖于当前状态或者DOM结构的异步操作能够正确地执行,例如MutationObserver就是一个典型的例子。MutationObserver用于监听DOM树上发生的变化,并在变化发生后触发回调函数。如果把MutationObserver放到宏任务队列中,那么就需要等待当前以及其他排在前面的宏任务都执行完毕后才能触发回调函数,这样可能会导致一些不符合预期或者错误的结果。而如果把MutationObserver放到微任务队列中,那么就可以在当前的宏任务结束后立即触发回调函数,这样可以保证回调函数能够获取到正确的状态和DOM结构。
事件循环的示例
为了更好地理解事件循环的工作原理,我们来看一个简单的示例:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
如何理解和使用宏任务和微任务?
通过上面的介绍,我们已经知道了事件循环是如何处理不同类型的异步代码块的,以及为什么要区分宏任务和微任务。那么如何理解和使用宏任务和微任务呢?有什么注意事项或者技巧呢?
首先,我们需要明确哪些异步代码块属于宏任务,哪些属于微任务。一般来说:
- 宿主环境发起的、需要在主线程上执行的异步代码块都属于宏任务。
- JavaScript引擎发起的、需要在当前宏任务执行完毕后立即执行
这段代码中,有一个宏任务(script) 和三个微任务(两个Promise.then()和一个setTimeout) 。那么它们会按照什么顺序执行呢?
script start
script end
promise1
promise2
setTimeout
解释如下:
- 首先,事件循环从宏任务队列中取出第一个宏任务(script) ,并开始执行。
- 然后,遇到了console.log(‘script start’),打印出’script start’。
- 接着,遇到了setTimeout,将其回调函数放入到宏任务队列中等待执行。
- 然后,遇到了Promise.resolve().then(),将其回调函数放入到微任务队列中等待执行。
- 接着,遇到了console.log(‘script end’),打印出’script end’。
- 此时,当前的宏任务(script)执行完毕,检查并清空微任务队列中所有的微任务,并依次执行。
- 因此,先执行第一个Promise.then()的回调函数,并打印出’promise1’。
- 然后,在第一个Promise.then()的回调函数中又返回了一个新的Promise对象,并在其上注册了第二个Promise.then()。
- 因此,将第二个Promise.then()的回调函数放入到微任务队列中等待执行。
- 接着,执行第二个Promise.then()的回调函数,并打印出’promise2’。
- 此时,微任务队列已经清空。再次从宏任务队列中取出下一个宏任务(setTimeout)并开始执行。因此,最后执行setTimeout的回调函数,并打印出’setTimeout’。
总结
本文介绍了JavaScript宏任务、微任务和事件循环的概念、区别和意义,并通过一个示例展示了事件循环的工作流程。希望本文能够帮助你更好地理解JavaScript异步编程机制,并提高你在前端开发领域的技能水平。如果你有任何疑问或者建议,请在评论区留言。谢谢!