事件循环

JavaScript 语言的一大特点就是单线程,同一个时间只能做一件事。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。

Event Loop 包含两类:一类是基于 Browsing Context,一种是基于 Worker。二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。

任务队列

根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次)
  • 检查是否存在 Microtasks,如果存在则不停地执行,直至清空 Microtasks Queue
  • 更新 render
  • 主线程重复执行上述步骤

在上诉tick的基础上需要了解几点:

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

案例解释

现在有这样一段代码

console.log(1)

Promise.resolve().then(() => {
console.log(2)
}) console.log(3)

要真正理解这一段代码,我们必须先搞懂Promise中实例方法then到底在做什么?

then相当于为Promise设置了一个回调函数,当Promise中的数据处理完毕时,便会调用then所设置的回调函数来继续后续任务。

上例中,我们通过Promise.resolve()创建了一个理解完成的Promise,那么按道理讲then中的回调函数应该立刻执行啊?因为Promise已经完成了啊?所以打印的顺序不应该是“1 2 3”吗?

then中的回调函数会在Promise完成后(status由pending变为fulfilled时)被调用,但是注意并不是立刻就调用,而是采用一种和定时器类似的处理方式,将函数放入到一个任务队列中,而队列中的代码会在调用栈中的代码执行完毕后才会执行。也就是说then中的代码总是在当前调用栈中的代码执行完后才执行。所以上边代码的输出结果应该为:“1 3 2”

setTimeout(()=>{
console.log(1)
}) Promise.resolve().then(() => {
console.log(2)
})

这段代码的执行顺序又是什么呢?

错误的分析:setTimeout是定时器,它会在一段时间后将函数放入到任务队列中,而我们没有指定时间,也就意味着函数会立刻放入到任务队列中。then同样也是将函数放入到任务队列中,并且这个Promise是一个立即完成的Promise所以函数也是立刻进入任务队列。那么按照执行顺序来讲,定时器在前,then在后,所以定时器中的函数应该先进入队列,队列又是先进先出的,所以应该先1后2。

上边的分析看似合理,实际上是不对的。因为setTimeout和then虽然都将函数放入到队列中,但是却不是同一个队列。为了更合理的处理异步任务,ES标准规定了一个内部的队列“PromiseJobs”,这个队列是专门用来放置由Promise产生的回调函数的(then、catch、finally),这个队列我们通常被称为“微任务队列(microtask queue)”。相对的,setTimeout这些方法是将函数放入到了“宏任务队列(macrotask queue)”。

简单来说,任务队列有两个,宏任务队列和微任务队列。代码执行时,宏任务进入到宏任务队列,微任务进入到微任务队列。大部分的任务都属于宏任务,而微任务通常在代码运行时产生,通常是由Promise所创建的,Promise的then、catch、finally中的回调函数会作为微任务进入到微任务队列中。

JS代码执行时,每一个宏任务执行完毕后,JS引擎会立即执行微任务队列中的所有任务,然后才是执行宏任务队列中的任务。换句话中then中的回调函数(微任务)会先于定时器中的回调函数(宏任务)执行。所以上例中代码的执行结果应该为:“2 1”。

console.log(1);

setTimeout(() => console.log(2));

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

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

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

setTimeout(() => console.log(6));

console.log(7);
// 1 7 3 5 2 6 4

宏任务

(macro)task,可以理解是每次执行栈 执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

微任务

microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

queneMicrotask()//向微任务队列添加微任务

运行机制

在事件循环中,每进行一次循环操作称为tick,每一次tick的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将其添加到微任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

有几个关键点如下:

  • 所有微任务总会在下一个宏任务之前全部执行完毕,宏任务必然是在微任务之后才执行的(因为微任务实际上是宏任务的其中一个步骤)。
  • 宏任务按顺序执行,且浏览器在每个宏任务之间渲染页面
  • 所有微任务也按顺序执行,且在以下场景会立即执行所有微任务
    • 每个回调之后且js执行栈中为空。
    • 每个宏任务结束后。

