我们平常在写 setTimeout,onclick,ajax, Promise 的时候,通常称之为异步。拿 setTimeout 来说,定时任务会在合适的时机去执行。既然 JavaScript 是单线程,那是谁在定时,谁在判断是否应该去执行定时任务了?所谓异步执行和单线程是什么关系?
再看网上很常见的前端题:
console.log(1)
setTimeout(function() {
console.log(2)
}, 0)
new Promise(function (resolve) {
console.log(3)
var i = 0
while (i < 9999999) {
i++
}
resolve()
console.log(4)
}).then(function() {
console.log(5)
})
console.log(6)
运行结果: 1,3,4,6, 5,2
如果你刚学 JavaScript ,可能会觉得很懵逼,为什么 setTimeout 0 不是马上执行?为什么 Promise then 比 setTimeout 快?如果你有点困惑,欢迎继续阅读下面的内容。
引擎和环境
Javascript 是由引擎和宿主环境构成,比如在 Chrome 浏览器里面就是 V8 引擎结合浏览器宿主环境。写页面的时候常用的 setTimeout,ajax, DOM 这些 API 是由宿主环境提供,引擎本身不负责这些功能。所以 Node.js Timer 里面的 setTimeout 和浏览器里的 setTimeout 不是一个东西。
V8 引擎只负责维护 Call Stack 和 Memory Heap,Memory Heap 就是平时内存申请发生的地方。Call Stack 是我们比较关注的点。
Call Stack
Call Stack 记录着当前代码执行位置,所以报错的时候我们会见到 stack 里面的信息。也就是走到错误这一步之前,经历了什么。
function foo () {
throw new Error('Oops!')
}
function bar () {
foo()
}
bar()
Oops
让程序爆炸——JS 是单线程的编程语言,单线程意味着,单位时间内只能做一件事。 Call Stack 顶端是 Javascript 当正前在运行的代码,并且称为 Stack,拥有 First In Last out 的特性。如果你写的 Function 没有被 pop 出栈,那其他后续的代码永远没有执行的机会。(然后你会收到 FBI 的红牌警告:Uncaught RangeError: Maximum call stack size exceeded)
function foo () {
foo()
}
foo()
console.log('call me please')
Call Stack 的执行单位是 Task,Task 分两类 Macro Task 和 Micro Task。
Macro Task & Micro Task
(有人翻译叫宏任务和微任务。)
常见的 Task 分类:
Macro Tasks(task): setTimeout, setInterval, setImmediate, I/O, UI rendering
Micro Tasks: process.nextTick, Promises, Object.observe, MutationObserver
Task 执行的位置发生在 Call Stack 上,执行的顺序是这样的:
- 浏览器会先取一个 MacroTask 执行,过程中可能会创造新的 Task (Macro Task 或者 Micro Task)
- 取出所有所有 MicroTask 依次执行,知道当前 MicroTask queue 没有内容
- 取出下一个可执行 MacroTask ,继续第二步
周而复始,这个过程称为 Event Loop。
问题解释
了解了上面这些内容,再来理解为什么执行结果是 1,3,4,6, 5,2
-
全局代码是一个 MacroTask ,开始执行 console.log(1)
console: callstack: console.log(1) macrotasks: [] microtasks:[]
-
遇上 setTimeout,生成一个 Macro Task 交给浏览器
console: 1 callstack: empty macrotasks: [setTimeout] microtasks:[]
-
new Promise 内部代码立即执行,走到 console.log(3)
console: 1 callstack: console.log(3) macrotasks: [setTimeout] microtasks:[]
-
执行 resolve,生成 Micro Task 添加到 Micro Task 队列末端
console: 1, 3 callstack: resolve macrotasksqueue: [setTimeout] microtasksqueue:[promise]
-
继续执行,console.log(4)
console: 1, 3 callstack: console.log(4) macrotasksqueue: [setTimeout] microtasksqueue:[promise]
-
继续执行,console.log(6)
console: 1, 3, 4 callstack: console.log(6) macrotasksqueue: [setTimeout] microtasksqueue:[promise]
-
当前 Call Stack 为空,提取 Micro Task 所有任务开始执行
console: 1, 3, 4, 6 callstack: console.log(5) macrotasksqueue: [setTimeout] microtasksqueue:[]
-
MicorTask 队列为空,取下一个 Macro Task
console: 1, 3, 4, 6, 5 callstack: console.log(2) macrotasksqueue: [] microtasksqueue:[]
-
没有 Macro Task 执行完毕。
console: 1, 3, 4, 6, 5, 2 callstack: empty macrotasksqueue: [] microtasksqueue:[]
拓展
理解这些内容,你对 JS 代码的执行顺序能有更准确的判断,以及阅读别人代码时能会心一笑。
比如 Vue nextTick 的实现。看过文档,你已经知道,Vue nextTick 方法会优先使用 Promise resolve,然后 fallback 到 setTimeout 0。
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
function microTimerFunc () {
const p = Promise.resolve()
p.then(flushCallbacks)
}
function macroTimerFunc () {
setTimeout(flushCallbacks, 0)
}
export function nextTick (cb?: Function, ctx?: Object) {
callbacks.push(() => {
cb.call(ctx)
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
}
已删减兼容代码和无关内容。
在不了解运行顺序的情况下,容易产生困惑,pending 参数是用来干嘛的? 两个 nextTick 同时执行会发生什么?为什么要用 callbacks 把回调存起来,而不是每次 nextTick 执行一次 timerFunc ?现在好像有了比较清晰地认识。
对运行机制一直没有搞清楚,前几天 Arvit 大哥问我为什么使用 async/await 去执行‘复杂运算‘的时候,为什么 UI 会被阻塞,点击按钮无法响应。 由于并且没审好题,犯了理所当然的错误(很丢脸)。现在看来,没有响应是因为‘复杂运算‘占住了 Call Stack,一直没有被处理完。浏览器渲染,以及 DOM 事件回调都在 task queue 里面等待着。
Arvit 这类问题解决方案是:
- 不要用 Javascript 去进行‘复杂运算‘。
- 如果非得前端进行运算,可以尝试使用 Web Worker 在不阻塞页面渲染的情况下去跑。
- 存在递归调用,可以使用 setTimeout 0 执行下一次递归,(不能使用 Promise then,因为 Micro Task Queue 不清理完是不会执行下一个 Macro Task 的)
由于水平有限,也没有阅读过 V8 引擎源码,所以以上大部分内容均来自别人分享的文章。可能有很多认知上的问题,如有错误,请指出。
学习资料
- 强烈推荐,Philip Roberts: What the heck is the event loop anyway? | JSConf EU 2014
- How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await
- What the heck is the event loop anyway.
- Tasks, microtasks, queues and schedules
- 通过microtasks和macrotasks看JavaScript异步任务执行顺序
- Promise的队列与setTimeout的队列有何关联?
如需转载,请注明出处: w3ctrain.com / 2018/02/01/javascript-event-loop/
我叫周晓楷
我现在是一名前端开发工程师,在编程的路上我还是个菜鸟,w3ctrain 是我用来记录学习和成长的地方。