事件循环(2024 面试题)

答题大纲

  1. 先说基本知识点,宏任务、微任务有哪些
  2. 说事件循环机制过程,边说边画图出来
  3. 说async/await执行顺序注意,可以把 chrome 的优化,做法其实是违法了规范的,V8 团队的PR这些自信点说出来,显得你很好学,理解得很详细,很透彻。
  4. 把node的事件循环也说一下,重复1、2、3点,node中的第3点要说的是node11前后的事件循环变动点


前端之所以会有事件循环机制,是因为JavaScript引擎在浏览器环境中是单线程的,这意味着在同一时间只能执行一段JavaScript代码,不能同时执行多个任务。然而,现代Web应用需要处理大量的异步操作,如用户交互(点击、滚动等)、网络请求(AJAX)、定时任务(setTimeout/setInterval)等。如果所有的操作都必须同步执行,将会严重阻塞主线程,导致用户体验下降,例如页面卡死、无响应等。

事件循环机制就是为了克服这个问题而设计的,它允许JavaScript引擎在执行同步代码的同时,能够异步处理上述的各种任务,并在合适的时候将这些异步任务的结果回调至主线程执行。

macro-task大概包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

micro-task大概包括:

  • process.nextTick
  • Promise
  • Async/Await(实际就是promise)
  • MutationObserver(html5新特性)


总的结论就是,执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环

async/await执行顺序


async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。
 

  • 如果await 后面直接跟的为一个变量,比如:await 1;这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码)。然后跳出async1函数,执行其他代码,当遇到promise函数的时候,会注册promise.then()函数到微任务队列,注意此时微任务队列里面已经存在await后面的微任务。所以这种情况会先执行await后面的代码(async1 end),再执行async1函数后面注册的微任务代码(promise1,promise2)。
  • 如果await后面跟的是一个异步函数的调用,比如上面的代码,将代码改成这样:


面试题:说说事件循环机制(满分答案来了) - 掘金

chrome优化 对 await后跟 同步任务做了优化,之前是按照一个异步任务。现在是如果是同步任务则按同步执行,但await语句之后算异步

node 中的事件循环


浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。


 

宏任务和微任务

node 中也有宏任务和微任务,与浏览器中的事件循环类似,其中,

macro-task 大概包括:

  • setTimeout
  • setInterval
  • setImmediate
  • script(整体代码)
  • I/O 操作等。

micro-task 大概包括:

  • process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
  • new Promise().then(回调)等。


 

node事件循环整体理解

图中的每个框被称为事件循环机制的一个阶段,每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。

因此,从上面这个简化图中,我们可以分析出 node 的事件循环的阶段顺序为:

输入数据阶段(incoming data)->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段(timers)->I/O事件回调阶段(I/O callbacks)->闲置阶段(idle, prepare)->轮询阶段...

