资讯专栏INFORMATION COLUMN

什么是浏览器的事件循环(Event Loop)?

JerryC / 744人阅读

摘要:本文围绕浏览器的事件循环,而有自己的另一套事件循环机制,不在本文讨论范围。现在我们知道了浏览器运行时有一个叫事件循环的机制。将事件循环的当前运行任务设置为。对于相应事件循环的每个环境设置对象通知它们哪些为。

本文围绕浏览器的事件循环,而node.js有自己的另一套事件循环机制,不在本文讨论范围。网上的许多相关技术文章提到了process.nextTicksetImmediate两个node.js的API,这里不予讨论。

先看HTML标准的一系列解释:

为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。

有两类事件循环:一种针对浏览上下文(browsing context),还有一种针对worker(web worker)。

现在我们知道了浏览器运行时有一个叫事件循环的机制。

一个事件循环有一个或者多个任务队列(task queues)。任务队列是task的有序列表,这些task是以下工作的对应算法:Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation。

每一个任务都来自一个特定的任务源(task source)。所有来自一个特定任务源并且属于特定事件循环的任务,通常必须被加入到同一个任务队列中,但是来自不同任务源的任务可能会放在不同的任务队列中。

举个例子,用户代理有一个处理鼠标和键盘事件的任务队列。用户代理可以给这个队列比其他队列多3/4的执行时间,以确保交互的响应而不让其他任务队列饿死(starving),并且不会乱序处理任何一个任务队列的事件。

每个事件循环都有一个进入microtask检查点(performing a microtask checkpoint)的flag标志,这个标志初始为false。它被用来组织反复调用‘进入microtask检查点’的算法。

总结一下,一个事件循环里有很多个任务队列(task queues)来自不同任务源,每一个任务队列里的任务是严格按照先进先出的顺序执行的,但是不同任务队列的任务的执行顺序是不确定的。按我的理解就是,浏览器会自己调度不同任务队列。网上很多文章会提到macrotask这个概念,其实就是指代了标准里阐述的task

标准同时还提到了microtask的概念,也就是微任务。看一下标准阐述的事件循环的进程模型:

选择当前要执行的任务队列,选择一个最先进入任务队列的任务,如果没有任务可以选择,则会跳转至microtask的执行步骤。

将事件循环的当前运行任务设置为已选择的任务。

运行任务。

将事件循环的当前运行任务设置为null。

将运行完的任务从任务队列中移除。

microtasks步骤:进入microtask检查点(performing a microtask checkpoint )。

更新界面渲染。

返回第一步。

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

设置进入microtask检查点的标志为true。

当事件循环的微任务队列不为空时:选择一个最先进入microtask队列的microtask;设置事件循环的当前运行任务为已选择的microtask;运行microtask;设置事件循环的当前运行任务为null;将运行结束的microtask从microtask队列中移除。

对于相应事件循环的每个环境设置对象(environment settings object),通知它们哪些promise为rejected。

清理indexedDB的事务。

设置进入microtask检查点的标志为false。

现在我们知道了。在事件循环中,用户代理会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行...

那么哪些行为属于task或者microtask呢?标准没有阐述,但各种技术文章总结都如下:

macrotasks: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering

microtasks: process.nextTick, Promises, Object.observe(废弃), 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");

(代码来自Tasks, microtasks, queues and schedules,推荐观看原文的代码可视化执行步骤

如果你测试的浏览器支持的Promise不支持Promise/A+标准,或是你使用了其他Promise polyfill,运行结果可能有差异。

运行结果是:

script start
script end
promise1
promise2
setTimeout

解释一下过程。

一开始task队列中只有script,则script中所有函数放入函数执行栈执行,代码按顺序执行。

接着遇到了setTimeout,它的作用是0ms后将回调函数放入task队列中,也就是说这个函数将在下一个事件循环中执行(注意这时候setTimeout执行完毕就返回了)。

接着遇到了Promise,按照前面所述Promise属于microtask,所以第一个.then()会放入microtask队列。

当所有script代码执行完毕后,此时函数执行栈为空。开始检查microtask队列,此时队列不为空,执行.then()的回调函数输出"promise1",由于.then()返回的依然是promise,所以第二个.then()会放入microtask队列继续执行,输出"promise2"。

此时microtask队列为空了,进入下一个事件循环,检查task队列发现了setTimeout的回调函数,立即执行回调函数输出"setTimeout",代码执行完毕。

继续看一个更有趣的例子:

HTML代码:

JavaScript代码:

// Let"s get hold of those elements
var outer = document.querySelector(".outer");
var inner = document.querySelector(".inner");

// Let"s listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log("mutate");
}).observe(outer, {
  attributes: true
});

