执行栈
- 执行栈使用到的是数据结构中的栈结构, 它是一个存储函数调用的栈结构,遵循先进后出的原则。它主要负责跟踪所有要执行的代码。 每当一个函数执行完成时,就会从堆栈中弹出(pop)该执行完成函数;如果有代码需要进去执行的话,就进行 push 操作。
- 当执行这段代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
bar()
baz()
}
foo()
任务队列
-
任务队列使用到的是数据结构中的队列结构,它用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理。任务队列其实不止一种,根据任务种类的不同,可以分为微任务(micro task)队列和宏任务(macro task)队列
-
宏任务
-
浏览器为了能够使得JS内部(macrotask)与DOM任务能够有序的执行,会在一个(macrotask)执行结束后,在下一个(macrotask) 执行开始前,对页面进行重新渲染。浏览器会将此任务交给浏览器的其他线程来执行(比如遇到setTimeout任务,会交给定时器触发线程去执行,待计时结束,就会将定时器回调任务放入任务队列等待主线程来取出执行)
- 宏任务->渲染->宏任务
- 每个宏任务在执行时,会创建自己的微任务队列
- 微任务的执行时长会影响当前宏任务的时长。比如一个宏任务在执行过程中,产生了 10 个微任务,执行每个微任务的时间是 10ms,那么执行这 10 个微任务的时间就是 100ms,也可以说这 10 个微任务让宏任务的执行时间延长了 100ms
- script(主程序代码),setTimeout, setInterval, setImmediate, I/O, UI rendering,UI交互事件,postMessage,MessageChannel,requestAnimationFrame
-
微任务
-
浏览器引擎不会将微任务交给浏览器的其他线程去执行,而是将任务回调存在微任务队列(只有一个)。
-
引入微任务的目的是:给紧急任务一个插队的机会,否则新入队的任务永远被放在队尾。区分了微任务和宏任务后,本轮循环中的微任务实际上就是在插队,这样微任务中所做的状态修改,在下一轮事件循环中也能得到同步
-
- 宏任务->微任务->渲染->宏任务
-
无需等渲染,也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)
-
在每次页面渲染之前是会清空所有的微任务(micro)。这也就意味着,我们对于 Dom 的操作如果放在微任务之中是会让 UI 线程进行一次少的绘制(更快的展示在用户视野中)。
-
process.nextTick, Promise.then.catch.finally回调回调, Object.observe, MutationObserver,await 后面的函数是同步的,下方的剩余代码是微任务、 V8 的垃圾回收过程
-
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
等价于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
通过浏览器渲染理解宏任务和微任务
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';
- 同属于一个宏任务,所以最终合并渲染成pink
document.body.style = 'background:blue';
setTimeout(()=>{
document.body.style = 'background:black'
},200)
- 每执行一次宏任务就会渲染页面
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:pink'
});
console.log(3);
- 因为微任务在宏任务之后执行,渲染页面之前执行,所以最终页面只会呈现pink
事件循环
浏览器事件循环
- JavaScript在执行代码时,会将同步的代码按照顺序排在执行栈(call stack)中,然后依次执行里面的函数。当遇到异步任务时,就将其放入任务队列中,等待当前执行栈所有同步代码执行完成之后,就会从异步任务队列中取出已完成的异步任务的回调并将其放入执行栈中继续执行,如此循环往复,直到执行完所有任务。
- 通过不断循环,去取出异步任务的回调来执行,这个过程就是事件循环,每一次循环就是一个事件周期。
- JavaScript 引擎首先从宏任务队列中取出第一个任务(如script)。
- 执行完毕后,再将微任务中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行,也就是说在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行。
- 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完
Node 事件队列
原生的libuv事件循环中的队列主要有 4 种类型:
- 过期的定时器和间隔队列
- IO 事件队列
- Immediates 队列
- close handlers 队列
除此之外,Node.js 还有两个中间队列
- Next Ticks 队列
- Other Microtasks 队列
Node 事件循环
- node11之前,会在事件循环的各个阶段之间执行,也就是一个阶段所有宏任务执行完毕,就会去执行microtask队列的任务。
- 当node升级到11后,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,这点就跟浏览器端一致。
- 但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现
Node.js的运行机制如下:
- V8引擎解析JavaScript脚本。
- 解析后的代码,调用Node API。
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
- V8引擎再将结果返回给用户。
node 中的 I/O 和 非 I/O 操作
- 非 I/O操作
- 定时器(setTimeout,setInterval)
- microtask(promise)
- process.nextTick
- setImmediate
- DNS.lookup
- I/O操作
- 网络I/O
- 各个平台的实现机制不一样,linux 是 epoll 模型,类 unix 是 kquene 、windows 下是高效的 IOCP 完成端口、SunOs 是 event ports,libuv 对这几种网络I/O模型进行了封装。
- 文件I/O 与DNS操作
- 网络I/O
node 线程池
- libuv 内部维护着一个默认4个线程的线程池,这些线程负责执行文件I/O操作、DNS操作、用户异步代码。当 js 层传递给 libuv 一个操作任务时,libuv 会把这个任务加到队列中
- 线程池中的线程都被占用的时候,队列中任务就要进行排队等待空闲线程
- 线程池中有可用线程时,从队列中取出这个任务执行,执行完毕后,线程归还到线程池,等待下个任务。同时以事件的方式通知 event-loop,event-loop 接收到事件执行该事件注册的回调函数
- 线程池数量可以通过设置环境变量 UV_THREADPOOL_SIZE 来调整,出于系统性能考虑,libuv 规定可设置线程数不能超过128个
libuv引擎中的事件循环
- 其中libuv引擎中的事件循环分 6 个阶段执行,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
- 各个阶段主要执行内容如下图
- 执行 js 阶段运行过程
从上图中,大致看出node中的事件循环的顺序逐个阶段自上而下执行,没有跳过的逻辑,并且这些阶段描述的都是宏任务
- timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调,由 poll 阶段控制。
- I/O callbacks 阶段:主要执行系统级别的回调函数,比如 TCP 连接失败的回调,以及处理一些上一轮循环中的少数未执行的 I/O 回调
- idle, prepare 阶段:空闲阶段、预备状态,仅node内部使用
- poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等,需要注意的是这一阶段会存在阻塞(也就意味着这之后的阶段可能不会被执行)。
- 有则执行,无则阻塞,直到超出timeout的时间
- check 阶段:执行 setImmediate() 的回调
- close callbacks 阶段:执行关闭请求的回调函数,比如socket.on(‘close’, …)。
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
//判断事件循环是否存活。
r = uv__loop_alive(loop);
//如果没有存活,更新时间戳
if (!r)
uv__update_time(loop);
//如果事件循环存活,并且事件循环没有停止。
while (r != 0 && loop->stop_flag == 0) {
//更新当前时间戳
uv__update_time(loop);
//执行 timers 队列
uv__run_timers(loop);
//执行由于上个循环未执行完,并被延迟到这个循环的I/O 回调。
ran_pending = uv__run_pending(loop);
//内部调用
uv__run_idle(loop);
//内部调用
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
//计算距离下一个timer到来的时间差。为 0 则跳过 poll
timeout = uv_backend_timeout(loop);
//进入 轮询 阶段,该阶段轮询I/O事件,有则执行,无则阻塞,直到超出timeout的时间。
uv__io_poll(loop, timeout);
//进入check阶段,主要执行 setImmediate 回调。
uv__run_check(loop);
//进行close阶段,主要执行 **关闭** 事件
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
//更新当前时间戳
uv__update_time(loop);
//再次执行timers回调。
uv__run_timers(loop);
}
//判断当前事件循环是否存活。
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
上面六个阶段都不包括 process.nextTick()
- node11之前
- 它会在上述各个阶段结束时,在进入下一个阶段之前立即执行。
- 在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务(Node11之前)
- node11之后
- 在同步任务执行完毕后,即将 micro-task 推入栈中时优先会将 Process.nextTick 推入栈中进行执行
- 表示当前调用栈清空后立即执行的逻辑(官方并不将它认为是 EventLoop 中的一部分,即不认为是微任务)
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.error('promise')
})
process.nextTick(() => {
console.error('nextTick')
})
//当前调用栈清空之后会立即清空所有 nextTick,然后才进入事件循环,输出:nextTick-》promise-〉timeout
timer 阶段
- timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。
- 这些回调被保存在一个最小堆(min heap) 中. 这样引擎只需要每次判断头元素, 如果符合条件就拿出来执行, 直到遇到一个不符合条件或者队列空了, 才结束 Timer Phase.
- 同时为了防止某个 Phase 任务太多, 导致后续的 Phase 发生饥饿的现象, 所以消息循环的每一个迭代(iterate) 中, 每个 Phase 执行回调都有个最大数量. 如果超过数量的话也会强行结束当前 Phase 而进入下一个 Phase. 这一条规则适用于消息循环中的每一个 Phase.
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
//取出定时器堆中超时时间最近的定时器句柄
heap_node = heap_min((struct heap*) &loop->timer_heap);
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
// 判断最近的一个定时器句柄的超时时间是否大于当前时间,如果大于当前时间,说明还未超时,跳出循环。
if (handle->timeout > loop->time)
break;
// 停止最近的定时器句柄
uv_timer_stop(handle);
// 判断定时器句柄类型是否是repeat类型,如果是,重新创建一个定时器句柄。
uv_timer_again(handle);
//执行定时器句柄绑定的回调函数
handle->timer_cb(handle);
}
}
Pending I/O Callback 阶段
- 上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
- 比如执行 fs.read, socket 等 IO 操作的回调函数, 同时也包括各种 error 的回调.
idle, prepare 阶段
- 空闲阶段、预备状态,仅 node 内部使用
poll 阶段
poll 是一个至关重要的阶段
- 比如文件I/O,网络I/O等等,那么当这些异步操作做完了,就会来通知 js 主线程,怎么通知呢?就是通过’data’、'connect’等事件使得事件循环到达 poll 阶段
- 当 js 层代码注册的事件回调都没有返回的时候,事件循环会阻塞在poll阶段
- 首先会判断后面的 Check Phase 以及 Close Phase 是否还有等待处理的回调. 如果有, 则不等待, 直接进入下一个阶段
- 如果没有其他回调等待执行, 它会给 epoll 这样的方法设置一个 timeout.
- timeout 设置为多少合适呢? 答案就是 Timer Phase 中最近要执行的回调启动时间到现在的差值, 假设这个差值是 detal. 因为 Poll Phase 后面没有等待执行的回调了. 所以这里最多等待 delta 时长
- 如果期间有事件唤醒了消息循环, 那么就继续下一个 Phase 的工作
- 如果期间什么都没发生, 那么到了 timeout 后, 消息循环依然要进入后面的 Phase, 让下一个迭代的 Timer Phase 也能够得到执行
- 在进入该阶段时如果设定了 timer
- 如果此阶段即使产生了 timer 也并不会在本次 Loop 中执行,因为此时 EventLoop 已经到达 poll 阶段了。
check 阶段
- setImmediate()的回调会被加入check队列中
- Poll Phase 阶段可能设置一些回调, 希望在 Poll Phase 后运行. 所以在 Poll Phase 后面增加了这个 Check Phase
Close Callbacks 阶段
- 专门处理一些 close 类型的回调. 比如 socket.on(‘close’, …). 用于资源清理.
我们先来看个例子(Node11之前):
- 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出start end,并将2个timer依次放入timer队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出promise3
- 然后进入timers阶段,执行timer1的回调函数,打印timer1,并将promise.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;这点跟浏览器端相差比较大,timers阶段有几个setTimeout/setInterval都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于Node与浏览器的 Event Loop 差异,下文还会详细介绍)。
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//nodejs:要看第一个定时器执行完,第二个定时器是否在完成队列中,若在,不在则和浏览器显示相同
// start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
//浏览器:start=>end=>promise3=>timer1=>promise1=>timer2=>promise2
setTimeout 和 setImmediate 谁先执行
二者非常相似,区别主要在于调用时机不同。
- 同步代码执行完毕时,会在 timer 以及 check 阶段分别推入对应的 timer 函数和 immediate 函数
- setImmediate属于check观察者,结果保存在链表上,每次循环只执行链表上的一个回调函数,这样设计是为了保证每轮循环能够较快地执行结束,防止CPU占用过多而阻塞后续I/O调用的情况。
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
当性能较差时
- 在 node 中所谓 setTimeout(cb,0) 实际上存在最小执行时间 1 ms,它是会被当作 setTimeout(cb,1) 来执行,在浏览器中是setTimeout(cb,4)
- 在同步代码执行完毕,以及进入 EventLoop 中这一切发生在 1ms 之外
- 到 timer 阶段时,因为定时器满足时间它应该被推入对应的 timers 队列中。当 EventLoop 执行到 timer 阶段时,会拿出这个 timer 的 callback 执行它,timer会优先执行
- 接下来会依次进入 pending callbacks ,显示上一次 EventLoop 中并不存在任何达到上限的操作。所以它是空的。
- 依次进入 idle prepare 阶段。
- 进入 poll 轮询阶段,此时 poll 阶段并不存在任何 IO 相关回调,在轮询阶段会检测到我们代码中存在 setImmediate ,并且 setImmediate 的 callback 也已经被推入到了 check 阶段。
- 所以,在 poll 并不会产生所谓的阻塞效果。会进入 check 阶段,调用代码中 setImmediate 产生的回调函数 immediate
- check 阶段清完成 immediate 后,会进入 Loop 中最后的 close callbacks 中。
当性能优越时:
- 在同步代码执行完毕,以及进入 EventLoop 中这一切发生在 1ms 之内
- timers 阶段由于代码中的 setTimeout 并没有达到对应的时间,它所对应的 callback 并没有被推入当前 timer 中。
- 依次进入接下的阶段。Loop 会依次向下进行检查。当执行到 poll 阶段时,即使定时器对应的 timer 函数已经被推入 timers 中了。由于 poll 阶段检查到存在 setImmediate 所以会继续进入 check 阶段并不会掉头重新进入 timers 中。
- 当close阶段执行完成后才会进入timer阶段。
保证 setImmediate 比 setTimeout 快
- 相当于保证在 EventLoop 中的 timers 阶段之后调用它们两个
- 比如在I/O回调中执行
const fs = require('fs')
// 在IO结束的callback执行 也就是 poll 阶段会执行该 fs.readFile callback
// IO回调中存在 setImmediate 那么EventLoop的下一个阶段一定会进入check阶段
// 进而一定会优先执行 setImmediate 的回调
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
setTimeout 和 setInterval
- 它们的实现原理不需要I/O线程池的参与,创建的定时器会被插入到定时器观察者内部的一个红黑树中。采用红黑树的时间复杂度为O(lgn)
- process.nextTick();不需要动用红黑树等,操作相对轻量。时间复杂度为O(1)
process.nextTick=function(callback){
...
nextTickQueue.push(callback);
...
}
setImmediate
- setImmediate属于check观察者,结果保存在链表上,每次循环只执行链表上的一个回调函数,这样设计是为了保证每轮循环能够较快地执行结束,防止CPU占用过多而阻塞后续I/O调用的情况。
- 在 I/O 事件、定时器和其他预定任务之后,check 阶段运行回调
process.nextTick
- 属于idle观察者,idle观察者优先I/O观察者,I/O观察者优先于check观察者
- 结果保存在数组中,每次循环会全部执行
- 在当前执行栈清空后,在事件循环的下一个阶段开始之前,任务过多会阻塞事件循环
-
process.nextTick插队带来的危害
- process.nextTick的回调会导致事件循环无法进入到下一个阶段。I/O处理完成或者定时器过期后仍然无法执行。会让其它的事件处理程序处于饥饿状态,为了防止这个问题,Node.js提供了一个process.maxTickDepth(默认为1000)。
// 加入两个nextTick()的回调函数
process.nextTick(function () {
console.log('nextTick延迟执行1');
});
process.nextTick(function () {
console.log('nextTick延迟执行2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
console.log('setImmediate延迟执行1');
// 进入下次循环
process.nextTick(function () {
console.log('强势插入');
});
});
setImmediate(function () {
console.log('setImmediate延迟执行2');
});
console.log(’正常执行’);
//其执行结果如下:
//正常执行
//nextTick延迟执行1
//nextTick延迟执行2
//setImmediate延迟执行1
//强势插入
//setImmediate延迟执行2
- 第一个setImmediate回调执行后,并没有立即执行第二个setImmediate回调,而是进入下一轮循环,再次按再次按process.nextTick()优先、setImmediate()次后的顺序执行。之所以这样设计,是为了保证每轮循环能够较快地执行结束,防止CPU占用过多而阻塞后续I/O调用的情况。
异步流程代码示例:
- 每次 for 循环遇到 setTimeout 都将其放入事件队列中等待执行,直到全部循环结束,i 作为全局变量当循环结束后 i = 10 ,再来执行 setTimeout 时 i 的值已经为 10 , 结果为十个10
- 将 var 改为 let ,变量作用域不同,let 作用在当前循环中(「let所声明的变量,只在let命令所在的代码块内有效」),所以进入事件队列的定时器每次的 i 不同,最后打印结果会是 0 1 2…9。
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
- 前两个setTimeout会放进宏任务队列
- 同步执行打印3
- 将两个Promise.then放进微任务队列
- 同步打印4,整段script宏任务执行完毕
- 执行两个微任务,打印1,2
- 执行第一个放进宏任务队列的setTimeout,打印h1
- 将h1后的Promise.then放进微任务队列
- 第一个放进宏任务队列的setTimeout执行完毕,执行所有微任务队列的微任务,打印3
- 微任务队列执行完毕,执行放进宏任务队列的第二个setTimeout,打印h2
//执行结果:3-4-1-2-h1-3-h2
setTimeout(() => {
console.log('h1');
Promise.resolve(3).then((res) => {
console.log(res);
})
}, 0)
setTimeout(() => {
console.log('h2');
}, 0)
new Promise(() => {
console.log(3);
})
Promise.resolve(1).then((res) => {
console.log(res);
})
Promise.resolve(2).then((res) => {
console.log(res);
})
console.log(4);
执行结果:1-7-2-3-8-4-6-5-0
- 遇到第一个setTimeout,放进宏任务队列
- 执行同步代码打印1,将然后将.then放进微任务队列,因为该.then还未执行,不会将外围第二个.then放进微任务队列,只在该.then执行完毕后,才会将外围.then放进微任务队列
- 此时微任务队列中有2所在的这个微任务
- 执行同步任务打印7,将8这个.then放进微任务队列
- 此时微任务队列有2所在的这个微任务->8这个.then
- 整段script宏任务执行完毕,执行微任务队列中的任务
- 执行微任务队列中第一个.then回调,打印2,然后同步打印3,遇到4这个.then,又放进微任务队列末尾,此时第一个.then执行完毕,然后将6所在的.then放进微任务队列
- 此时微任务队列有8所在的微任务->4所在的微任务->6所在的微任务
- 执行微任务中第二个8这个.then,打印8
- 此时8这个微任务执行完毕,然后执行4所在的微任务,打印4,然后将4之后的5所在的这个.then放进微任务队列
- 此时微任务队列还有6、5
- 最后打印6、5
- 微任务队列执行完毕,执行宏任务0
- 先遇到setTimout,放进宏任务队列中
- 执行同步任务打印a、b
- 遇到.then,放进微任务队列中
- 执行同步任务打印d
- 此时整段script宏任务执行完毕,执行微任务
- 执行c所在的微任务,打印c,遇到宏任务,放进宏任务队列中
- 微任务执行完毕,开始执行宏任务
- 打印setTimout,然后打印.then中的setTimeout
//打印a b d c setTimeout then中的setTimeout
setTimeout(function(){
console.log('setTimeout')
}, 0)
const p = new Promise(resolve => {
console.log('a')
resolve()
console.log('b')
})
p.then(() => {
console.log('c')
setTimeout(function(){
console.log('then中的setTimeout')
}, 0)
})
console.log('d')
- 打印同步任务a、b
- 遇到c所在的.then,放进微任务队列中
- 遇到e所在的setTimout,放进宏任务队列中
- 遇到h所在的setTimout,放进宏任务队列中
- 此时整段script宏任务执行完毕,开始清空微任务队列
- 执行c所在的.then,打印c,遇到d所在的setTimout放进宏任务队列中
- 此时宏任务队列有:e所在的宏任务->h所在的宏任务->d所在的宏任务
- 虽然e位置优先,但e的执行时间晚于其他,所以实际调用顺序是:h所在的宏任务->d所在的宏任务->e所在的宏任务
- 此时所有微任务执行完毕,开始执行宏任务
- 执行h所在的宏任务,打印h、j,遇到i所在的.then放进微任务队列中
- 此时h所在的宏任务执行完毕,清空微任务,执行打印i
- 此时微任务队列清空,执行宏任务
- 执行d所在的宏任务,打印d,然后执行e所在的宏任务
- 打印e、f,遇到微任务g放进微任务队列,此时e所在宏任务结束,清空微任务队列
- 打印g
//打印a b c h j i d e f g
console.log('a');
new Promise(resolve => {
console.log('b')
resolve()
}).then(() => {
console.log('c')
setTimeout(() => {
console.log('d')
}, 0)
})
setTimeout(() => {
console.log('e')
new Promise(resolve => {
console.log('f')
resolve()
}).then(() => {
console.log('g')
})
}, 100)
setTimeout(() => {
console.log('h')
new Promise(resolve => {
resolve()
}).then(() => {
console.log('i')
})
console.log('j')
}, 0)