阶段概述
  • 定时器检测阶段(timers):本阶段执行 timer 的回调,即 setTimeout、setInterval 里面的回调函数。
  • I/O事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的一些I/O回调。
  • 闲置阶段(idle, prepare):仅系统内部使用。
  • 轮询阶段(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检查阶段(check):setImmediate() 回调函数在这里执行
  • 关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on('close', ...)。
三大重点阶段

日常开发中的绝大部分异步任务都是在 poll、check、timers 这3个阶段处理的,所以我们来重点看看。

timers

timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。 同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。

poll

poll 是一个至关重要的阶段,poll 阶段的执行逻辑流程图如下:

如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到 timers 阶段。

如果没有定时器, 会去看回调函数队列。

  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
  • 如果 poll 队列为空时,会有两件事发生
    • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
    • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去,一段时间后自动进入 check 阶段。
check

check 阶段。这是一个比较简单的阶段,直接执行 setImmdiate 的回调。

process.nextTick

process.nextTick 是一个独立于 eventLoop 的任务队列。

在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。

看一个例子:

setImmediate(() => { console.log('timeout1') Promise.resolve().then(() => console.log('promise resolve')) process.nextTick(() => console.log('next tick1')) }); setImmediate(() => { console.log('timeout2') process.nextTick(() => console.log('next tick2')) }); setImmediate(() => console.log('timeout3')); setImmediate(() => console.log('timeout4')); 复制代码

  • 在 node11 之前,因为每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行,因此上述代码是先进入 check 阶段,执行所有 setImmediate,完成之后执行 nextTick 队列,最后执行微任务队列,因此输出为timeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve
  • 在 node11 之后,process.nextTick 是微任务的一种,因此上述代码是先进入 check 阶段,执行一个 setImmediate 宏任务,然后执行其微任务队列,再执行下一个宏任务及其微任务,因此输出为timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4

node 版本差异说明

这里主要说明的是 node11 前后的差异,因为 node11 之后一些特性已经向浏览器看齐了,总的变化一句话来说就是,如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列,一起来看看吧~

timers 阶段的执行时机变化
setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
  • 如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,这就跟浏览器端运行一致,最后的结果为timer1=>promise1=>timer2=>promise2
  • 如果是 node10 及其之前版本要看第一个定时器执行完,第二个定时器是否在完成队列中.
    • 如果是第二个定时器还未在完成队列中,最后的结果为timer1=>promise1=>timer2=>promise2
    • 如果是第二个定时器已经在完成队列中,则最后的结果为timer1=>timer2=>promise1=>promise2
check 阶段的执行时机变化
setImmediate(() => console.log('immediate1'));
setImmediate(() => {
    console.log('immediate2')
    Promise.resolve().then(() => console.log('promise resolve'))
});
setImmediate(() => console.log('immediate3'));
setImmediate(() => console.log('immediate4'));
  • 如果是 node11 后的版本,会输出immediate1=>immediate2=>promise resolve=>immediate3=>immediate4
  • 如果是 node11 前的版本,会输出immediate1=>immediate2=>immediate3=>immediate4=>promise resolve
nextTick 队列的执行时机变化
setImmediate(() => console.log('timeout1'));
setImmediate(() => {
    console.log('timeout2')
    process.nextTick(() => console.log('next tick'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
  • 如果是 node11 后的版本,会输出timeout1=>timeout2=>next tick=>timeout3=>timeout4
  • 如果是 node11 前的版本,会输出timeout1=>timeout2=>timeout3=>timeout4=>next tick

以上几个例子,你应该就能清晰感受到它的变化了,反正记着一个结论,如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列。

node 和 浏览器 eventLoop的主要区别

两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而nodejs中的微任务是在不同阶段之间执行的。

JavaScript的确是一门单线程语言,但是浏览器UI是多线程的,异步任务借助浏览器的线程和JavaScript的执行机制实现。 例如,setTimeout就借助浏览器定时器触发线程的计时功能来实现

浏览器线程

  1. GUI渲染线程
    • 绘制页面,解析HTML、CSS,构建DOM树等
    • 页面的重绘和重排
    • 与JS引擎互斥(JS引擎阻塞页面刷新)
  1. JS引擎线程
    • js脚本代码执行
    • 负责执行准备好的事件,例如定时器计时结束或异步请求成功且正确返回
    • 与GUI渲染线程互斥
  1. 事件触发线程
    • 当对应的事件满足触发条件,将事件添加到js的任务队列末尾
    • 多个事件加入任务队列需要排队等待
  1. 定时器触发线程
    • 负责执行异步的定时器类事件:setTimeout、setInterval等
    • 浏览器定时计时由该线程完成,计时完毕后将事件添加至任务队列队尾
  1. HTTP请求线程
    • 负责异步请求
    • 当监听到异步请求状态变更时,如果存在回调函数,该线程会将回调函数加入到任务队列队尾

Event loop执行顺序

  • 首先执行同步代码,这属于宏任务
  • 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
  • 执行同步代码时遇到微任务则将其放入微任务队列,遇到宏任务则存入宏任务队列
  • 执行所有微任务
  • 当执行完所有微任务后,如有必要会渲染页面
  • 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数

进程与线程区别?JS 单线程带来的好处?

进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。

线程是进程中的更小单位,描述了执行一段指令所需的时间。

浏览器中来说,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁

在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。这其实也是一个单线程的好处,得益于 JS 是单线程运行的,可以达到节省内存,节约上下文切换时间,没有锁的问题的好处。当然前面两点在服务端中更容易体现,对于锁的问题,形象的来说就是当我读取一个数字 15 的时候,同时有两个操作对数字进行了加减,这时候结果就出现了错误。解决这个问题也不难,只需要在读取的时候加锁,直到读取完毕之前都不能进行写入操作

什么是执行栈?

可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。


当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题

做题技巧

1、 new构造函数中的内容不属于异步事件

  • 其中resolve 才输入一次微任务
const p = new Promise((resolve)=>{
  console.log(111)   // 1
  resolve(222)     ///4
  console.log(333) ///2
})

p.then((v)=>{
	console.log(v)
})

console.log(333)   ///3

2、async await

  • 执行async函数,返回的是Promise对象
  • await必须在async包裹之下执行
  • await相当于Promise的then并且同一作用域下await下面的内容全部作为then中回调的内容
  • try……catch可捕获异常,代替了Promise的catch
  • 异步中先执行微任务,再执行宏任务
  • await 中的 同步代码按照同步顺序执行,但是 await语句看成一个Promise 后面的语句需要等await执行完
    • async 内的await 后面内容看成一个微任务
    • 可以理解为 紧跟着await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中

3、resolve, reject这两个参数其实也都是函数 内部值是一个微任务

4、then、catch中属于微任务

1、无论是then还是catch里的回调内容只要代码正常执行或者正常返回,则当前新的Promise实例为fulfilled状态。如果有报错或返回Promise.reject()则新的Promise实例为rejected状态。

2、fulfilled状态能够触发then回调

3、rejected状态能够触发catch回调

4、“紧跟Promise实例的then的参数等于resolve接受的参数;紧跟Promise实例的catch的参数等于reject接受的参数。”

其他解释

JavaScript 是单线程的,单线程意味着,所有任务都需要排队,前一个任务结束,才会执行后一个任务。

而这种 主线程从 “任务队列” 中读取执行事件,不断循环重复的过程,就被称为 事件循环(Event Loop)

如果前一个任务耗时很长,后一个任务就不得不一直等着,那么我们肯定要对这种情况做一些特殊处理,毕竟很多时候我们并不是完全希望它如此执行。

所以为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。

  • Node.js:Node.js 的 Event Loop 是基于 libuv。libuv 已经对 Event Loop 作出了实现。
  • 浏览器:浏览器的 Event Loop 是基于 HTML5 规范的。而 HTML5 规范中只是定义了浏览器中的 Event Loop 的模型,具体实现留给了浏览器厂商

libuv 是一个多平台支持库,主要用于异步 I/O。它最初是为 Node.js 开发的,现在 Luvit、Julia、pyuv 和其他的框架也使用它。Github - libuv 仓库

 浏览器 Event Loop

在讲解浏览器的 Event Loop 前,我们需要先了解一下 JavaScript 的运行机制:

  • 所有同步任务都在主线程上执行,形成一个 “执行栈”(execution context stack)。
  • 主线程之外,存在一个 “任务队列”(task queue),在走主流程的时候,如果碰到异步任务,那么就在 “任务队列” 中放置这个异步任务。
  • 一旦 “执行栈” 中所有同步任务执行完毕,系统就会读取 “任务队列”,看看里面存在哪些事件。那些对应的异步任务,结束等待状态,进入执行栈,开始执行。
  • 主线程不断重复上面三个步骤。

而 JavaScript 的异步任务,还细分两种任务:

  • 宏任务(Macrotask)script(整体代码)、setTimeoutsetIntervalXMLHttpRequest.prototype.onloadI/O、UI 渲染
  • 微任务(Microtask)PromiseMutationObserver、async await

Node.js Event Loop

Node.js 的 Event Loop 是基于 libuv。libuv 已经对 Event Loop 作出了实现。

Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
  • 定时器(timers):本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
  • 待定回调(pending callbacks):执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 Node 将在适当的时候在此阻塞。
  • 检测(check)setImmediate() 回调函数在这里执行。
  • 关闭的回调函数(close callbacks):一些关闭的回调函数,如:socket.on('close', ...)

有些时候,前端面试官还是会跟你扯 setTimeout & setImmediate 和 process.nextTice()

node中的特殊事件

  • setTimeout:众所周知,这是一个定时器,指定 n 毫秒后执行定时器里面的内容。
  • setImmediate:Node.js 发现使用 setTimeoutsetInterval 有些小弊端,所以设计了个 setImmediate,该方法被设计为一旦在当前轮询阶段完成,就执行这个脚本。
  • process.nextTick() nextTick 比较特殊,它存有自己的队列,并且它独立于 Event Loop,无论 Event Loop 处于何种阶段,都会在阶段结束的时候清空 nextTick 队列。

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

事实上这两个结局都是会存在的。

按照官网的解释:

  • 执行计时器的顺序将根据调用它们的上下文而异。
  • 如果两则都从主模块内调用,则计时器将受到进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。
  • 如果你将这两个函数放入一个 I/O 循环内调用,setImmediate 总是被有限调用使用

 setImmediate() 相对于 setTimeout 的主要优势是:如果 setImmediate() 是在 I/O 周期内被调度的,那么它将会在任何的定时器之前执行,跟这里存在多少个定时器无关出处。

  • 《不要混淆 nodejs 和浏览器中的 event loop》
  • 《浏览器与 Node 的事件循环(Event Loop)有何区别?》

浏览器和node的 异步任务区别

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任

浏览器和Node 环境下,microtask 任务队列的执行时机不同

  • Node端,microtask 在事件循环的各个阶段之间执行
  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

实际例总结

  • 注意setTime中有没有倒计时
  • 注意then后面的then 属于一个新的微任务
  • 注意new Promise 中没有return 之前都属于外部任务内的一部分

经典面试题

宏任务和微任务都空了会发生什么

总的来说,当宏任务和微任务队列都空了时,JavaScript引擎会检查是否有新的事件等待处理。如果没有,浏览器或Node.js环境会进入空闲状态,浏览器可能会进入空闲状态,停止执行JavaScript代码,并开始进行其他操作,如垃圾回收、页面渲染(如果有必要)等。此时,CPU资源可以释放给其他系统进程或浏览器内部的非JavaScript任务使用。。等待新的事件触发。一旦有新的事件产生,事件循环会继续运转,处理新产生的任务。这个过程确保了JavaScript应用能够及时响应外部事件,同时有效地利用系统资源。

经典面试题7个demo

 如果可以彻底掌握Event Loop面试题,基本上你能遇到的面试题都可以拿下

demo1、await 与微任务

难点:await 后的代码先执行还是外面同步 代码先执行

console.log('aaa');

(async ()=>{
  console.log('bbb');
  await console.log('ccc');
  console.log('eee')
})().then(()=>{
  console.log('fff')
});

console.log('ddd');

难点:

1、asyc 中 await 之前算同步还是异步?

2、await 后如果是同步代码 算同步还是异步

3、await 后跟的是同步代码,那 await 还会生效吗

一个结论:await其实等价于then(事实上他俩也确实是一个东西),都是将后续任务放到微任务队列中等待,而不会立即执行

  • 第1步、aaa不说了
  • 第2步、111是同步执行的,上面说过
  • 第3步、222这里很重要了,首先,console.log自己是同步的,所以立即就会执行,我们能直接看到222,但是await本身就是then,所以console.log(333)无法直接执行,而是老老实实去排队,而且,因为整个async并未执行完,它的then(也就是444)无法触发
  • 第4步、ddd应该也不用说,当前任务到这里执行完毕
  • 第5步、从任务队列中把333拉出来,并且执行了,这时整个async才算完成,所以把then推到队列中等待执行
  • 第6步、把console.log(444)拉出来执行,看到444

总结

  • Promise构造函数中的代码属于同步,包含 await 后跟的同步代码
  • then、catch中的代码才属于 微任务

demo2、宏任务与微任务

难点:微任务内的宏任务先执行还是外面宏任务先执行

console.log('aaa');

setTimeout(()=>console.log('t1'), 0);
(async ()=>{
  console.log(111);
  await console.log(222);
  console.log(333);

  setTimeout(()=>console.log('t2'), 0);
})().then(()=>{
  console.log(444);
});

console.log('bbb');

  • 第1步、毫无悬念aaa,过
  • 第2步、t1会放入任务队列等待
  • 第3步、111会直接执行,因为async本身不是异步的(上面有说)
  • 第4步、222也会直接执行,但是接下来的console.log(333);和setTimeout(()=>console.log('t2'), 0);就塞到微任务队列里等待了
  • 第5步、bbb毫无疑问,而且当前任务完成,优先执行微任务队列,也就是console.log(333)开始的那里
  • 第6步、执行333,然后定时器t2会加入任务队列等待(此时的任务队列里有t1和t2两个了),并且async完成,所以console.log(444)进入微任务队列等待
  • 第7步、优先执行微任务,也就是444,此时所有微任务都完成了
  • 第8步、执行剩下的普通任务队列,这时t1和t2才会出来

总结

  • 1、先将当前宏任务内的同步代码执行完
  • 2、然后执行当前宏任务内的微任务
    • 遇到宏任务直接丢入宏任务队列
  • 3、执行下一个宏任务

demo3 Promise 内嵌 Promise

难点:双层内嵌 Promise 第二个 then 的执行顺序?

setTimeout(() => {
  console.log('0');
}, 0)

new Promise((resolve, reject) => {
  console.log('1');  
  resolve();
}).then(() => {     // w1
  console.log('2');    
  
  // return
  new Promise((resolve, reject) => {
    console.log('3');
    resolve();   // w2
  }).then(() => {     // 📌
    console.log('4');
  }).then(() => {   // w4
    console.log('5');
  })
}).then(() => {
  console.log('6');   //w3 📌
})

new Promise((resolve, reject) => {
  console.log('7');
  resolve()
}).then(() => {        
  console.log('8');
})
//[ w1,w2,w3,w4]
//注释为📌的两个then是同层级的,所以按照执行顺序来打印

  • 同步代码中遇到 then 则将 then 中的代码(包含 then 后的 then 和 catch)加入微任务队列,后面遇到微任务继续加入队列
    • 同理宏任务嵌套宏任务也是如此
  • 直到当前微(宏)任务执行完

demo4

难点:多次resolve 以哪个为准

new Promise((resolve, reject) => {
    console.log('promise');

    for (var i = 0; i < 10000; ++i) {
        i === 9999 && resolve(i);
    }

    console.log('promise after for-loop');
}).then((v) => {
    console.log('promise1 i:'+v);
}).then(() => {
    console.log('promise2');

});

// promise1 i:  会是几?

console.log('start');

var intervalA = setInterval(() => {
    console.log('intervalA');
}, 0);

setTimeout(() => {
    console.log('timeout');

    clearInterval(intervalA);
}, 0);

var intervalB = setInterval(() => {
    console.log('intervalB');
}, 0);

var intervalC = setInterval(() => {
    console.log('intervalC');
}, 0);

new Promise((resolve, reject) => {
    console.log('promise');

    for (var i = 0; i < 10000; ++i) {
        i === 9999 && resolve();
    }

    console.log('promise after for-loop');
}).then(() => {
    console.log('promise1');
}).then(() => {
    console.log('promise2');

    clearInterval(intervalB);
});

new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('promise in timeout');
        resolve();
    });

    console.log('promise after timeout');
}).then(() => {
    console.log('promise4');
}).then(() => {
    console.log('promise5');

    clearInterval(intervalC);
});

Promise.resolve().then(() => {
    console.log('promise3');
});

console.log('end');

  1. clearInterval(intervalA); 运行的时候,实际上已经执行了 intervalA 的macrotask了\
  2. promise函数内部是同步处理的,不会放到队列中,放入队列中的是它的then或catch回调\
  3. promise的then返回的还是promise,所以在输出promise4后,继续检测到后续的then方法,马上放到microtask队列尾部,再继续取出执行,马上输出promise5;

  • 提前clearInterval 并不会销毁宏任务,因为当前宏任务已经加入 宏任务队列等待执行

demo5

难点:多个script 内的微任务宏任务 执行顺序??

    <script>
        console.log('start');

        setTimeout(() => {
            console.log('timeout1');
        }, 0);

        Promise.resolve().then(() => {
            console.log('promise1');
        });
    </script>
    <script>
        setTimeout(() => {
            console.log('timeout2');
        }, 0);

        requestAnimationFrame(() => {
            console.log('requestAnimationFrame');
        });

        Promise.resolve().then(() => {
            console.log('promise2');
        });

        console.log('end');
    </script>

demo6 new Promsie  后的throw问题

new Promise((res, rej) => {
  console.log(1);
  throw new Error('abc');  //抛出错误,下面的代码不执行
  res(2);
  console.log(3);
}).catch((e) => {

  console.log('catch');

}).then((t) => {

  console.log(t);

});

变形

new Promise((res, rej) => {

  console.log(1);
  
  res(2);   //状态发生转变后,后续的错误也无法捕获
  
  throw new Error('abc');

  console.log(3);


}).catch((e) => {

  console.log('catch');

}).then((t) => {

  console.log(t);

});
// 输出 1 --> 2

demo7 new Promise中的await问题

//快手一面
let a;
const b = new Promise((resolve, reject) => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
}).then(() => {
  console.log('promise3');
}).then(() => {
  console.log('promise4');
});

