[JS]事件循环怎么处理宏任务与微任务

Posted ouyangshima

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[JS]事件循环怎么处理宏任务与微任务相关的知识,希望对你有一定的参考价值。

JavaScript的三座大山:单线程与异步,原型与原型链(继承),作用域和闭包。 

new Promise((resolve) => 
	console.log('外层宏事件2');
	resolve()
).then(() => 
	console.log('微事件1');
).then(() => 
	console.log('微事件2')
)
console.log('外层宏事件1');
setTimeout(() => 
	//执行后 回调一个宏事件
	console.log('内层宏事件3')
, 0)

// 打印顺序是什么?
// 外层宏事件2
// 外层宏事件1
// 微事件1
// 微事件2
// 内层宏事件3

为什么会出现这样打印顺序呢?

要理解这些你首先需要对事件循环机制处理宏任务和微任务的方式有了解。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。--先执行一个宏任务,再执行所有微任务

事件循环机制--宏任务和微任务之间的关系

宏任务(macrotask)和微任务(microtask)

异步任务又分为“微队列”和“宏队列”里的任务,微队列里的都执行完才会去执行宏队列里的异步任务

宏任务(macrotask)和微任务(microtask)表示异步任务的两种分类。

宏任务主要有script全局任务、setTimeout、setInterval、setImmediate(node环境)、I/O、UI rendering。

  1. setTimeout和setInterval优先级比setImmediate高.
  2. setImmediate方法则是在当前“任务队列”的尾部添加事件,也即是说,它指定的任务总是在下一次EventLoop时执行。

微任务主要有:process.nextTick(node环境)、Promise.then()、Object.observe、MutationObserver。

  1. 在微任务中 process.nextTick 优先级高于Promise。
  2. process.nextTick方法可以在当前“执行栈”的尾部——下一次Event Loop(主线程读取“任务队列”)之前——触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。

补充概念理解

多线程/单线程的简单理解:

  • 多线程: 程序可以同一时间做几件事。
  • 单线程: 程序同一时间只能做一件事。

面向对象语言JAVA,C++,都是多线程的,即一个进程中可以并发多个线程,而每条线程并行执行不同的任务,也就是说在同一时刻可以同时进行多个任务
而单线程:没有多个线程可供主程序来调用,简单来说,就是同一时刻只能做一件事情

JS为什么是单线程的?

javascript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
  所以,为了避免复杂性(为了避免dom渲染冲突),从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变
  为了利用多核CPU的计算能力,html5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

JS本身必须是单线程的,且必须和浏览器渲染共用一个线程

JS为什么需要异步?

JS的异步---单线程的解决方案

如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。 对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验。
为了避免dom渲染冲突,所以JavaScript必须是单线程的。但是单线程又会带来一系列的问题,比如卡顿,即前面的代码没有执行完,后面的代码又只能一直等待。比如定时器任务setTimeout/serInteral

var i,sum = 0;
for(i=0;i<1000000000;i++)
    sum+=i

//循环执行期间,JS执行和dom渲染暂时卡顿
console.log("abc") 
//由于前面的代码没有执行完,后面的代码也只能是一直等待着,即没有“abc”被打印出

为了解决单线程带来的问题,有了异步这个解决方案。
在可能需要等待的情况下,为了不让这些等待的过程像alert程序一样阻塞程序,这时候就需要异步了,即:所有“等待的情况”都需要异步.
故需要“等待”的情况也是通常前端使用的异步的场景:

  1. 定时任务: setTimeout , setInverval和 process.nextTick ,setImmediate(node特有的定时器)
  2. 网络请求:ajax请求,动态<img>加载
  3. 事件绑定(比如点击事件)

JS单线程又是如何实现异步的呢?

既然JS是单线程的,只能在一条线程上执行,又是如何实现的异步呢?
是通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制。
理解事件循环机制前,我们先来看看任务队列。

