事件循环
我们都知道JS是一门单线程的非阻塞的脚本语言,他最初的定位就是用来与浏览器DOM交互的,所以决定了他是单线程,防止多个线程操作同一个DOM引起冲突。非阻塞则是JS在执行异步任务的时候不会阻塞整个JS主线程的执行,而是会挂起任务,等待执行完成之后再回调。
有人说web worker
不是多线程吗,其实web worker
严格来讲是浏览器JS主线程的子线程,而且这些子线程也没有I/O的权限,不能操作DOM。web worker
的主要作用是替主线程分担一些耗时的计算任务。
上面说到了非阻塞的特性会让JS挂起异步任务,那么这些被挂起的异步任务需要一个时机去执行,这种执行的策略在JS中被称作是“事件循环”(Event Loop)。由于浏览器和node环境的JS执行环境不同,这也导致了他们的事件循环不一致。下面分别来讲下事件循环在这两种环境下的表现。
宏任务与微任务
在讲之前先说下两个概念一个是微任务,一个是宏任务。
微任务:
microtask
在ES6中改名为jobs
,微任务包括process.nextTick
(Node中独有)promise
(ES6+支持)Object.observe
(被废弃)MutationObserver
(新API,监听DOM节点变化,MDN文档)
宏任务:
macrotask
在ES6中改名为task
,宏任务包括script
(浏览器)setTimeout
setInterval
setImmediate
(IE和node支持)I/O
UI render
(浏览器)
微任务与宏任务有一个共同点是他们都是异步的任务(script
除外,同步任务),不同的是在同一个事件循环中微任务会在宏任务之前执行(script
除外)。
浏览器中
在浏览器中事件循环执行顺序是:
- 执行
script
同步任务 - 执行并清空微任务列表
- UI render
- 下一轮Event Loop执行异步宏任务 ... 循环
来段代码遛遛:
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
Node
Node中的事件循环和浏览器中的略有不同,不过大体上是类似的。
Node中的事件循环可以分为6个阶段,下面是我从Node文档找的示例图:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
- timer:会执行已经到期的 setTimeout() 和 setInterval() 的回调函数
- pending callbacks(I/O callbacks):会执行除了
timer
,close
,setImmediate
之外的事件回调 - idle, prepare:仅系统内部使用
- poll:轮询,不断检查有没有新的
I/O
事件,事件环可能会在这里阻塞。这里主要分为两个步骤- 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调
- 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。
- check:执行
setImmediate
回调 - close:执行
close
事件
这六个阶段好像漏掉了上面提到的一个Node中的微任务process.nextTick()
,process.nextTick()
会先于其他的microtask执行
理解 process.nextTick() 您可能已经注意到 process.nextTick() 在关系图中没有显示,即使它是异步 API 的一部分。这是因为 process.nextTick() 在技术上不是事件循环的一部分。相反,无论事件循环的当前阶段如何,都将在当前操作完成后处理 nextTickQueue。这里的一个操作被视作为一个从 C++ 底层处理开始过渡,并且处理需要执行的 JavaScript 代码。
回顾我们的关系图,任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前得到解决。这可能会造成一些糟糕的情况, 因为它允许您通过进行递归 process.nextTick() 来“饿死”您的 I/O 调用,阻止事件循环到达 轮询 阶段。