a = new Promise(async (resolve, reject) => {
  console.log(a);
  await b;
  console.log(a); 
  console.log('after1');
  await a  // 后面的代码属于新的微任务,所以,后面非同步代码 且a第一步构造函数内未经历resolve。故a有值状态为pending  
  resolve(true);
  console.log('after2');
}).then(data => {console.info(data)})

console.log('end');

其实以上难点集中在

let a
a = new Promise(async ()=>{
    console.log(0)
    await a;
    console.log(1)
    await a;
    console.log(2)
})

为什么console.log(2)不会执行呢

首先第一步 await a; 执行时当成同步代码,此时a为undefined,await a; 之后的代码为下一次微任务,此时同步代码执行完毕开始执行 第一个await a 后面的下一次微任务代码 , 第二个 await a;之前无 resolve 所以,第二个await a; 即a为pending状态的Promise,其之后的代码一直无法执行

原题可拆解为

let a;
const b = new Promise((resolve, reject) => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
}).then(() => {
  console.log('promise3');
}).then(() => {
  console.log('promise4');
});

a = new Promise(async (resolve, reject) => {
  console.log(a);
  b.then(() => {
  console.log(a); 
  console.log('after1');
  a.then((resolve)=>{  // a中没有resolve处理所有  a后面的都处于pending状态无法执行
        resolve(true);
        console.log('after2');
  })

})
 
}).then(data => {console.info(data)})

