关于浏览器:事件循环Event-loop到底是什么

44次阅读

共计 3691 个字符,预计需要花费 10 分钟才能阅读完成。

摘要:本文通过联合官网文档 MDN 和其余博客深刻解析浏览器的事件循环机制,而 NodeJS 有另一套事件循环机制,不在本文探讨范畴中。process.nextTick 和 setImmediate 是 NodeJS 的 API,所以本文也不予探讨。

首先,先理解几个概念。

Javascript 到底是单线程还是多线程语言?


Javascript 是一门单线程语言。 置信应该有不少敌人对于 Javascript 是单线程语言还有些疑难(题外话:之前在某次面试中遇到一个面试官,一来就是“咱们晓得 JS 是一门多线程语言。。。”巴拉巴拉,过后就把我给愣住了。),不是有 Web Worker 能够创立多个线程吗?答案就是,Javascript 是单线程的,然而他的运行环境不是单线程 。要如何了解这句话,首先得从 Javascript 运行环境比方浏览器的多线程说起。

浏览器通常蕴含以下线程:

  1. GUI 渲染线程

    • 次要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等。
    • 当界面须要重绘或者因为某种操作引发回流时,将执行该线程。
    • 该线程与 JS 引擎线程互斥,当执行 JS 引擎线程时,GUI 渲染会被挂起。
  2. JS 引擎线程

    • 该线程负责解决 Javascript 脚本,执行代码。
    • 负责执行待执行的事件,比方定时器计数完结,或者异步申请胜利并正确返回时,将顺次进入工作队列,期待 JS 引擎线程执行。
    • 该线程与 GUI 线程互斥,当 JS 线程执行 Javascript 脚本事件过长,将导致页面渲染的阻塞。
  3. 定时器触发线程

    • 负责执行异步定时器一类函数的线程,如:setTimeout,setInterval。
    • 主线程顺次执行代码时,遇到定时器会将定时器交给该线程解决,当计数结束后,事件触发线程会将计数结束的事件回调退出到工作队列,期待 JS 引擎线程执行。
  4. 事件触发线程

    • 次要负责将期待执行的事件回调交给 JS 引擎线程执行。
  5. 异步 http 申请线程

    • 负责执行异步申请一类函数的线程,如:Promise,axios,ajax 等。
    • 主线程顺次执行代码时,遇到异步申请,会将函数交给该线程解决,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数退出到工作队列,期待 JS 引擎线程执行。

Web Worker 是浏览器为 Javascript 提供的一个能够在浏览器后盾开启一个新的线程的 API(相似下面说到浏览器的多个线程),使 Javascript 能够在浏览器环境中多线程运行,但这个多线程是指浏览器自身,是它在负责调度治理 Javascript 代码,让他们在失当机会执行。所以 Javascript 自身是不反对多线程的。

异步


Javascript 的异步过程通常是这样的:

  1. 主线程发动一个异步申请,异步工作承受申请并告知主线程已收到(异步函数返回);
  2. 主线程继续执行后续代码,同时异步操作开始执行;
  3. 异步操作执行实现后告诉主线程;
  4. 主线程收到告诉后,执行异步回调函数。

这个过程有个问题,异步工作各工作的执行工夫过程长短不同,执行实现的工夫点也不同,主线程如何调控异步工作呢?这就引入了音讯队列。

栈、堆、音讯队列


:函数调用造成的一个由若干帧组成的栈。

:对象被调配在堆中,堆是一个用来示意一大块(通常是非结构化的)内存区域。

音讯队列 :一个 Javascript 运行时蕴含了一个待处理音讯的音讯队列。每一个音讯都关联着一个用来解决这个音讯的回调函数。在事件循环期间,运行时会从最先进入队列的音讯开始解决,被解决的音讯会被移出队列,并作为输出参数来调用与之关联的函数。而后事件循环在解决队列中的下一个音讯。

事件循环 Event loop


理解了上述要点,当初回到主题事件循环。那么 Event loop 到底是什么呢?

Event loop 是一个执行模型,在不同的中央有不同的实现。浏览器和 NodeJS 基于不同的技术实现了各自的 Event loop。
当初明确为什么要把 NodeJS 排除在外了吧?同样网上很多 Event loop 的相干博文一来就是 Javascript 的 Event loop,实际上说的都是浏览器的 Event loop。
浏览器的 Event loop 是在 Html5 标准中定义的,大抵总结如下:

一个事件循环里有很多个工作队列(task queues)来自不同工作源,每一个工作队列里的工作(task)都是严格依照先进先出的程序执行的,然而不同工作队列的工作执行程序是不确定的,浏览器会本人调度不同工作队列。也有中央把 task 称之为 macrotask(宏工作)。

标准中还提到了 microtask(微工作)的概念,以下是标准论述的过程模型:

  1. 抉择以后要执行的工作队列,抉择一个最先进入工作队列的工作,如果没有工作能够抉择,则会跳转至 microtask 的执行步骤;
  2. 将事件循环的以后运行工作设置为已抉择的工作;
  3. 运行工作;
  4. 将事件循环的当前任务设置为 null,将运行完的工作从工作队列中移除;
  5. microtask 步骤:进入 microtask 检查点;
  6. 更新界面渲染;
  7. 返回第一步。

执行进入 microtask 检查点时,用户代理会执行以下步骤:

  1. 设置进入 microtask 检查点的标记为 true;
  2. 当事件循环的微工作队列不为空时:抉择一个最先进入 microtask 队列的 microtask,设置事件循环以后运行工作为此 microtask;
  3. 运行 microtask;
  4. 设置事件循环以后运行工作为 null,将运行完结的 microtask 从 microtask 队列中移除;
  5. 对于相应事件循环的每个环境设置对象,告诉它们哪些 promise 为 rejected;
  6. 清理 indexedDB 的事务;
  7. 设置进入 microtask 检查点的标记为 false。

由上可总结为: 在事件循环中,用户代理会一直从 task 队列中按程序取 task 执行,每执行完一个 task 都会查看 microtask 队列是否为空(执行完一个 task 的具体标记时函数执行栈为空),如果不为空则会一次性执行完所有 microtask。而后再进入下一个循环去 task 队列中取下一个 task 执行。

task/macrotask(宏工作)

  • script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI rendering

microtask(微工作)

  • Promise.then catch finally
  • MutationObserver

来看一个例子:

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 

那么问题来了,不是说每个事件循环开始会从 task 队列取最先进入的 task 执行,而后再执行所有 microtask 吗?为什么 setTimeout 是 task 却在 Promise.then 这个 task 的后面呢?反正我一开始是有这个纳闷的,很多文章都没有说分明这个具体执行的程序,大部分都是在形容标准的时候说的是“每个事件循环开始会从 task 队列中取一个 task 执行,而后再执行所有 microtask”,然而也有局部文章说的是“每个事件循环开始都是先执行所有 microtask”。通过自己多方查证,标准里的形容如上的确就是每个事件循环都是先执行 task,那为什么下面例子外面体现进去的是先执行所有 microtask 呢?

script(整体代码)属于 task。

来看一下下面例子的具体执行过程:

  1. 事件循环开始,task 队列中只有一个 script,抉择 script 作为事件循环的已抉择工作;
  2. script 按程序执行,同步代码间接输入(script start、script end);
  3. 遇到 setTimeout,0ms 后将回调函数放入 task 队列;
  4. 遇到 Promise,将第一个 then 的回调函数放入 microtask 队列;
  5. 当所有 script 代码执行实现后,此时函数执行栈为空,开始查看 microtask 队列,队列只有第一个.then 的回调函数,执行输入“promise1”,因为第一个.then 返回的仍然是 promise,所以第二个.then 的回调会放入 microtask 队列继续执行,输入“promise2”;
  6. 此时 microtask 队列空了,进入下一个事件循环,查看 task 队列取出 setTimeout 回调函数,执行输入“setTimeout”,代码执行实现。

这样是不是分明了?所以实际上一开始执行 script 代码的时候就曾经开始事件循环了,这就解释了为什么如同每次都是先执行所有的 microtask。同时,这个例子中还引申出一个要点: 在执行 microtask 工作的时候,如果又产生了新的 microtask,那么会持续增加到队列的开端,且也会在这个事件循环周期执行,直到 microtask 队列为空为止。

正文完
 0