Event Loop

JavaScript的学习零散而庞杂,很多时候我们学到了一些东西,但是却没办法感受到进步!甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直在试图寻找一条核心的线索,只要顺着这条线索,我就能够一点一点的进步。

前端基础进阶正是围绕这条线索慢慢展开,而事件循环机制(Event Loop),则是这条线索的最关键的知识点。

所以,我就马不停蹄的去深入的学习了事件循环机制,并总结出了这篇文章跟大家分享。

1

事件循环机制从整体上告诉了我们JavaScript代码的执行顺序。但是在我学习的过程中,找到的许多国内博客文章对于它的讲解浅尝辄止,不得其法,很多文章在图中画个圈就表示循环了,看了之后也没感觉明白了多少。但是他又如此重要,以致于当我想要面试中高级岗位时,事件循环机制总是绕不开的话题。特别是ES6中正式加入了Promise对象之后,对于新标准中事件循环机制的理解就变得更加重要。这就很尴尬了。

最近有两篇比较火的文章也表达了这个问题的重要性。

这个前端面试在搞事
80% 应聘者都不及格的 JS 面试题

但很遗憾的是,大神们告诉了大家这个知识点很重要,却并没有告诉大家为什么会这样。所以当我们在面试遇到这样的问题时,就算你知道了结果,面试官再进一步问一下,我们依然懵逼。

学习事件循环机制之前,我默认你已经懂得了如下概念

  • 执行上下文(Execution context)
  • 函数调用栈(call stack)
  • 队列数据结构(queue)
  • Promise(我会在下一篇文章专门总结Promise的详细使用)

因为chrome浏览器中新标准中的事件循环机制与nodejs类似,因此此处就整合nodejs一起来理解,其中会介绍到几个nodejs有,但是浏览器中没有的API,大家只需要了解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate

OK,那我就先抛出结论,然后以例子与图示详细给大家演示事件循环机制。

我们知道JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。

当然新标准中的web worker涉及到了多线程,这里就不讨论了。

JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。

队列数据结构

  • 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
  • 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
  • setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
// setTimeout中的回调函数才是进入任务队列的任务setTimeout(function() {    console.log('xxxx');})// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行
  • 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
  • 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
  • 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
    纯文字表述确实有点干涩,因此,这里我们通过2个例子,来逐步理解事件循环的具体顺序。
// demo01  出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。// 为了方便理解,我以打印出来的字符作为当前的任务名称setTimeout(function() {    console.log('timeout1');})new Promise(function(resolve) {    console.log('promise1');    for(var i = 0; i < 1000; i++) {        i == 99 && resolve();    }    console.log('promise2');}).then(function() {    console.log('then1');})console.log('global1');

首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去,所以,上面例子的第一步执行如下图所示。

首先script任务开始执行,全局上下文入栈

第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。

setTimeout(function() {    console.log('timeout1');})

宏任务timeout1进入setTimeout队列

第三步:script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-task的Promise队列中去。

因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。

promise1入栈执行,这时promise1被最先输出

resolve在for循环中入栈执行

构造函数执行完毕的过程中,resolve执行完毕出栈,promise2输出,promise1页出栈,then执行时,Promise任务then1进入对应队列

script任务继续往下执行,最后只有一句输出了globa1,然后,全局任务就执行完毕了。

第四步:第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行,也是进入函数调用栈中执行的。

执行所有的微任务

第五步:当所有的micro-tast执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。

微任务被清空

这个时候,我们发现宏任务中,只有在setTimeout队列中还要一个timeout1的任务等待执行。因此就直接执行即可。

timeout1入栈执行

这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。

那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。

这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复杂一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。

// demo02console.log('golb1');setTimeout(function() {    console.log('timeout1');    process.nextTick(function() {        console.log('timeout1_nextTick');    })    new Promise(function(resolve) {        console.log('timeout1_promise');        resolve();    }).then(function() {        console.log('timeout1_then')    })})setImmediate(function() {    console.log('immediate1');    process.nextTick(function() {        console.log('immediate1_nextTick');    })    new Promise(function(resolve) {        console.log('immediate1_promise');        resolve();    }).then(function() {        console.log('immediate1_then')    })})process.nextTick(function() {    console.log('glob1_nextTick');})new Promise(function(resolve) {    console.log('glob1_promise');    resolve();}).then(function() {    console.log('glob1_then')})setTimeout(function() {    console.log('timeout2');    process.nextTick(function() {        console.log('timeout2_nextTick');    })    new Promise(function(resolve) {        console.log('timeout2_promise');        resolve();    }).then(function() {        console.log('timeout2_then')    })})process.nextTick(function() {    console.log('glob2_nextTick');})new Promise(function(resolve) {    console.log('glob2_promise');    resolve();}).then(function() {    console.log('glob2_then')})setImmediate(function() {    console.log('immediate2');    process.nextTick(function() {        console.log('immediate2_nextTick');    })    new Promise(function(resolve) {        console.log('immediate2_promise');        resolve();    }).then(function() {        console.log('immediate2_then')    })})

