浅析浏览器的Event Loop

前言

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程语言的话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点,这样的话浏览器就知不道要怎么操作了,到底是新增还是删除,很矛盾)。所以 JS 就被设计成单线程语言,之前是这样,今后也是这样。

 

Event Loop

Event Loop 是计算机系统的一种运行机制。

想要理解 Event Loop,就要从程序的运行模式讲起。运行以后的程序叫做"进程"(process),一般情况下,一个进程一次只能执行一个任务。

如果有很多任务需要执行,不外乎三种解决方法:

(1)排队。因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。

(2)新建进程。使用fork命令,为每个任务新建一个进程。

(3)新建线程。因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。

JavaScript 是一种单线程语言,所有任务都在一个线程上完成,即采用上面的第一种方法。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。所以 JavaScript 就采用 Event Loop 这种机制,来解决单线程运行带来的一些问题。

值得注意的是,Event Loop 在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的 Event Loop 。本文讲的是浏览器的 Event Loop,它是在 Html5 的规范中就明确定义了的。

 

浏览器的Event Loop

JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。

console.log('script start')

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

console.log('script end')

输出的结果依次是 script start -> script end -> setTimeout 。有些人可能会觉得很奇怪,我们先抱着这个疑问,继续往下看。

前面说到,执行异步代码的时候(比如setTimeout),异步代码会被分配到不同的 task 里面。这里的 task 一共有两种:微任务(microtask)和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task

宏任务,macrotask,也叫tasks。 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:

  • setTimeout
  • setInterval
  • setImmediate (Node独有)
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

微任务,microtask,也叫jobs。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:

  • process.nextTick (Node独有)
  • Promise
  • Object.observe
  • MutationObserver

(注:这里只针对浏览器和NodeJS)

我先来讲一下浏览器的事件循环 Event Loop 的具体流程:

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
  2. 全局Script代码执行完毕后,调用栈Stack会清空;
  3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行
  5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
  6. UI渲染 (如果有需要的话)
  7. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
  8. 执行完毕后,调用栈Stack为空;
  9. 重复第3-8个步骤;
  10. 重复第3-8个步骤;
  11. ......

记着这个执行流程,回顾一下上面我们留下的疑问(第一个代码例子),是不是就理解了?如果不理解,就把这个流程多看几遍。如果理解了,我们就再来看下面一个例子。

console.log('script start')

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

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

console.log('script end')
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。合着执行顺序,是不是也看懂了?!

要注意的是,很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,而script 属于全局的同步代码,浏览器会先执行这一个宏任务,接下来有异步代码的话就先执行微任务。

Event Loop 有3个重点:

  1. 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  2. 微任务队列中所有的任务都会被依次取出来执行,知道直到microtask queue为空;
  3. 是否需要UI rendering,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。

好了,以上本文章“浅析浏览器的Event Loop”的内容了。如果有什么纰漏或者错误,请大家指出。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值