Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >一篇文章搞懂浏览器Js事件循环机制

一篇文章搞懂浏览器Js事件循环机制

作者头像
努力的Greatiga
发布于 2022-07-25 02:22:08
发布于 2022-07-25 02:22:08
90400
代码可运行
举报
运行总次数:0
代码可运行

浏览器事件循环机制

前言

在初次入门学习和使用 JavaScript 的过程中,相信遇到过许多程序执行顺序及结果与预期不一致的问题,在查阅资料的过程中了解到原来是程序的执行有同步与异步之分;与此同时也会看到许多有关概念,例如回调函数、执行栈、任务队列、事件循环机制(Event Loop)、宏任务、微任务、Promise(ES6)等等。此时对于一个刚入门不久的小白来说,要理解消化这些概念真的不容易。对于入门不久的我来说也一样,所以写一篇博客记录一下,有关 JavaScript 的运行机制,以及上述的这些概念为什么会出现,又解决了什么问题。

一、JavaScript 是单线程

我们知道多线程是可以并行执行程序的,能提高程序运行效率。但是 JS 是一门单线程语言,同一时间内做一件事。

最初作为服务于浏览器的脚本语言,很多时候都是在与用户交互,这个过程涉及了许多 DOM 的操作,倘若使用多线程,那么就容易出现几个线程同时操作一个 DOM 的问题,那么浏览器此时要以哪一个线程为主呢?这样一来无疑增加了复杂性,所以 JS 成为了单线程。虽然说多线程处理起来也很高效,但对于当时直接服务于浏览器用户的 JS 来说,尽可能避免过度复杂,能更简单的处理相对好点吧。

二、异步任务及其回调函数

虽然单线程降低了复杂性,但是也有了新的问题。单线程是顺序执行程序,每一个任务要等待上一个任务执行完毕才执行,如果遇到执行时间太长或者出现了别的问题,那么就会一直卡在那,导致整个程序无法顺利执行完毕。为了解决问题,语言设计者希望在程序执行时,将一些耗时、有延迟的任务先挂起,让能快速执行完毕的任务先执行;按照这样的方式执行完整个程序后,在返回去执行那些被挂起的任务。因此有了同步任务与异步任务之分;在执行过程中,当前执行程序的线程称为主线程,同步任务直接在主线程立即执行,而那些异步任务,先给它挂在一边放着,等到主线程执行完了所有同步任务,再回来读取挂在一旁的异步任务,并且执行他们。

(1) 任务队列

任务队列是一系列事件组成的一个队列,也就是上面说到的异步任务挂起的地方。程序执行时会将定义的异步任务送入任务队列,或者用户点击鼠标触发的异步任务送入队列。等待主线程来执行它们。例如常见的各种事件(鼠标点击、键盘敲击、滚动等等)、又或者是 Ajax 那样等待响应的异步任务。

实际上,任务队列不止一种,因为处理的异步任务种类可能不同

(2) 回调函数 (callback)

回调函数往往就是异步任务所定义的代码。主线程执行完同步任务,就会回来开始读取任务队列中的异步任务并执行这些代码,同时也称为回调函数。

(3) 宏任务和微任务

异步任务又可以看为两种,通常由宿主环境(浏览器、node)提供的为宏任务,由语言标准提供的为微任务。 JavaScript 可能会在不同的宿主环境下运行,所以宏任务来自于宿主环境,而微任务作为语言标准,在任何环境下都可以使用。

常见宏任务
  • setTimeout
  • setInterval
  • setImmediate (仅 node 提供)
  • requestAnimationFrame (仅浏览器提供)
  • 各种交互 (鼠标点击、滚动等等)
  • I/O
常见微任务
  • Promise.then catch finally
  • MutationObserver (仅浏览器提供)
  • process.nextTick (仅 node 提供)

三、事件循环机制 (Event Loop)

