vlambda博客
学习文章列表

【第2114期】浏览器环境下的JavaScript Event Loop

前言

老话题了。今日早读文章由作者@周雄投稿分享。

正文从这开始~~

什么是JavaScript的Event Loop?

在JavaScript中有一个很重要的Event Loop概念,从字面意思理解为事件循环,为什么会有Event Loop这个概念呢?我们得先从JavaScript语言的特征说起。

细心的读者可能发现了,文章的标题中我们着重强调了是在浏览器环境,这是为什么呢?

因为JavaScript的Event Loop不仅存在于浏览器环境下,还存在于Node环境下,而且两者还存在一些差别。

本篇文章是基于浏览器环境下对于JavaScript Event Loop的讲解。

JavaScript 的单线程

我们都知道JavaScript语言的一大特点就是单线程,这就意味着在同一个时间,只能做一件事情。

为什么JavaScript不做多线程处理呢?还能更加充分利用CPU呢。

这要从JavaScript的使用场景说起,JavaScript作为浏览器语言,主要用途是通过操作DOM,与用户进行互动。

我们设想一下,如果一个线程去更新某个DOM元素,而另一个线程去删除这个DOM元素,那么浏览器该执行哪个操作呢?

为了避免复杂的多线程机制,JavaScript从设计之初就选择了单线程标准,而且未来也不会发生变化。

你可能会问,Web Worker不是允许JavaScript来创建多线程吗?

这个是没错的,但是要注意的是,Web Worker创建的子线程是不能操作DOM的,操作DOM的任务需要交给主线程来执行,而且子线程完全受到主线程的控制,因此Web Worker的提出也并没有改变JavaScript单线程的本质。

既然JavaScript是单线程执行,那么这种运行机制的优点是什么吗?

最大的优点就是,在单线程中运行代码会相当轻松,因为你不用处理多线程环境中出现的一些复杂情况,比如死锁。

但是单线程同样会带来一些问题。当“执行栈”中需要运行耗费大量时间的函数时,例如DOM,AJAX,setTimeout等,由于单线程的机制,浏览器只能等待耗时代码的完成,而不能去做其它的事情,因为它被阻塞了,站在用户的角度,就是浏览器停止渲染,卡住了。

那么,聪明的JS是如何来解决这个问题的呢?

没错,采用异步回调函数的机制,Event Loop事件循环机制也就应运而生。

Event Loop的作用

在JavaScript引擎的主线程上,会包含一个执行栈,按照栈后进先出的顺序处理栈中的任务。

而在执行栈之外,会独立使用一个容器来专门管理异步状态,这个容器就是task queue (任务队列)。

所有异步操作的回调,都会暂时被塞入这个队列。Event Loop 处在两者之间,扮演一个调度者的角色,它会以一个固定的时间间隔不断轮询,当它发现执行栈空闲,就会去到 Task 队列里拿一个异步回调,把它塞入执行栈中执行,一段时间后,主线程执行完成,弹出上下文环境,再次空闲,Event Loop 又会执行同样的操作依次循环,于是构成了一套完整的事件循环运行机制。

microtask 和 macrotask

在任务队列中维护的任务虽然都会因为Event Loop而得到执行,但是不同类型的任务执行的时机是不一样的。

任务队列中的任务会被分为两种:微任务(microtask)和宏任务(macrotask)。

在这里我们可以简单概括下两种任务包含的基本操作。

· 宏任务包括 script脚本, setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。

· 微任务包括 process.nextTick,Promise,MutationObserver,注意process.nextTick是在Node环境中具有的

执行顺序

通过下面这张图,我们来看看宏任务和微任务执行的顺序。

配合这张图,我们看看JavaScript代码的执行过程:

  • 首先执行同步代码,即script脚本,这属于宏任务

  • 当执行完所有同步代码后,执行栈清空

  • 从微任务队列中逐个取出回调任务,放入执行栈中执行,直至所有微任务执行完成。注意:如果在执行微任务的过程中,产生了新的微任务,那么这个微任务会加入到队列的末尾,同样会在这个周期内被执行。

  • 当执行完所有微任务后,如果有必要会开始渲染页面

  • 开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数。

实例验证

在学习完相关的理论知识后,我们通过一些代码实例来检验下大家对event loop的理解。

 
   
   
 
  1. console.log(1);

  2. setTimeout(() => {

  3. console.log(2);

  4. Promise.resolve().then(() => {

  5. console.log(3);

  6. });

  7. });

  8. new Promise((resolve, reject) => {

  9. console.log(4);

  10. resolve(5)

  11. }).then((data) => {

  12. console.log(data);

  13. });

  14. setTimeout(() => {

  15. console.log(6);

  16. });

  17. console.log(7);

首先我们来看看这段代码的输出结果。

1->4->7->5->2->3->6