// Here"s a click listener…
function onClick() {
  console.log("click");

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

  Promise.resolve().then(function() {
    console.log("promise");
  });

  outer.setAttribute("data-random", Math.random());
}

// …which we"ll attach to both elements
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);

(代码来自Tasks, microtasks, queues and schedules,推荐观看原文的代码可视化执行步骤
点击内框后,结果如下:

click
promise
mutate
click
promise
mutate
timeout
timeout

解释一下过程:
点击inner输出"click",Promise和设置outer属性会依次把Promise和MutationObserver推入microtask队列,setTimeout则会推入task队列。此时执行栈为空,虽然后面还有冒泡触发,但是此时microtask队列会先执行,所以依次输入"promise"和"mutate"。接下来事件冒泡再次触发事件,过程和开始一样。接着代码执行完毕,此时进入下一次事件循环,执行task队列中的任务,输出两个"timeout"。

好了,如果你理解了这个,那么现在换一下事件触发的方式。在上面的代码后面加上

inner.click()

思考看看会有什么不同。

运行结果:

click
click
promise
mutate
promise
timeout
timeout

造成这个差异的结果是什么呢?因为第一次执行完第一个click事件后函数执行栈并不为空。
具体代码运行解释,可以查看Tasks, microtasks, queues and schedules。

本文参考:
html.spec.whatwg.org
difference-between-javascript-macrotask-and-microtask
Event loop

墙裂建议大家阅读HTML标准里阐述的Event Loop,欢迎指正和建议。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87228.html

相关文章

  • 一篇文章教会你Event loop——览器和Node

    摘要:如果没到毫秒,那么阶段就会跳过,进入阶段,先执行的回调函数。参考文档什么是浏览器的事件循环不要混淆和浏览器中的定时器详解浏览器和不同的事件循环深入理解事件循环机制篇中的执行机制 最近对Event loop比较感兴趣,所以了解了一下。但是发现整个Event loop尽管有很多篇文章,但是没有一篇可以看完就对它所有内容都了解的文章。大部分的文章都只阐述了浏览器或者Node二者之一,没有对比...

    Leck1e 评论0 收藏0
  • 彻底搞懂览器Event-loop

    摘要:检查宏任务队列,发现有的回调函数立即执行回调函数输出。接着遇到它的作用是在后将回调函数放到宏任务队列中这个任务在再下一次的事件循环中执行。 为什么会写这篇博文呢? 前段时间,和头条的小伙伴聊天问头条面试前端会问哪些问题,他称如果是他面试的话,event-loop肯定是要问的。那天聊了蛮多,event-loop算是给我留下了很深的印象,原因很简单,因为之前我从未深入了解过,如果是面试的时...

    source 评论0 收藏0
  • JavaScript 事件循环(译文JavaScript Event Loop

    摘要:事件循环了解了在引擎中是如何工作了之后,来看下如何使用异步回调函数来避免代码。从回调函数被放入后秒钟,把移到中。由于事件循环持续地监测调用栈是否已空,此时它一注意到调用栈空了,就调用并创建一个新的调用栈。 听多了JavaScript单线程,异步,V8,便会很想去知道JavaScript是如何利用单线程来实现所谓的异步的。我参考了一些文章,了解到一个很重要的词汇:事件循环(Event L...

    K_B_Z 评论0 收藏0
  • 关于览器Event Loop

    摘要:的宿主最开始本身就是浏览器,处理用户的交互事件。既然是单线程的,那就意味着任务需要排队,只有前一个任务执行完毕,下一个任务才能开始,于是就有了任务队列。事件循环有两种用于浏览上下文的事件循环和用于的事件循环。 最近看到Event Loop这个词出现的频率有点高,于是查阅各方资料在此记录一下。 先不说概念,我们来看段代码: console.log(script start); setT...

    youkede 评论0 收藏0
  • JavaScript执行机制、事件循环

    摘要:曾经的理解首先,是单线程语言,也就意味着同一个时间只能做一件事,那么为什么不是多线程呢这样还能提高效率啊假定同时有两个线程,一个线程在某个节点上编辑了内容,而另一个线程删除了这个节点,这时浏览器就很懵逼了,到底以执行哪个操作呢所以,设计者把 Event Loop曾经的理解 首先,JS是单线程语言,也就意味着同一个时间只能做一件事,那么 为什么JavaScript不是多线程呢?这样还能提...

    rose 评论0 收藏0
  • 10分钟理解JS引擎执行机制

    摘要:深入理解引擎的执行机制灵魂三问为什么是单线程的为什么需要异步单线程又是如何实现异步的呢中的中的说说首先请牢记点是单线程语言的是的执行机制。 深入理解JS引擎的执行机制 1.灵魂三问 : JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 2.JS中的event loop(1) 3.JS中的event loop(2) 4.说说setTimeout 首先,请牢记2...

    zzbo 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<