这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。

第一步:宏任务script首先执行。全局入栈。glob1输出。

script首先执行

第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。

setTimeout(function() {    console.log('timeout1');    process.nextTick(function() {        console.log('timeout1_nextTick');    })    new Promise(function(resolve) {        console.log('timeout1_promise');        resolve();    }).then(function() {        console.log('timeout1_then')    })})

timeout1进入对应队列

第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。

setImmediate(function() {    console.log('immediate1');    process.nextTick(function() {        console.log('immediate1_nextTick');    })    new Promise(function(resolve) {        console.log('immediate1_promise');        resolve();    }).then(function() {        console.log('immediate1_then')    })})

进入setImmediate队列

第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。

process.nextTick(function() {    console.log('glob1_nextTick');})

nextTick

第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。

new Promise(function(resolve) {    console.log('glob1_promise');    resolve();}).then(function() {    console.log('glob1_then')})

先是函数调用栈的变化

然后glob1_then任务进入队列

第六步:执行遇到第二个setTimeout。

setTimeout(function() {    console.log('timeout2');    process.nextTick(function() {        console.log('timeout2_nextTick');    })    new Promise(function(resolve) {        console.log('timeout2_promise');        resolve();    }).then(function() {        console.log('timeout2_then')    })})

timeout2进入对应队列

第七步:先后遇到nextTick与Promise

process.nextTick(function() {    console.log('glob2_nextTick');})new Promise(function(resolve) {    console.log('glob2_promise');    resolve();}).then(function() {    console.log('glob2_then')})

glob2_nextTick与Promise任务分别进入各自的队列

第八步:再次遇到setImmediate。

setImmediate(function() {    console.log('immediate2');    process.nextTick(function() {        console.log('immediate2_nextTick');    })    new Promise(function(resolve) {        console.log('immediate2_promise');        resolve();    }).then(function() {        console.log('immediate2_then')    })})

nextTick

这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。

其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。

当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。

这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。

第二轮循环初始状态

setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。

只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。

setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。

当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。

大家需要注意这里的循环结束的时间节点。

当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。

OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。

当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。

// 用数组模拟一个队列var tasks = [];// 模拟一个事件分发器var addFn1 = function(task) {    tasks.push(task);}// 执行所有的任务var flush = function() {    tasks.map(function(task) {        task();    })}// 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中setTimeout(function() {    flush();})// 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法var dispatch = function(name) {    tasks.map(function(item) {        if(item.name == name) {            item.handler();        }    })}// 当然,我们把任务丢进去的时候,多保存一个name即可。// 这时候,task的格式就如下demoTask =  {    name: 'demo',    handler: function() {}}// 于是,一个订阅-通知的设计模式就这样轻松的被实现了

这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级。

需要注意的是,这里的执行顺序,或者执行的优先级在不同的场景里由于实现的不同会导致不同的结果,包括node的不同版本,不同浏览器等都有不同的结果。

建议收藏,不然刷着刷着就可能找不到了

有什么前端的问题欢迎私信我~期待你的到来。

我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年我花了一个月整理了一份最适合2020年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取