任务队列

 为什么会有任务队列呢,还是因为 javascript 单线程的原因,单线程,就意味着一个任务一个任务的执行,执行完当前任务,执行下一个任务,这样也会遇到一个问题,就比如说,要向服务端通信,加载大量数据,如果是同步执行,js 主线程就得等着这个通信完成,然后才能渲染数据,为了高效率的利用cpu, 就有了 同步任务和 异步任务 之分。

  • - 同步任务,进入主线程,一个一个执行
  • - 异步任务,进入 `event table ` , 注册回调函数` callback `, 任务完成之后,将`callback`移入`event queue`,等待主线程调用

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。

任务队列更多的指是异步任务队列,同步任务队列没有多大必要性。

JS的执行机制-->事件循环(Event Loop)

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。EventLoop是javascript的执行机制.异步的实现机制—EventLoop事件轮询.

JavaScript是怎么实现可以不依照代码顺序执行,实现部分代码(也就是异步任务)的异步执行的呢?就是通过EventLoop即事件轮询的机制。换句话说,EventLoop是JavaScript实现异步的具体方案.EventLoop 机制的核心:

  1. 代码分为同步代码和异步代码(异步任务会有对应的回调函数)
  2. 同步代码放在主执行栈里,直接执行
  3. 异步函数先放在异步任务队列里,暂时先不执行
  4. 待同步函数执行完毕,轮询执行异步队列里的函数(执行的就是相关异步任务对应的回调函数)

第3点 将异步任务放入任务队列时分为三种情况:

  1. 若异步任务没有延时,则直接将其放入异步队列
  2. 若有延时,则等延时时间到了才会放入异步队列
  3. 若有ajax请求,则等ajax加载完成才放入异步队列

JS的promise----异步的解决方案

由于JS单线程的本质,需要通过异步来解决单线程带来的问题。而异步也会带来问题:

  1. 代码没按照书写形式执行,导致可读性变差
  2. callback(回调函数)中不容易模块化

回调嵌套是解决异步最直接的方法,即将后一个的操作放在前一个操作的异步回调里,但回调的多层嵌套会导致使代码很冗杂,而且导致检查代码会费劲。
Promise可以用来优雅避免callback hell.

单线程、异步、EventLoop、Promise的关系

  1. 为了避免dom渲染冲突,要求JavaScript的本质就是单线程的.
  2. 为了解决单线程带来的可能造成卡顿和等待的问题,需要JavaScript的异步.
  3. 为了实现JavaScript的异步,利用的是EventLoop事件轮询的机制.
  4. 为了解决异步里回调函数嵌套带来的问题,利用Promise 优雅避免callback hell问题.

JavaScript的同步和异步

单线程的不足

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务, 如果前一个任务耗时很长,后一个任务就不得不一直等着。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
这时候就需要把任务区分为同步任务和异步任务。

同步任务: 指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务: 指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

同步和异步解决单线程的不足

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

同步任务就是顺次执行的那一种,而异步任务就是需要等待的那一种。同步任务和异步任务的执行路线是不同的:同步任务会进入主线程顺次执行。只有前一个执行结束才会执行下一个;而异步任务不进入主线程,而会进入一个等待的"任务队列"(task queue)只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

读取到一个异步任务,首先是将异步任务放进事件表格(Event table)中,当放进事件表格中的异步任务完成某种事情或者说达成某些条件(如setTimeout事件到了,鼠标点击了,数据文件获取到了)之后,才将这些异步任务推入事件队列(Event Queue)中,这时候的异步任务才会在执行栈中空闲的时候才能读取到的异步任务。

以上是关于[JS]事件循环怎么处理宏任务与微任务的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript之事件循环,宏任务与微任务

javaScript-宏任务与微任务/事件轮询

20230515学习笔记——js中的同步任务与异步任务,宏任务与微任务

js事件循环运行机制

js事件循环运行机制

JS同步任务与微任务和宏任务