主线程执行程序时会将定义的异步任务放入任务队列中,宏任务会放在宏任务队列,微任务放在微任务队列,当触发 UI 事件时,也会把相应任务放入队列。为了确保事件处理正常进行,主线程不阻塞。所以有了解决方案 Event Loop,事件循环线程是独立于主线程的,并且一直存在直到整个脚本环境被关闭。无论是主线程执行时添加的异步任务,还是 UI 交互触发后添加的异步任务,事件循环机制都会按一定规则循环读取并且执行。

那么该循环机制如何运行呢?

  • (1) 打开某个宿主环境时,主线程执行同步任务的所有代码,形成一个执行栈;把遇到的异步任务放入相应的队列里;同时一个独立于主线程的事件循环线程也被创建并一直存在。
  • (2) 当主线程执行完同步任务,会将该执行过程中添加的微任务全部执行完,之后由事件循环机制协调。
  • (3) 事件循环读取当前宏任务队列的一个宏任务,并放入执行栈中执行
  • (4) 在执行过程中遇到宏任务和微任务,按照相同的方式放入相应队列
  • (5) 该宏任务执行完毕后立即执行此次宏任务中所添加的所有微任务
  • (6) 回到第 (3) 步开始重复后面步骤。
  • 说那么多,看个例子
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
console.log('1-1');

Promise.resolve().then(() => console.log('微任务 1-1'));

new Promise((resolve) => {
  console.log('1-2');
  resolve();
}).then(() => {
  console.log('微任务 1-2')
});

setTimeout(() => console.log('宏任务 1-1'), 100);

console.log('1-3');
//1-1
//1-2
//1-3
//微任务 1-1
//微任务 1-2
//宏任务 1-1
  • 主线程开始执行,形成一个执行栈
  • 碰到第一个 console.log('1-1'),并打印 -> 1-1
  • 碰到第一个 Promise,已为成功状态,将其 then() 加到微任务中
  • 碰到第二个 Promise,先执行其中的 console.log('1-2'),打印 -> 1-2,并将其 then() 放入微任务队列
  • 碰到第一个宏任务,放入宏任务队列
  • 碰到 console.log('1-3'),打印 -> 1-3
  • 主线程执行完所有同步任务,开始执行本次添加的所有微任务
  • 读取微任务队列
  • 遇到先进去的第一个 then() ,打印 -> 微任务 1-1
  • 遇到后进去的 then() 打印 -> 微任务 1-2
  • 本次主线程任务完成,下面由事件循环机制来协调。开始读取宏任务队列
  • 遇到第一个放入的宏任务 setTimeout(),将其丢到执行栈延时 100ms 执行,打印 -> 宏任务 1-1
  • 第一次宏任务执行完毕,读取微任务队列,发现没有微任务。进入第二次循环
  • 读取宏任务队列,发现没有宏任务。JS 执行栈开始摸鱼...

到这里其实会发现,微任务都会紧跟在当前执行栈执行同步任务后执行,而存好的宏任务被放在下次执行,好似重新开始一样。

按个人总结来就是(不一定对),主线程的执行栈是专门用来执行代码的;当事件循环线程读取到一个宏任务时,将其放入执行栈执行,主线程会执行其中定义的同步任务,将遇到的宏任务和微任务存起来,在本次同步任务执行完之后立即执行微任务。而此次存好的宏任务又会按照相同的方式在下一次循环中进行。因为事件循环机制一次循环只读取执行一个宏任务。

由此看来其实整个程序也可以看成是一个宏任务,而首次添加的宏任务和微任务是按照上面的方式一层层刨开,按照一次执行一个宏任务和里面所有微任务的规则进行

  • 再看个例子说明宏任务是一次循环读取一次,并且会执行宏任务下所有微任务
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
console.log('开始执行主线程');
console.log('0-1');

Promise.resolve().then(() => console.log('微任务 0-1\n-----'));

