Event Loop 总结

@sakila1012 2018-04-26 07:48:28发表于 sakila1012/blog

JavaScript 是一门非阻塞单线程语言,因为最初的 JavaScript 就是为了和浏览器交互而诞生的。如果 JS 是门多线程语言的话,我们在多个线程中处理 DOM 就可能发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。

JavaScript 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task (有多种 task)队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说,JavaScript 中的异步是同步行为。

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0)

console.log('script end');

以上代码虽然 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimout 还是会在 script end 之后打印。

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 成为 jobs,macrotask 称为 task

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0)

new Promise((resolve) => {
  console.log('promise');
  resolve()
}).then(function(){
  console.log('promise1');
}).then(function(){
  console.log('promise2');
});

console.log('script end');

// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。

微任务包括: process.nextTickpromiseObject.observeMutationObserver
宏任务包括:scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

很多人有个误区,认为微任务快于宏任务,其实是错误的,因为宏任务中包括了 script,浏览器会先执行一个宏任务,接下来有异步代码的话先执行微任务。

所以正确的一次 Event loop 顺序是这样的。

  1. 执行同步任务,这属于宏任务
  2. 执行栈为空,查询是否为微任务需要执行
  3. 执行所有微任务
  4. 必要的话渲染 UI
  5. 然后开始下一轮的 Event Loop,执行宏任务中的异步代码

通过上述的 Event Loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的界面响应,我们可以把操作 DOM 放入微任务中。