boost log 能不能循环覆盖_前端基础进阶(十四):深入核心,详解事件循环机制...相关推荐

  1. c++ post请求_前端工程师进阶:网络请求方法详解,GET和POST的区别

    1 前言 最近看了一些同学的面经,发现无论什么技术岗位,还是会问到 get 和 post 的区别,而搜索出来的答案并不能让我们装得一手好逼,那就让我们从 HTTP 报文的角度来撸一波,从而搞明白他们的 ...

  2. 电脑主板跳线_电脑基础进阶必学知识,详解电脑主板跳线!

    在DIY装机时新手总会有不同的问题,虽然目前网上流传着各种版本的教学文章或者视频,但是细致的小技巧讲解还是有限,不少网友在装机的时候虽然大致明白各个硬件的组合,但是在跳线的环节可以难住了不少的人,也挡 ...

  3. protobuf前后端解析_前端后台以及游戏中使用google-protobuf详解

    前端后台以及游戏中使用google-protobuf详解 [TOC] 0.什么是protoBuf protoBuf是一种灵活高效的独立于语言平台的结构化数据表示方法,与XML相比,protoBuf更小 ...

  4. devc 无法编译循环语句_鸡生蛋还是蛋生鸡?详解第一个编译器是怎么来的~

    详解编译器自举原理 不知道你有没有想过,某种编程语言的第一个编译器是怎么来的呢?这不就是"鸡生蛋,蛋生鸡"的问题吗? 先说最后的结论:任何一种语言的第一个编译器肯定是使用其他语言写 ...

  5. 前端之CSS篇(四)——CSS浮动详解

    前言:页面布局要学习三大核心,盒子模型,浮动 和定位 . 1.CSS浮动 场景:整个网页的布局 本节需要解决的问题: 为什么需要浮动 浮动的排列特性 3种最常见的布局方式 为什么需要清除浮动 写出至少 ...

  6. python提供两种基本循环结构_零基础学python 12 程序不枯燥:for循环结构(课后习题)...

    上节课我们一起学习了咱们的for循环结构,这节课咱们来就之前学过的知识做一下习题吧: 1. 主题: Duck, Duck, Goose! 假设你在一个家禽农场工作,需要计算从传送带传下来的动物数量(不 ...

  7. Web前端基础知识:ES5及ES6this详解

    今天,我们学习一下JavaScript中的this.我们从什么是this,ES5及ES6中this的几种情况进行学习.让this变的so easy,我们这里说的都是非严格模式下. 什么是this th ...

  8. 2 数据源配置_[Mybatis]-[基础支持层]-数据源信息-数据源详解

    该系列文章针对 Mybatis 3.5.1 版本 在上一篇文章中,谈到了 <environment> 标签解析会构建 Environment 对象,Environment 对象中有两个关键 ...

  9. jquery function_前端基础进阶(十三)详细图解jQuery扩展jQuery插件

    UI 鉴赏 早几年学习前端,大家都非常热衷于研究jQuery源码. 我至今还记得当初从jQuery源码中学到一星半点应用技巧的时候常会有一种发自内心的惊叹,"原来JavaScript居然可以 ...

最新文章

  1. gorm 返回多条数据_如何优雅的操作数据库?
  2. 四元数相关总结-未完
  3. 【机器学习】NeuralProphet,这个时序工具包也太强了吧...
  4. 利用鼠标点击绘制出三棱锥
  5. C#中使用opencv处理图像
  6. 用python计算ln函数_python-含参函数
  7. 执行计划中cpu耗时_面试被问怎么排查遇到的系统CPU飙高和频繁GC,到底该怎么回答?...
  8. mysql正在加载_mysql 数据库基本操作
  9. opencv实战3: CascadeClassifier+Haar特征进行人脸检测
  10. 华为云数据库内核专家为您揭秘:GaussDB(for MySQL)并行查询有多快?
  11. java接收参数_javaWeb收传参数方式总结
  12. 新征程 linux下C编程
  13. ICCV2021 | 简单有效的长尾视觉识别新方案:蒸馏自监督(SSD)
  14. Flash制作标题出现动画
  15. png能转换成html吗,png转化成pdf
  16. 郑州大学计算机上机模拟题库,郑州大学VB考试模拟试题
  17. matlab怎么做空间计量,六步学会用MATLAB做空间计量回归详细步骤
  18. 机器学习(1)-项目 0: 预测泰坦尼克号乘客生还率
  19. 十大靠谱“计算机视觉数据集”榜单
  20. 如何用WxJump解决微信二维码无法跳转

热门文章

  1. 三维空间点进行空间平面拟合原理及MATLAB和C++代码实现
  2. Houdini魔法特效制作学习教程
  3. Complete C# Unity Game Developer 2D
  4. UE capability与 双连接相关的参数。
  5. leetcode-295 数据流的中位数
  6. Linux进程管理:进程状态和CPU平均负载
  7. awk 6.0 — awk模式之二
  8. params.success params.success(res.data)
  9. Sql语法---DDL
  10. redis实现对账(集合比较)功能