console.log('end');

以下是一个最简单的demo加深理解

let a = new Promise(()=>{})
new Promise(async (resolve, reject) => {
  console.log(0);
  resolve(1)
  console.log(2);

}).then(()=>{

    console.log(3)
})
// 输出 1,2,3

let a = new Promise(()=>{})
new Promise(async (resolve, reject) => {
  console.log(0);
  await a
  resolve(1)
  console.log(2);

}).then(()=>{

    console.log(3)
})
// 输出 0

为何1,2不会执行,将上面改造如下即可,观察代码可知第二个then  无resolve  和reject故始终保持pending   其后面的then代码始终无法执行

let a = new Promise(()=>{})
new Promise(async (resolve, reject) => {
  console.log(0);
}).then(()=>{
   console.log(3)
}).then(()=>{
}).then((resolve)=>{
  resolve(1)
  console.log(2);
}).then(()=>{
    console.log(3)
})
// 输出 0

知识点

  • 无论是写new Promise 还是直接const a= new Promise ;都会立即执行,
  • const a= new Promise 内部 取不到当前值
  • 构造函数中无 resolve 则后面的then 不会执行
  • 没有 resolve reject 则 不会往下执行
  • Promise构造函数内遇到await时,await后的代码看出then中的代码,如果 await之前没有resolve 则该构造函数始终保持pending 而无法走到自己的then状态

