Event Loop
JS 之所以是单线程的是因为浏览器(多线程)只分配一个线程来执行 JS 代码,之所以只分配一个线程试因为浏览器考虑到多线程操作会导致的一些问题,假设 JS 是多线程的,其中一个线程在 DOM 节点上添加内容,而另一个线程在这个节点上删除内容,那么浏览器该执行哪一个呢?所以 JS 的设计就是单线程的。但是单线程会造成很多的任务都需要等待执行,所以就引入了浏览器的事件循环机制。
JS 是单线程的,只有一个 Call Stack,浏览器是多线程的,并且 DOM 事件、AJAX(XMLHttpRequest)、setTimeout 都是有单独的线程处理。在这些异步事件结束,runtime会把它们的 callback 按顺序放到 Callback Queue 里,Event Loop 会检测 Call Stack,一旦它为空,就会把 Callback Queue 里的回调函数依次放到 Call Stack 里执行,直到 Callback Queue 为空。
宏任务/微任务
在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
Callback Queue(Task Queue)里的回调事件称为宏任务(macrotask),每次异步事件结束后,它们的回调函数会依次按时间顺序放在 Callback Queue 里,等待 Event Loop 依次把它们放到 Call Stack 里执行。
比如:setInterval
、setTimeout
、script
、setImmediate
、I/O
、UI rendering
就是宏任务(macrotask)。
setImmediate>MessageChannel>setTimeout
微任务(microtasks)是指异步事件结束后,回调函数不会放到 Callback Queue,而是放到一个微任务队列里(Microtasks Queue),在 Call Stack 为空时,Event Loop 会先查看微任务队列里是否有任务,如果有就会先执行微任务队列里的回调事件;如果没有微任务,才会到 Callback Queue 执行回到事件。
比如:promise
、process.nextTick
Object.observe
MutationObserver
就是微任务(microtasks)。
Promise 中的代码是被当做同步任务立即执行的。而在 async/await 中,在出现 await 出现之前,其中的代码也是立即执行的,await 后面的代码是 microtask。
整个 Event Loop 的执行顺序如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取,也就是 callbacke queue)
流程图如下:
微任务的优先级要高于宏任务
因为微任务是ES6语法规定的,而宏任务是浏览器规定的。
和DOM渲染的关系
每次 Call Stack清空(即每次轮询结束),即同步任务执行完。DOM都有机会改变则重新渲染,然后再进行下一次 Event Loop。而微任务在DOM渲染前触发,宏任务在DOM渲染后触发。(宏任务处于下一轮的Event Loop)
参考
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7
https://limeii.github.io/2019/05/js-eventloop/
令人费解的 async/await 执行顺序