答题大纲
- 先说基本知识点,宏任务、微任务有哪些
- 说事件循环机制过程,边说边画图出来
- 说async/await执行顺序注意,可以把 chrome 的优化,做法其实是违法了规范的,V8 团队的PR这些自信点说出来,显得你很好学,理解得很详细,很透彻。
- 把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就借助浏览器定时器触发线程的计时功能来实现
浏览器线程
- GUI渲染线程
-
- 绘制页面,解析HTML、CSS,构建DOM树等
- 页面的重绘和重排
- 与JS引擎互斥(JS引擎阻塞页面刷新)
- JS引擎线程
-
- js脚本代码执行
- 负责执行准备好的事件,例如定时器计时结束或异步请求成功且正确返回
- 与GUI渲染线程互斥
- 事件触发线程
-
- 当对应的事件满足触发条件,将事件添加到js的任务队列末尾
- 多个事件加入任务队列需要排队等待
- 定时器触发线程
-
- 负责执行异步的定时器类事件:setTimeout、setInterval等
- 浏览器定时计时由该线程完成,计时完毕后将事件添加至任务队列队尾
- 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
(整体代码)、setTimeout
、setInterval
、XMLHttpRequest.prototype.onload
、I/O
、UI 渲染 - 微任务(Microtask):
Promise
、MutationObserver、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 发现使用
setTimeout
和setInterval
有些小弊端,所以设计了个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');
- clearInterval(intervalA); 运行的时候,实际上已经执行了 intervalA 的macrotask了\
- promise函数内部是同步处理的,不会放到队列中,放入队列中的是它的then或catch回调\
- 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汇总【字节/快手】持续更新 - 掘金