参考
深入理解JavaScript的事件循环(Event Loop) - 知乎
https://www.cnblogs.com/thatme/p/10159017.html
一篇文章解决Promise...then,async/await执行顺序类型题 - 掘金
什么是 Event Loop? - 阮一峰的网络日志
no-return-await - ESLint - Pluggable JavaScript Linter
事件大厂手写前端JS汇总【字节/快手】持续更新 - 掘金

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/514979.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unsafe类详解

1. Unsafe 概念 Unsafe类位于sun.misc包下&#xff0c;它是java实现高并发的基础&#xff0c;通过它可以执行一些不安全的操作&#xff0c;如像C语言一样直接操作内存资源&#xff0c; 它提供的这些方法增强了java对底层资源的操作能力&#xff0c;但同时也增加了程序出错的风…

typdef:深入理解C语言中typdef关键词的用法

typedef&#xff1a;C语言中的类型重命名关键词 在C语言中&#xff0c;typedef 是一个非常有用的关键词&#xff0c;它允许我们为现有的数据类型定义一个新的名称。这不仅使得代码更加清晰易读&#xff0c;还提高了代码的可维护性。在这篇博客中&#xff0c;我们将深入探讨 ty…

git中对子模块的本地修改、提交和推送远程仓库

场景 当前的某个项目&#xff0c;其使用了另一个项目&#xff0c;我在本地需要对子项目进行修改&#xff0c;并将这些修改提交到github中的子项目和父项目。其实在github中&#xff0c;子项目都是特定的指向子项目的某次提交&#xff0c;因此对于父项目的修改&#xff0c;其实…

