任务队列

js是单线程的,因为js可以操作DOM,如果多线程的话,会造成冲突的问题。

js的任务分为同步任务和异步任务。同步任务是指在主线程上依次执行的任务,形成一个执行栈。而异步任务不在主线程,在任务队列中,如网络请求,定时器等。在执行栈的任务执行完毕之后,系统会检查任务队列,看是否有可以执行的异步任务。-

而任务队列分为两种,一种是mircotask,另一种是marcotask。按照我的理解,mircotask和marcotask的区别在于mircotask的任务可以在本次循环/页面刷新前被加入到任务队列,而marcotask不可以

mircotask

  • promise
  • mutation.oberver
  • process.nextTick

marcotask

  • setTimeout,setInterval
  • requestAnimationFrame
  • 解析HTML
  • 执行主线程js代码
  • 修改url
  • 页面加载
  • 用户交互

浏览器篇

浏览器的event loops由HTML标准而不是ECMAScript定义,具体可以查看event-loop-processing-model,在这里列出比较关键的步奏

  1. 检查macrotask队列,运行最前面的任务,如果队列为空,前往第二步
  2. 检查mircotask队列,一直运行队列中的任务直到该队列为空
  3. 渲染过程

  4. 执行resize,scroll,媒体查询,动画,全屏等步奏

  5. 运行animation frame回调
  6. 运行IntersectionObserver回调
  7. 渲染

  8. 回到第一步

因此,eventloop分为三个阶段,执行一个marcotask,清空mircotask队列,运行render阶段
用代码验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setTimeout(()=>{
console.log('t0')Promise.resolve().then(res =>{
console.log('p0')})})
let i =0function raf (){
console.log(i)
document.querySelector('div').style.width = i *20+'px'Promise.resolve().then(res => console.log('p'+ i))
setTimeout(()=>{
console.log('t'+ i)if(i ===1){
document.querySelector('div').style.background ='red'
document.querySelector('div').style.height ='50px'}if(i ===2){
let j =0while(j++<1000000000){}
document.querySelector('div').style.background ='blue'
document.querySelector('div').style.height ='300px'}if(i ===3){
document.querySelector('div').style.width ='40px'}Promise.resolve(3).then(res =>{
console.log('tp'+ i)})})if(++i <=10){
requestAnimationFrame(raf)}}
requestAnimationFrame(raf)

输出结果为t0,p0,0,p1,t1,tp1,1,p2,t2,tp2,2,p3,p4,t4,tp4,t4,tp4,4...

使用chrome dev tool的performance查看过程

在Event log选项卡,分析一下过程,可以观察到,代码执行顺序为,timer,animation frame,paint,而timer和animation frame中又会执行属于各自顺序的mircotasks。尽管在i=2的时候会阻塞代码,然而还是会执行ainmtion frame的代码。

nodejs篇

nodejs六阶段

看这篇文章就够了,The Node.js Event Loop, Timers, and process.nextTick()

nodejs的事件循环有六个阶段

  • timers: setTimeout,setInterval
  • pending callbacks: 上一轮残留的IO回调
  • idle,prepare: 内部使用
  • poll:接受新的IO事件,处理其他阶段不处理的回调,node在合适的情况会停留在该阶段
  • check: setImmediate的回调
  • close callbacks: 关闭的回调

每个阶段有自己的callback队列,清空了队列或被执行的callback达到最大限制,进入下一个阶段,此时会运行process.nextTick

1
2
3
setImmediate(()=> console.log(2));
setTimeout(()=> console.log(1));Promise.resolve().then(()=> console.log(4));
process.nextTick(()=> console.log(3));(()=> console.log(5))();

输出5,3,4,1,2

nodejs定时器

nodejs有四种定时器,setTimeout和setInterval算是一种类型,另外还有setImmediate和process.nextTick两种类型。他们跟mircotask如promise之间的执行顺序是怎样的呢?

有点摸不到头绪?写个代码看下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
setTimeout(()=>{
console.log('t1')
setTimeout(()=>{console.log('t3')})
setTimeout(()=> console.log('t4'))Promise.resolve(1).then(res => console.log('p3'))
setImmediate(()=>{
console.log('i2')
setTimeout(()=> console.log('t6'))
process.nextTick(()=>{
console.log('n4')
process.nextTick(()=>{
console.log('n5')})})Promise.resolve(1).then(res => console.log('p4')).then(res => console.log('p5'))})
setImmediate(()=>{
console.log('i3')
setImmediate(()=> console.log('i4'))})
process.nextTick(()=> console.log('n2'))
process.nextTick(()=> console.log('n3'))})
setTimeout(res =>{
process.nextTick(()=> console.log('n4'))
console.log('t2')})Promise.resolve(1).then(res => console.log('p1')).then(res => console.log('p2'))
process.nextTick(()=> console.log('n1'))
setImmediate(()=>{
console.log('i1')
setTimeout(()=> console.log('t5'))})

输出结果是n1,p1,p2,t1,t2,n2,n3,n4,p3,i1,i2,i3,n4,n5,p4,p5,t3,t4,t5,t6,i4

过程如下

  1. t1,t2进timer队列,p1进入mircotask队列,i1进入setImmediate队列
  2. 运行process.nextTick,输出n1,清空mircotask队列,输出p1,又加入新的promise任务,输出p2
  3. 进入timers阶段,清空timer队列

  4. 运行t1,输出t1,然后t3,t4加入下一轮的timers队列,p3加入mircotask队列,i2,i3加入setImmediate队列,n2,n3加入process.nextTick队列,

  5. 运行t2,输出t2,n4加入process.nextTick队列

  6. 切换阶段,清空nextTick队列,输出n2,n3,n4,清空mircotask队列,输出p3

  7. 进入check阶段

  8. 运行i1,输出i1,t5加入timers队列

  9. 运行i2,输出i2,t6加入timers队列,输出i2,i3,n4加入nextTick,p4加入mircok队列
  10. 运行i3,输出i3,i4加入setImmediate队列

  11. 切换阶段,运行n4,输出n4,添加n5进队列,运行n5,输出n5,清空mircotask,运行p4,p5加入队列,输出p4,p5

  12. timers阶段 运行t3,t4,t5,t6
  13. check阶段,输出i4

可以发现,promise和nexttick的任务是添加在本次循环,其他的是添加到下次循环。并且,是按照timers->nexttick->mircotask->check->nexttick->mircotask->timers这样的流程来运行