如何理解JS中的事件循环(Event Loop)

Posted Javascript每日一题

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何理解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 startscript endpromise-1promise-2setTimeout


为什么会这样?


要了解这一点,我们先聊聊几个概念:


下图是运行时概念的模拟图:堆(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/awaitPromise 的语法糖。async 是异步的简写。而awaitasync 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*/


点击查看全文即可查看原文链接

以上是关于如何理解JS中的事件循环(Event Loop)的主要内容,如果未能解决你的问题,请参考以下文章

事件循环Event Loop--JS篇

js执行机制:event loop(多图理解)

深入理解JavaScript的事件循环(Event Loop)

高级前端养成39js专精07之Event Loop

JavaScript:彻底理解同步异步和事件循环(Event Loop) (转)

深入理解javascript中的事件循环event-loop