资讯专栏INFORMATION COLUMN

你不得不知的Event Loop

call_me_R / 690人阅读

摘要:具体的可以用下面的图来大致说明一下同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入。主线程内的任务执行完毕为空,会去读取对应的任务,推入主线程执行。

前言

众所周知,JavaScript是一门单线程语言,虽然在html5中提出了Web-Worker,但这并未改变JavaScript是单线程这一核心。可看HTML规范中的这段话:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,用户引擎必须使用event loops。Event Loop包含两类:一类是基于Browsing Context,一种是基于Worker,二者是独立运行的。
下面本文用一个例子,着重讲解下基于Browsing Context的事件循环机制。

来看下面这段JavaScript代码:

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");

先猜测一下这段代码的输出顺序是什么,再去浏览器控制台输入一下,看看实际输出的顺序和你猜测出的顺序是否一致,如果一致,那就说明,你对JavaScript的事件循环机制还是有一定了解的,继续往下看可以巩固下你的知识;而如果实际输出的顺序和你的猜测不一致,那么本文下面的部分会为你答疑解惑。

任务队列

所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout定时函数等都属于异步任务,异步任务会通过任务队列(Event Queue)的机制来进行协调。具体的可以用下面的图来大致说明一下:

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入Event Queue。主线程内的任务执行完毕为空,会去Event Queue读取对应的任务,推入主线程执行。
上述过程的不断重复就是我们说的Event Loop(事件循环)。

在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次tick的任务处理模型是比较复杂的,其关键的步骤可以总结如下:

在此次tick中选择最先进入队列的任务(oldest task),如果有则执行(一次)

检查是否存在Microtasks,如果存在则不停地执行,直至清空Microtask Queue

更新render

主线程重复执行上述步骤

可以用一张图来说明下流程:

这里相信有人会想问,什么是microtasks?规范中规定,task分为两大类, 分别是Macro Task (宏任务)和Micro Task(微任务), 并且每个宏任务结束后, 都要清空所有的微任务,这里的Macro Task也是我们常说的task,有些文章并没有对其做区分,后面文章中所提及的task皆看做宏任务(macro task)。

(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(Node.js 环境)

microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

setTimeout/Promise等API便是任务源,而进入任务队列的是由他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。

分析示例代码

千言万语,不如就着例子讲来的清楚。下面我们可以按照规范,一步步执行解析下上面的例子,先贴一下例子代码(免得你往上翻)。

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作为第一个宏任务进入主线程,遇到console.log,输出script start

遇到setTimeout,其回调函数被分发到宏任务Event Queue中

遇到Promise,其then函数被分到到微任务Event Queue中,记为then1,之后又遇到了then函数,将其分到微任务Event Queue中,记为then2

遇到console.log,输出script end

至此,Event Queue中存在三个任务,如下表:

宏任务 微任务
setTimeout then1 then2

执行微任务,首先执行then1,输出promise1,然后执行then2,输出promise2,这样就清空了所有微任务

执行setTimeout任务,输出setTimeout

至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout

so,你猜对了吗?

看看你掌握了没

再来一个题目,来做个练习:

console.log("script start");

setTimeout(function() {
  console.log("timeout1");
}, 10);

new Promise(resolve => {
    console.log("promise1");
    resolve();
    setTimeout(() => console.log("timeout2"), 10);
}).then(function() {
    console.log("then1")
})

console.log("script end");

这个题目就稍微有点复杂了,我们再分析下:

首先,事件循环从宏任务(macrotask)队列开始,最初始,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出script start
接着往下走,遇到setTimeout任务源,将其分发到任务队列中去,记为timeout1;
接着遇到promise,new promise中的代码立即执行,输出promise1,然后执行resolve,遇到setTimeout,将其分发到任务队列中去,记为timemout2,将其then分发到微任务队列中去,记为then1;
接着遇到console.log代码,直接输出script end
接着检查微任务队列,发现有个then1微任务,执行,输出then1
再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行timeout1,输出timeout1
接着执行timeout2,输出timeout2
至此,所有的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

用流程图看更清晰:

总结

有个小tip:从规范来看,microtask优先于task执行,所以如果有需要优先执行的逻辑,放入microtask队列会比task更早的被执行。

最后的最后,记住,JavaScript是一门单线程语言。

参考文献

这一次,彻底弄懂 JavaScript 执行机制
Tasks, microtasks, queues and schedules
从一道题浅说 JavaScript 的事件循环

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

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

相关文章

  • 一文掌握前端面试浏览器相关知识点

    摘要:决定了注册的事件是捕获事件还是冒泡事件。浏览器会自动进行通信,实现通信的关键是后端。该方式只能用于二级域名相同的情况下,比如和适用于该方式。中的中的和浏览器中的不相同。 事件机制 事件触发三阶段 事件触发有三个阶段 window 往事件触发处传播,遇到注册的捕获事件会触发 传播到事件触发处时触发注册的事件 从事件触发处往 window 传播,遇到注册的冒泡事件会触发 事件触发一般...

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

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

    K_B_Z 评论0 收藏0
  • 【转】深入理解JS单线程机制【原文作者:MasterYao】

    摘要:的单线程,与它的用途有关。只要指定过回调函数,这些事件发生时就会进入任务队列,等待主线程读取。四主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环。令人困惑的是,文档中称,指定的回调函数,总是排在前面。 原文:http://www.cnblogs.com/Master... 一、为什么JavaScript是单线程? JavaScript语言的一大特点...

    LittleLiByte 评论0 收藏0
  • 2018成长了么?一份给前端技术清单

    摘要:由于个人精力有限,一些技术点的归纳可能有失偏颇,或者目前并未纳入进来,因此上的清单内容也会不断更新。 2018 眼看就要过去了,今年的你相较去年技术上有怎样的收获呢? 记得年初的时候我给自己制定了一个学习计划,现在回顾来看完成度还不错。但仍有些遗憾,一些技术点没有时间去好好学习。 在学习中我发现,像文章这样的知识往往是碎片化的,而前端涉及到的面很多,如果不将这些知识有效梳理,则无法形成...

    K_B_Z 评论0 收藏0

发表评论

0条评论

call_me_R

|高级讲师

TA的文章

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