评论

从一道题目窥探事件循环机制

JS的事件循环机制决定了JS代码执行的顺序,你了解时间循环机制吗?

本文中提到的事件循环机制只针对于浏览器。浏览器和 Node 的事件循环机制是不同的,Node的事件循环是基于libuv实现的,感兴趣的可自行了解。

开篇先来道题目吧:这位同学,请把下面代码在浏览器控制台的输出顺序手写一下。

console.log('1');

setTimeout(()=>{
    console.log('2');
}, 0);

new Promise((resolve)=>{
    console.log('3');
    resolve();
}).then(()=>{
    console.log('4');
})

setTimeout(()=>{
	console.log('5')
    new Promise((resolve)=>{
        console.log('6')
        resolve();
    }).then(()=>{
        console.log('7')
    })
    console.log('8')
},0)

console.log('9')

几个重要的概念

1. 函数调用栈(Call stack)

函数调用栈的作用:用于记录函数调用。

在执行的一个函数时,会将该函数 push 到调用栈顶,调用完成后将栈顶函数 pop 弹出。同理,如果一个函数调用期间调用了子函数,则该子函数会被push到调用栈顶,然后执行。

2. Web APIs

浏览器提供了多种异步的Web API,如DOM,times(计时器),AJAX等。

当我们调用一个 Web API 时,如 setTimeout,setTimeout函数会被 push 调用栈顶然后执行,但是 setTimeout 的回调函数不会立即被 push 到调用栈顶,而是起一个计时器任务。当这个计时器结束时,该回调函数会被塞到任务队列(CallBack Queue)中。这个队列中的回调函数的调用就是由事件循环机制来控制的。

3. 任务队列(CallBack Queue)

任务队列可分为两类:

  • 宏任务(macro-task):script(整体代码),setTimeout/setInterval
  • 微任务(micro-task):Promise, Object.observe(已废弃)

任务源:setTimeout/setInterval, Promise这些属于任务源,不同的任务源会进入不同的任务队列

什么是事件循环机制

事件循环机制决定了Javascript代码执行的顺序

众所周知,Javascript是单线程的。在整个代码执行期间,会执行各种同步和异步的代码,保证其代码的执行顺序就很重要了。而事件循环机制就是干这个事的。

事件循环机制的简要流程如下:

题目分析

第一轮事件循环

首先,执行全局代码(script):

  1. 输出 1
  2. 遇到第一个 setTimeout, setTimeout 函数是立即执行的,而 Web API 的计时器会给其回调函数起个计时器,0 ms后将该回调函数插入 macro-task队列。
  3. 遇到第一个Promise, Promise的回调函数是立即执行的,输出**3**,并且 resolve。此时 then回调进入micro-task队列
  4. 遇到第二个 setTimeout,经过 Web API 计时器后(0 ms),其回调函数插入macro-task队列
  5. 输出 9
  6. 全局代码执行完毕

然后,检查micro-task是否有待执行的微任务。步骤3then 回调在micro-task中,取出执行。

  1. 输出 4

然后,检查micro-task是否有待执行的微任务。此时发现没有了,第一轮事件循环结束

第二轮事件循环

首先,取出macro-task队头的 **第一个setTimeout回调函数 **执行:

  1. 输出 2

然后,检查micro-task是否有待执行的微任务。此时发现没有了,第二轮事件循环结束

第三轮事件循环

首先,取出macro-task队头的 **第二个setTimeout回调函数 **执行:

  1. 输出 5
  2. 遇到第二个Promise,输出 6, 并且 resolve,此时then回调函数进入micro-task队列
  3. 输出 8

然后,检查micro-task是否有待执行的微任务。发现有个 then 回调在micro-task中,取出执行。

  1. 输出 7

输出结果

1,3,9,4,2,5,6,8,7

把问题升级一下

让我们来加入 async,看看输出结果如何:

async function async1(){
	console.log('async1')
}

console.log('1');

setTimeout(()=>{
    console.log('2');
}, 0);

new Promise((resolve)=>{
    console.log('3');
    resolve();
}).then(()=>{
    console.log('4');
})

setTimeout(()=>{
	console.log('5')
    new Promise((resolve)=>{
        console.log('6')
        resolve();
    }).then(()=>{
        console.log('7')
    })
    console.log('8')
},0)

await async1();

console.log('9')

其实async只是一个语法糖,其返回值是一个Promise,上面代码中 await 语句后面的代码其实相当于 Promisethen回调,与下面代码等同:

async function async1(){
	console.log('async1')
}

console.log('1');

setTimeout(()=>{
    console.log('2');
}, 0);

new Promise((resolve)=>{
    console.log('3');
    resolve();
}).then(()=>{
    console.log('4');
})

setTimeout(()=>{
	console.log('5')
    new Promise((resolve)=>{
        console.log('6')
        resolve();
    }).then(()=>{
        console.log('7')
    })
    console.log('8')
},0)

// await 部分的代码与下面代码等同
new Promise((resolve)=>{
    async1();
    resolve();
}).then(()=>{
    console.log('9')
})

输出结果

// 1,3,async1,4,9,2,5,6,8,7

总结

浏览器中事件循环的大致流程:

  1. 选取marco-task队头的回调函数执行(第一轮为 script 全局代码)
  2. 执行完后执行micro-task队列中所有微任务,若没有微任务则跳过
  3. 一轮事件循环结束,重复1,2,3

以上总结的是我自己对事件循环机制的理解,如有描述的不对的地方,请不要吝啬指出错误哈,大家共同学习进步!

参考链接

点赞 0
收藏
评论
登录 后发表内容