通过例子加深理解

setTimeout(() => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(() => {
console.log(3)
})
console.log(2)

流程如下:

  1. 整体script作为第一个宏任务进入主线程,遇到setTimeout入栈处理,发现是异步函数(宏任务),出栈,移交给Web API处理,0秒等待后,将回调函数加到宏任务队列尾部;
  2. 遇到new Promise,入栈处理,发现是同步任务,直接执行,console输出1;
  3. 遇到then,入栈处理,发现是异步函数(微任务),出栈,移交给Web API处理,将回调函数加入微任务队列尾部;
  4. 遇到console.log(2),入栈处理,同步任务,直接console输出2, 出栈;
  5. 栈已清空,检查微任务队列;
  6. 取出第一个回调函数,入栈处理,发现是同步任务,直接console输出3, 出栈;
  7. 继续从取微任务队列中取下一个,发现微任务队列已清空,结束第一轮事件循环;
  8. 从宏任务队列中取出第一个宏任务,入栈处理,发现是同步任务,直接console输出4;

所以,最终输出结果为:1 > 2 > 3 > 4

稍微改变一下

setTimeout(_ => console.log(4))

new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
}) console.log(2)

最终输出结果为:1 > 2 > 3 > before timeout > also before timeout > 4

before timeout与also before timeout在4之前输出的原因是,在微任务执行的过程中,新产生的微任务会被直接添加到微任务队列尾部并在下一宏任务执行之前,全部执行掉

而如果在微任务执行的过程中,新产生了宏任务,则会进入到宏任务队列尾部,按照宏任务顺序在后面的事件循环中执行。

再来一个嵌套的例子

Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
}) setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)

最后输出结果是Promise1 > setTimeout1 > Promise2 > setTimeout2

  1. 一开始执行栈的同步任务执行完毕,会去 microtasks queues 找,清空 microtasks queues ,输出Promise1,同时会生成一个异步任务 setTimeout1
  2. 去宏任务队列查看此时队列是 setTimeout1 在 setTimeout2 之前,因为setTimeout1执行栈一开始的时候就开始异步执行,所以输出 setTimeout1
  3. 在执行setTimeout1时会生成Promise2的一个 microtasks ,放入 microtasks queues 中,接着又是一个循环,去清空 microtasks queues ,输出 Promise2
  4. 清空完 microtasks queues ,就又会去宏任务队列取一个,这回取的是 setTimeout2

作者:pl

链接:https://juejin.cn/post/7072666354402197540

来源:稀土掘金

作者: 李立超

链接:https://www.lilichao.com/index.php/2022/10/12/宏任务和微任务/

