如何理解JS中的事件循环(Event Loop)
首先,我们先看一段代码:
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve()
.then(function() {
console.log('promise-1')
})
.then(function() {
console.log('promise-2')
})
console.log('script end')
以上代码输出顺序是什么呢?
正确答案是:
script start
script end
promise-1
promise-2
setTimeout
为什么会这样?
要了解这一点,我们先聊聊几个概念:
下图是运行时概念的模拟图:堆(heap)、栈(stack)、队列(quene)和帧(frame)
帧
一个帧是一个连续的工作单元。在上面的图示中,帧表现为栈中的小块。
当一个Javascript函数被调用时,运行时环境就会在栈中创建一个帧。帧里保存了特殊函数的参数和局部变量。当函数返回时,帧就从栈中弹出。我们看一个例子:
function foo(b) {
var a = 12
return a + b + 35
}
function bar(x) {
var m = 4
return foo(m * x)
}
执行bar函数:
bar(21)
当bar被执行时,运行时会创建一个包含bar的参数和所有局部变量的帧。这个帧(frame)被添加到了栈(stack)的顶部。
bar函数内部调用了foo函数。当foo函数被调用时,栈的顶部就又创建了一个新的帧。当foo执行完毕,栈顶部对应的帧也就被移除。当bar函数执行完毕后,响应的帧也就被移除。
如果foo中又调用了bar函数,那么就创建了一个无限的函数调用循环。每调用一次,一个新的帧就被添加到栈的顶部,最终,栈被填满,然后,抛出一个程序员都熟知的错误:栈溢出错误。
栈
栈是一种先进后出的数据结构。由于栈是先进后出的集合,所以事件循环会从上至下的处理栈中的帧。单帧所依赖的其他帧,将会被添加在此帧的上面,以保证它从栈中可以获取到需要依赖的信息。
队列
队列是一种先进先出的数据结构。它包含一个待执行信息的列表,每个信息都与一个函数相互联系。当栈为空时,队列的一条信息就会被取出并且被处理。处理的过程为调用该信息所关联的函数,然后将此帧添加到栈的顶部。当栈再次为空时,本次信息处理即视为结束。
堆
堆是一个内存存储空间,它不关注内部存储内容的保存顺序。堆中保存了所有正在被使用的变量和对象。同时也保存了一些当前作用域已经不再会用到,但是还没被垃圾回收的帧。
Event Loop(事件循环)
在Javascript中,任务被分为两种,一种是宏任务(MacroTasks),也称Tasks;一种是微任务(MicroTasks)。
MacroTasks
MacroTasks是一个计划表,以便安排浏览器内部访问Javascript/DOM,并保证这些操作的顺序发生。在任务中,浏览器可以呈现更新。
它包括 script 全部代码,setTimeout 、setInterval 、setImmediate 、I/O 、UI Rendering 。
MicroTasks
通常,MicroTasks 是为应在当前正在执行的脚本之后立即发生的事情安排的。
它包括:process.nextTick (node)、Promise 、Object.observe、MutationObserver 。
现在我们再看下开头的代码,一步一步的运行,看看发生了什么?
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve()
.then(function() {
console.log('promise-1')
})
.then(function() {
console.log('promise-2')
})
console.log('script end')
第一步
Tasks |
Run script、setTimeout callback |
MicroTasks |
Promise then |
JS stack |
script |
Log |
script start、script end |
执行同步代码,将宏任务和微任务划分到各自的队列中。
第二步
Tasks |
Run script、setTimeout callback |
MicroTasks |
Promise then |
JS stack |
Promise callback |
Log |
script start、script end、promise-1 |
宏任务执行完毕后,我们开始执行微任务,微任务执行完成Promise1后,将Promise2放入微任务队列中,在执行Promise2。
第三步
Tasks |
Run script、setTimeout callback |
MicroTasks |
Promise2 then |
JS stack |
Promise2 callback |
Log |
script start、script end、promise-1、promise-2 |
第四步
Tasks |
setTimeout callback |
MicroTasks |
|
JS stack |
|
Log |
script start、script end、promise-1、promise-2 |
至此,第一个Tasks(script) 已经执行完毕,浏览器开始渲染。
第五步
Tasks |
setTimeout callback |
MicroTasks |
|
JS stack |
setTimeout callback |
Log |
script start、script end、promise-1、promise-2、setTimeout |
最后,执行第二个Tasks,并打印setTimeout
最后
在知乎上看到一个比较好的题目,那么正确输出顺序是什么呢?
tips: async/await 是Promise 的语法糖。async 是异步的简写。而await 是async wait 的简写,可以认为是等待异步方法执行完成。也就是说:
async function test() {
await t
console.log('hello')
}
// 可以写成
function test() {
return Promise.resolve(t).then(function() {
console.log('hello')
})
}
// 也就是说 t 会立即被调用
题目:
console.log('script start')
async function async0() {
await async1()
console.log('async0 end')
}
async function async1() {
console.log('async1 end')
}
async0()
setTimeout(function() {
console.log('setTimeout')
})
new Promise(resolve => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise0')
}).then(function() {
console.log('promise1')
})
console.log('script end')
// output
/*
script start
async1 end
Promise
script end
async0 end
promise0
promise1
setTimeout
*/
点击查看全文即可查看原文链接