setTimeout(() => {//宏任务 1
  console.log('第一个宏任务');
  console.log('宏任务 1-1');
  Promise.resolve().then(() => console.log('微任务 1-1'));
  Promise.resolve().then(() => console.log('微任务 1-2\n-----'));

  setTimeout(() => {//宏任务3
    console.log('第三个宏任务');
    console.log('宏任务 3-1')
    Promise.resolve().then(() => console.log('微任务 3-1\n-----'))
  },10);

},100);

setTimeout(() => {//宏任务2
  console.log('第二个宏任务');
  console.log('宏任务2-1');
  Promise.resolve().then(() => console.log('微任务 2-1\n-----'));
},100);

console.log('0-2');

***************************

执行结果

开始执行主线程
0-1
0-2
微任务 0-1
-----
第一个宏任务
宏任务 1-1
微任务 1-1
微任务 1-2
-----
第二个宏任务
宏任务 2-1
微任务 2-1
-----
第三个宏任务
宏任务 3-1
微任务 3-1
-----
  • 开始执行主线程后,将 微任务 0-1 、 宏任务1 、 宏任务2 存入队列,并先打印其同步任务代码,又打印微任务代码
  • 开始第一次事件循环,读取宏任务1(第一个定时),将 微任务 1-1 、微任务 1-2、和宏任务3 存入队列。打印方式如上一条。
  • 开始第二次事件循环,读取宏任务2(第二个定时),将 微任务 2-1 存入队列,打印方式如上。
  • 开始第三次事件循环,读取宏任务队列中最后一个进去的宏任务3(宏任务1中定义的定时器),将 微任务 3-1 存入队列,打印方式如上。

大概流程图

提示,虽然说是一次循环只读取一个宏任务,但是他没说要等当前宏任务执行完才进行下一次循环哦!!,事件循环读取到队列中的任务并且让它开始执行后,就可以开始下次循环,不需要等待

  • 下面改动的例子,留给自己做练习吧
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
console.log(1);

Promise.resolve().then(() => console.log(2));

setTimeout(() => {
  console.log(3);
  Promise.resolve().then(() => console.log(4));

  setTimeout(() => {
    console.log(5);
  },10);

},200);

setTimeout(() => {
  console.log(6);
  Promise.resolve().then(() => console.log(7));
  setTimeout(() => {
    console.log(8)
  }, 300);
},100);

console.log(9);

自己在纸上写了一下,将代码在浏览器上运行之后对比,发现完全正确。你也可以自己写一下哦。

2020/9/22 更新

有一种情况,那就是 then() 之后接着 then() ,那么此时的顺序呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
console.log('1-1');

Promise.resolve().then(() => console.log('微任务 1-1')).then(() => console.log('微任务 1-3'));

new Promise((resolve) => {
  console.log('1-2');
  resolve();
}).then(() => {
  console.log('微任务 1-2')
}).then(() => console.log('微任务 1-4'));

setTimeout(() => console.log('宏任务 1-1'), 100);

//1-1
//1-2
//1-3
//微任务 1-1
//微任务 1-2
//微任务 1-3
//微任务 1-4
//宏任务 1-1

可以看到他的运行顺序,说明在 then() 执行之后,如果后面还接着 then() 那么按照同样的方式添加到微任务队列,等到之前添加的第一层 then() 都执行完后,在到微任务队列里面读取后面添加的 then(),运行方式如上。并且只有当微任务队列为空时,事件循环机制才会进行到下一轮并读取新的宏任务。

参考链接

阮一峰的网络日志 JavaScript 运行机制详解:再谈Event Loop

知乎作者:tigerHee js中的宏任务与微任务

博客园作者:daisy,gogogo JavaScipt 中的事件循环 event loop,以及微任务和宏任务的概念