看看你答对了吗?

然后我们来看看代码的执行流程,包括执行栈Stack Queue,宏任务队列Macrotask Queue和微任务队列Microtask Queue的变化。

1、执行全局的script代码,第1行代码

此时Stack Queue:[console]

Macrotask Queue:[],

Microtask Queue:[]

此阶段输出1。

2、执行到2-7行setTimeout代码,此时会将setTimeout的回调推入Macrotask Queue

此时Stack Queue:[setTimeout本身]

Macrotask Queue:[setTimeout callback],

Microtask Queue:[]

此阶段没有输出。

3、执行8-13行Promise代码,在Promise构造函数内部的代码实际是同步代码,直到遇到resolve或者reject,才会触发异步的回调。

而Promise的异步任务回调会被推入到Microtask Queue。

此时Stack Queue:[Promise内的console],

Macrotask Queue:[setTimeout callback1],

Microtask Queue:[Promise callback]

此阶段输出4。

4、执行14-16行setTimeout代码,与上一个setTimeout一样,会被推入到Macrotask Queue

此时Stack Queue:[setTimeout本身],

Macrotask Queue:[setTimeout callback1,setTimeout callback2],

Microtask Queue:[Promise callback]

此阶段没有输出。

5、执行第17行代码,实际是同步代码,会进入到执行栈中。

此时Stack Queue:[console],

Macrotask Queue:[setTimeout callback1,setTimeout callback2],

Microtask Queue:[Promise callback1]

此阶段输出7。

6、至此第一轮的宏任务执行完成,开始从微任务队列中取出任务执行,直至微任务队列为空。

第10行代码resolve(5)执行后,会进入到then()方法内部,执行第12行代码。

此时Stack Queue:[Promise callback1],

Macrotask Queue:[setTimeout callback1,setTimeout callback2],

Microtask Queue:[]

此阶段输出5。

7、至此微任务队列清空,第一轮微任务也执行完成,开始下一轮event loop

8、执行setTimeout宏任务的回调,并将其推入到执行栈中。

此时Stack Queue:[setTimeout callback1],

Macrotask Queue:[setTimeout callback2],

Microtask Queue:[]

此阶段输出2。

但是在执行到第4行代码时,又生成了一个新的Promise,会将其推入到Microtask Queue中。

Microtask Queue:[promise callback2]。

9、宏任务执行完成后,开始从微任务队列中取出任务执行,即第5行代码

此时Stack Queue:[promise callback2],

Macrotask Queue:[setTimeout callback2],

Microtask Queue:[]

此阶段输出3。

10、第二轮任务轮询结束,开始第三轮event loop

11、执行第二个setTimeout任务回调,即第15行代码。

此时Stack Queue:[setTimeout callback2],

Macrotask Queue:[],

Microtask Queue:[]

此阶段输出6。

12、至此全部执行完成,Stack Queue,Macrotask Queue,Microtask Queue均为空。

关于宏任务和微任务中的setTimeout和Promise,我们再增加一些更加容易让人理解的解释。

setTimeout的作用是等待给定的时间后为它的回调产生一个新的宏任务,这个宏任务将在下一轮循环中执行,而Promise的回调是在当前轮循环中产生,并推入到当前循环的微任务队列中。

这就是为什么打印'2'在'5'之后。因为打印'5'是第一轮微任务里做的事情,而打印'2'是在第二轮的宏任务里做的事情。

通过上面的整体讲述,相信大家对event loop的过程已经有了深入的了解。

下面再给大家留一道题目,看看大家能不能答对,这其中增加了async-await关键字。

 
   
   
 
  1. console.log('script start');


  2. async function async1() {

  3. await async2();

  4. console.log('async1 end')

  5. }


  6. async function async2() {

  7. console.log('async2 end');

  8. }


  9. async1();


  10. setTimeout(function() {

  11. console.log('setTimeout');

  12. }, 0);


  13. new Promise(resolve => {

  14. console.log('Promise');

  15. resolve();

  16. })

  17. .then(function() {

  18. console.log('promise1');

  19. })


  20. .then(function() {

  21. console.log('promise2');

  22. });


  23. console.log('script end');

在这里也给出参考答案吧,看看你答对了没?

 
   
   
 
  1. script start -> async2 end -> Promise -> script end -> async1 end-> promise1 -> promise2 -> setTimeout

这里给大家一个提示,await在执行async函数时是会有阻塞性的,所以'async2 end'会出现在'Promise'之前。

为你推荐






欢迎自荐投稿,前端早读课等你来



在《javascript重难点实例精讲》这本书里,都是形如这种方式的内容讲解,不仅包含理论知识的描述,还会通过丰富的实例来做验证,详细讲解各种代码的执行过程,还会适当的留一些题目给你当练习,让你真正从原理上去学会它。