Linux-Arm GDB调试(本地和远程)

目录 问题描述 已有coredump 没有coredump 小结 问题描述 Linux本机调试使用GDB非常方便&#xff0c;但嵌入式Linux设备资源有限&#xff0c;通常并没有交叉编译工具&#xff0c;那嵌入式设备上的应用发生问题如何查找问题&#xff1f;通常IDE有远程DEBUG功能&#xff0c;这…

OpenHarmony实战:标准系统移植指南

本文描述了移植一块开发板的通用步骤&#xff0c;和具体芯片相关的详细移植过程无法在此一一列举。后续社区还会陆续发布开发板移植的实例供开发者参考。 定义开发板 本文以移植名为MyProduct的开发板为例讲解移植过程&#xff0c;假定MyProduct是MyProductVendor公司的开发板…

[Arduino学习] ESP8266读取DHT11数字温湿度传感器数据

目录 1、传感器介绍 2、接线 3、DHT.h库 1、传感器介绍 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器&#xff0c;是简单环境监测项目的理想选择。 温度分辨率为1C&#xff0c;相对湿度为1&#xff05;。温度范围在0C到50C之间&#xff0c;湿度的测…

自注意力机制详解

视频链接&#xff1a;李宏毅 self-attention讲解上 参考文章&#xff1a;RNN详解      Attention详解      彻底搞懂Attention机制      知乎Transformer详解 传统的编码器解码器架构 一般最简单的编码器-解码器架构都是基于RNN模型的&#xff0c;编码器将输入…