国外作者写的一篇文章 Tasks, microtasks, queues and schedules

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
面了十多家,总结出20道JavaScript 必考的面试题!
面临毕业季,相信有很多朋友正在进行找工作,背面试题;今天就分享给大家20道JavaScript必会的问题
程序员老鱼
2023/09/07
2180
面了十多家,总结出20道JavaScript 必考的面试题!
【面试】386- JavaScript 面试 20 个核心考点
Javascript是前端面试的重点,本文重点梳理下 Javascript 中的常考基础知识点,然后就一些容易出现的题目进行解析。限于文章的篇幅,无法将知识点讲解的面面俱到,本文只罗列了一些重难点,如果想要了解更多内容欢迎点击https://github.com/ljianshu/Blog。
pingan8787
2019/10/23
4720
【面试】386- JavaScript 面试 20 个核心考点
学会6大类型JavaScript面试题,面试官都不淡定了
当我们找实例对象的属性时,如果找不到,就会查找与对象关联的原型中去找,如果还找不到,就去找原型的原型,直到最顶层。
can4hou6joeng4
2023/11/29
1720
滴滴前端一面经典手写面试题
一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了
helloworld1024
2023/01/04
9200
js手写题汇总(面试前必刷)
event bus既是node中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。
helloworld1024
2022/11/09
1.1K0
前端一面必会手写面试题(边面边更)4
函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
helloworld1024
2023/01/06
3250
这样回答前端面试题才能拿到offer2
Webkit 和 Firefox 都做了这个优化,当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。
loveX001
2023/01/04
4860
腾讯前端手写面试题及答案
函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
helloworld1024
2022/12/19
6640
2021JavaScript面试题(最新)不定时更新(2021.11.6更新)
js 一共有六种基本数据类型,分别是 Undefined、Null、Boolean、Number、String,还有在 ES6 中新增的 Symbol 类型。 Symbol 代表创建后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。
全栈程序员站长
2022/09/07
2.6K0
2022高频前端面试题合集之JavaScript篇(上)
解析:该题主要考察就是对 js 中的继承是否了解,以及常见的继承的形式有哪些。最常用的继承就是「组合继承」(伪经典继承)和圣杯模式继承。下面附上 js 中这两种继承模式的详细解析。
程序员法医
2022/12/20
1.1K0
2022高频前端面试题合集之JavaScript篇(上)
字节前端高频手写面试题(持续更新中)1
观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了 内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者
helloworld1024
2023/01/03
6980
前端二面手写面试题总结
then 方法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,我们使用一个 callbacks 数组先把传给then的函数暂存起来,等状态改变时再调用。
helloworld1024
2022/10/27
8300
腾讯前端高频手写面试题
函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
helloworld1024
2022/11/15
5940
【吐血整理】前端JavaScript高频手写面试大全,助你查漏补缺
https://segmentfault.com/a/1190000038910420
@超人
2021/02/26
8650
【吐血整理】前端JavaScript高频手写面试大全,助你查漏补缺
js面试题及答案2020_JS面试题大全
number、string、bootlean、null、undefined、Bigint 、Symbol
全栈程序员站长
2022/09/27
3790
前端常见20道高频面试题深入解析
今年来,各大公司都缩减了HC,甚至是采取了“裁员”措施,在这样的大环境之下,想要获得一份更好的工作,必然需要付出更多的努力。本文挑选了20道大厂面试题,建议在阅读时,先思考一番,不要直接看解析。尽管,本文所有的答案,都是我在翻阅各种资料,思考并验证之后,才给出的。但因水平有限,本人的答案未必是最优的,如果您有更好的答案,欢迎给我留言。如果有错误,可以在评论区指出。本文篇幅较长,希望小伙伴们能够坚持读完。
前端达人
2020/04/08
1.2K0
前端常见20道高频面试题深入解析
前端js手写面试题汇总(二)
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。
helloworld1024
2022/11/16
5480
高级前端手写面试题
该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可.
helloworld1024
2022/09/17
7060
2022必会的前端面试手写题
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
buchila11
2022/07/29
5820
百度前端必会手写面试题及答案
防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行
helloworld1024
2022/09/17
5440
推荐阅读
相关推荐
面了十多家,总结出20道JavaScript 必考的面试题!
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档