宏任务&微处理的更多相关文章

  1. js 同步 异步 宏任务 微任务 文章分享

    分享一篇 写的很好的 宏任务 微任务  同步异步的文章 文章原地址: https://juejin.im/post/59e85eebf265da430d571f89 这一次,彻底弄懂 JavaScri ...

  2. 2--面试总结-深入理解js线程进阶-宏任务微任务

    前言:Event Loop即时间循环,是指浏览器或Node的一种解决javascript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理 Js运行机制      1.不同运行环境,js运行 ...

  3. $nextTick 宏任务 微任务 macrotasks microtasks

    1.nextTick调用方法 首先看nextTick的调用方法: https://cn.vuejs.org/v2/api/#Vue-nextTick // 修改数据 vm.msg = 'Hello' ...

  4. 理解 Events Loop 宏任务微任务

    在 JavaScript 中,任务被分为两种,一种宏任务(MacroTask),一种叫微任务(MicroTask). MacroTask(宏任务) script全部代码.setTimeout.setI ...

  5. Event Loop我知道,宏任务微任务是什么鬼?

    在介绍宏任务和微任务之前,先抛出一个问题.相信大家在面试的时候,会遇到这样的相似的问题: setTimeout(function(){undefined console.log('1') }); ne ...

  6. 从EventLoop到宏任务与微任务

    1.javascript是单线程的 javascript是单线程的,意思是javascript在同一时间内只能做一件事情. 为什么是单线程的? 因为js的主要用途是用于用户交互和操作DOM,如果是多线 ...

  7. 宏任务、微任务与Event Loop

    说到宏任务和微任务,我们就不得不提 Event Loop 了 JS的本质是单线: 1. 一般来说,非阻塞性的任务采取同步的方式,直接在主线程的执行栈完成. 2. 一般来说,阻塞性的任务都会采用异步来执 ...

  8. JavaScript的事件队列(Event Queue)---宏任务和微任务

    前言 在写代码的时候经常思考一个问题,到底是那个函数先执行,本身JavaScript是一门单线程的语言,意思就是按照顺序执行.但是加入一些setTimeout和promise的函数来又实现了异步操作, ...

  9. JS中EventLoop、宏任务与微任务的个人理解

    为什么要EventLoop? JS 作为浏览器脚本语言,为了避免复杂的同步问题(例如用户操作事件以及操作DOM),这就决定了被设计成单线程语言,而且也将会一直保持是单线程的.而在单线程中若是遇到了耗时 ...

  10. javascript中的宏任务和微任务(二)

    js事件轮询执行顺序总结: 1)所有的同步任务都在主线程上执行,行成一个执行栈. 2)除了主线程之外,还存在一个任务列队,只要异步任务有了运行结果,就在任务列队中植入一个时间标记. 3)主线程完成所有 ...

随机推荐

  1. js正则 -180 到180 小数点后无限位、el-input

    正则 -180 到180 小数点后无限位/^0$|^-?0\.\d*[1-9]$|^-?[1-9](\.\d*[1-9])?$|^-?[1-9]\d(\.\d*[1-9])?$|^-?1[0-7]\d ...

  2. Appium自动化测试之键盘操作pressKeyCode()方法(Android特有)

    电话键 KEYCODE_CALL 拨号键 5 KEYCODE_ENDCALL 挂机键 6 KEYCODE_HOME 按键Home 3 KEYCODE_MENU 菜单键 82 KEYCODE_BACK ...

  3. HCIP-ICT实战进阶03-OSPF高级特性

    HCIP-ICT实战进阶03-OSPF高级特性 1 ospf的快速收敛 ospf快速收敛是为了提高路由的手来你熟读而做的扩展特性, 包括PRC(Partial Route Calculation, 部 ...

  4. Python编码转换图

  5. 07 从RDD创建DataFrame

    1.pandas df 与 spark df的相互转换 df_s=spark.createDataFrame(df_p) df_p=df_s.toPandas() 2. Spark与Pandas中Da ...

  6. 为动态二级域名申请https的免费证书.

    前面已经讲过将nginx部署,并注册了免费的二级域名.但将网址发给儿子,儿子说微信已经不能打开http的网址了,所以一想还是研究一下https的证书申请. 网上有很多讲通过,acme的脚本来自动化申请 ...

  7. 爱心代码_HTML

    直接上效果 <!doctype html> <html> <head> <meta charset="utf-8"> <tit ...

  8. centos7中通过源码安装postgresql13.6

    下载地址:https://www.postgresql.org/ftp/source/ 0.安装相关依赖库 centos依赖包下载地址:https://developer.aliyun.com/pac ...

  9. 使用fontmin,压缩字体文件,从十几M到几kb,只选择需要使用的文字

    字体文件压缩fontmin,大幅压缩字体文件 快速熟练fontmin的使用,只需要在代码中配置文章中需要用到的文字,可以大幅度缩减代码大小 安装 npm install fontmin 目录结构 sr ...

  10. Vivado工程常见报错及解决办法

    1. 在进行自定义 IP 后,将自定义 IP 添加到当前的工程时,出现如下报错: [IP_Flow 19-167] Failed to deliver one or more file(s). [IP ...