突破校园网限速:使用 iKuai 多拨分流负载均衡(内网带宽限制通用)

文章目录 1. 简介2. iKuai 部署2.1 安装 VMware2.2 安装 iKuai(1) 下载固件(2) 安装 iKuai 虚拟机(3) 配置 iKuai 虚拟机(4) 配置 iKuai(5) 配置多拨分流 2.3 测试速度 1. 简介 由于博主连的内网是限速的&#xff0c;但是不同设备之间的网速却始终差不多&#xff0c;有一天看着…

CSS3新增的语法(三)【2D,3D,过渡,动画】

CSS3新增的语法&#xff08;三&#xff09;【2D,3D,过渡&#xff0c;动画】 10.2D变换10.1. 2D位移10.2. 2D缩放10.3. 2D旋转10.4. 2D扭曲&#xff08;了解&#xff09;10.5. 多重变换10.6. 变换原点 11. 3D变换11.1. 开启3D空间11.2. 设置景深11.3. 透视点位置11.4. 3D 位移11…

R语言中的常用数据结构

目录 R对象的基本类型 R对象的属性 R的数据结构 向量 矩阵 数组 列表 因子 缺失值NA 数据框 R的数据结构总结 R语言可以进行探索性数据分析&#xff0c;统计推断&#xff0c;回归分析&#xff0c;机器学习&#xff0c;数据产品开发 R对象的基本类型 R语言对象有五…

使用OMP复原一维信号(MATLAB)

