谈谈浏览器的 Event Loop


关于 Event Loop 网络上的文章写得很详尽,多看几篇反复思考勤动笔记录大致就能明白些了,这也是我一贯的学习方法。

在了解 Event Loop 之前先理解什么是执行栈(调用栈),记住,执行栈是存储 函数调用 的栈结构,遵循先进后出的原则(我因为这一点没想明白所以花了好多时间 😭)

进程与线程

先看下这个形象的类比

把计算机的核心 CPU 比作一座时刻运行的工厂。

进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。『进程是 CPU 资源分配的最小单位』

线程就好比车间里的工人,车间的房间(内存资源)对于工人共享的,这些工人协同完成一个任务。『线程是 CPU 调度的最小单位』

单线程的 JavaScript

所谓单线程,是指在 JavaScript 引擎中负责解释和执行 JavaScript 代码的线程唯一,同一时间上只能执行一件任务。假设 JavaScript 支持多线程,当一个线程在某 DOM 节点上添加内容,而另一个线程同时执行删除该 DOM 节点的任务,这时就会把浏览器搞懵了,所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征。

同步任务和异步任务

既然 JavaScript 是单线程的,那么当有多个任务则需要排队执行,如果前一个任务耗时很长,后一个任务就不得不一直等着,那么程序可能因为等待会出现假死状态,这对于一个用户体验很强的语言来说是非常不友好的。

为了解决这个问题,JavaScript 语言将任务的执行模式分为两种:同步和异步。

同步任务:必须等到结果来了之后才能做其他的事情,举例来说就是逛街时买衣服,你必须付了款才能把衣服带走。

异步任务:不需要等到结果来了才能继续往下走,等结果期间可以做其他的事情,结果来了会收到通知。举例来说就是要吃火锅,要排号等叫到了号才能进去吃,排号期间你可以继续逛街。

执行栈和任务队列

JavaScript 的代码执行时:

  1. 主线程会从上到下一步步的执行代码,同步任务会被依次加入执行栈中先执行。

  2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。

  3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

  4. 主线程不断重复上面的第三步。

宏任务和微任务

那么问题来了,如果在执行异步任务回调事件的过程中突然有重要的数据需要获取,或是说有事件突然需要处理一下,按照队列遵循先进先出的原则,后来的事件都是被加在队尾等到前面的事件执行完了才会被执行。这个时候就催生了宏任务和微任务,微任务使得一些异步任务得到及时的处理。

也就是在上文 『2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件』 这一步中,将微任务放到本层循环的微任务队列(优先级高),将宏任务放到下层循环的宏任务队列(优先级低)。

这里也更正一下上文 『3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。』 ,应当是 『一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"微任务任务队列"并将事件压入执行栈中执行至"微任务任务队列"为空(到这里也就是完成了一轮事件循环),完毕后将"宏任务队列"队头的第一个宏任务压入执行栈执行(后面就对应了上文步骤1、步骤2,有相关的微任务就进入微任务队列,宏任务就插到宏任务队列队尾)

任务队列.png

关于宏任务和微任务这有个例子解释得很形象。

记住:在当前的微任务没有执行完成时,是不会执行下一个宏任务的

先介绍一下(浏览器环境的)宏任务和微任务大致有哪些:

  • 宏任务

    • script全部代码
    • setTimeout
    • setInterval
    • I/O
    • mouseover(之类的事件)
    • Web API大部分异步返回方法(XHR,fetch)
  • 微任务

    • Promise.then catch finally
    • MutationObserver
    • queueMicrotask

浏览器中的 Event Loop

终于进入正题了,看前面的文字可能还有些晕乎,现在我们先来通过代码理解。

console.log('script start');

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(function () {
    console.log('promise1');
  })
  .then(function () {
    console.log('promise2');
  });

console.log('script end')

复制这段代码到控制台,会发现输出顺序为:

script start
script end
promise1
promise2
setTimeout

现在,联系上文提及的 执行栈宏任务微任务 来解释一下。

主线程会从上到下一步步的执行代码,同步任务会被依次加入执行栈中先执行,而异步任务会在任务队列中放置一个回调事件,等待执行栈清空后执行。而根据事件的优先级将事件划分到对应的宏任务和微任务队列中。

宏任务:Run script | setTimeout callback 

微任务:Promise then | Promise then

执行栈:Promise callback

控制台输入:script start | script end | promise1 | promise2

第一次执行

到这里事件循环的第一层循环结束,开始第二层循环

宏任务:setTimeout callback 

微任务:

执行栈:setTimeout callback

控制台输入:script start | script end | promise1 | promise2 | setTimeout

第二次执行.gif

动图体验戳这里

再来看一个比较复杂的案例

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()

setTimeout(function() {
    console.log('setTimeout1')
    new Promise(function(resolve) {
        console.log('setTimeout1 Promise')
        resolve();
    }).then(function() {
        console.log('setTimeout1 Promise then')
    })
    setTimeout(function() {
        console.log('setTimeout1 setTimeout');
        new Promise(function(resolve) {
            console.log('setTimeout1 setTimeout Promise')
            resolve();
        }).then(function() {
            console.log('setTimeout1 setTimeout Promise then')
        })
    })
})

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
.then(function() {
  console.log('Promise then')
})

setTimeout(function() {
    console.log('setTimeout2')
    new Promise(function(resolve) {
        console.log('setTimeout2 Promise')
        resolve();
    }).then(function() {
        console.log('setTimeout2 Promise then')
    })
})
console.log('script end')

把代码copy到浏览器控制台运行可以看到如下输出

运行结果.png

用上面同样的思路解释下

第一次循环

宏任务	Run script | setTimeout1 callback | setTimeout2 callback			
微任务	async1 end | Promise then				
						
控制台输出	script start | async2 end | Promise | script end | async1 end | Promise then

第二次循环

宏任务	setTimeout1 callback | setTimeout2 callback | setTimeout1 setTimeout callback	
微任务	setTimeout1 Promise then			
				
控制台输出	setTimeout1 | setTimeout1 Promise | setTimeout1 Promise then

第三次循环

宏任务	setTimeout2 callback | setTimeout1 setTimeout callback		
微任务	setTimeout2 Promise then			
				
控制台输出	setTimeout2 | setTimeout2 Promise | setTimeout2 Promise then

第四次循环

宏任务	setTimeout1 setTimeout callback		
微任务	setTimeout1 setTimeout Promise then		
			
控制台输出	setTimeout1 setTimeout | setTimeout1 setTimeout Promise | setTimeout1 setTimeout Promise then

本来还要再写 Node.js 中的 Event Loop 的,但是因为时间关系而且我发现我的理解还是有些模糊,这个就留着搞明白了再写写~

参考文章:

JavaScript 运行机制详解:再谈Event Loop

我是这样理解EventLoop的

一次弄懂Event Loop(彻底解决此类面试问题)

这一次,Event Loop 一波带走


文章作者: April-cl
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 April-cl !
  目录