参考文献 https://github.com/aresmiki/CS-Recovery-Algorithms/tree/master MATLAB代码 %% 含有噪声 % minimize ||x||_1 % subject to: (||Ax-y||_2)^2<eps; % minimize : (||Ax-y||_2)^2lambda*||x||_1 % y传输中可能含噪 yyw % %% clc;clearvars; close all; %% 1.构…

js类型转换

类型转换只有这四种&#xff0c;例如如果要对象转数字&#xff0c;那么就需要先把对象转成原始类型&#xff0c;再从原始类型转到数字。 空数组转原始类型是一个空字符串。空对象转原始类型是[object Object]。 let a {} console.log(a);// NaN //等价于 a->原始 然后原始…

线控悬架系统分析

线控悬架系统分析 附赠自动驾驶学习资料和量产经验&#xff1a;链接 1 线控悬架系统系统发展现状 • 车辆驾乘过程中&#xff0c;操控性和舒适性是两个重要的评价指标&#xff0c;两者很难兼顾&#xff1b; • 线控悬架就是根据路况实际情况自动调节悬架的高度、刚度、阻尼实…

OpenHarmony实战:小型系统移植概述

驱动主要包含两部分&#xff0c;平台驱动和器件驱动。平台驱动主要包括通常在SOC内的GPIO、I2C、SPI等&#xff1b;器件驱动则主要包含通常在SOC外的器件&#xff0c;如 LCD、TP、WLAN等 图1 OpenHarmony 驱动分类 HDF驱动被设计为可以跨OS使用的驱动程序&#xff0c;HDF驱动框…

【JAVA】基础学习03变量和关键字

文章目录 JAVA变量与运算符1.关键字&#xff08;keyword&#xff09;2.标识符( identifier)2.1命名规则2.2命名规范2.3变量作用和类型2.3.1整型变量2.3.2补充&#xff1a;计算机存储单位2.3.3浮点类型&#xff1a;float、double2.3.4 关于浮点型精度的说明2.3.5 字符类型&#…

Linux:ip和ip协议的初步认识

文章目录 ip协议基本认识ip协议的报头网段划分ip的类型划分 ip协议基本认识 前面对于TCP的内容已经基本结束了&#xff0c;那么这也就意味着在传输层也已经结束了&#xff0c;那么下一步要进入的是的是网络层&#xff0c;网络层中也有很多种协议&#xff0c;这里主要进行解析的…

【yolov5小技巧(1)】---可视化并统计目标检测中的TP、FP、FN

文章目录 &#x1f680;&#x1f680;&#x1f680;前言一、1️⃣相关名词解释二、2️⃣论文中案例三、3️⃣新建相关文件夹四、4️⃣detect.py推理五、5️⃣开始可视化六、6️⃣可视化结果分析 &#x1f440;&#x1f389;&#x1f4dc;系列文章目录 嘻嘻 暂时还没有~~~~ &a…

OpenHarmony实战:小型系统平台驱动移植

在这一步&#xff0c;我们会在源码目录//device/vendor_name/soc_name/drivers目录下创建平台驱动。 建议的目录结构&#xff1a; device ├── vendor_name │ ├── drivers │ │ │ ├── common │ │ │ ├── Kconfig # 厂商驱动内核菜单入口 │ …

线上线下陪玩,APP小程序H5。源码交付,支持二开!

线下陪玩的风险与管理方式 1、陪玩者的身心健康风险 线下陪玩的模式决定了陪玩者需要与不同的需求方见面&#xff0c;并满足他们的陪伴和娱乐需求。这种工作方式可能会给陪玩者带来身心上的压力和负担。因为陪玩者需要面对各种需求方的要求&#xff0c;有时还需要虚拟出一种完…

mac/win使用pyinstaller打包app/exe文件,活着执行脚本,双击运行

&#x1f338; 踩坑记录 python环境最好使用虚拟环境&#xff0c;推荐使用conda管理&#xff0c;并且若本地有python环境&#xff0c;不要使用和 本地环境版本 相同的虚拟环境 这里踩坑较多&#xff0c;已经记不清楚注意点 虚拟环境python版本不要和本地环境一样 mac/win只能…