任务队列、事件循环与定时器

1,955 阅读4分钟

任务队列

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. 渲染过程
    1. 执行resize,scroll,媒体查询,动画,全屏等步奏
    2. 运行animation frame回调
    3. 运行IntersectionObserver回调
    4. 渲染
  4. 回到第一步

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

setTimeout(() => {
    console.log('t0')
    Promise.resolve().then(res => {
      console.log('p0')
    })
  })
  let i = 0
  function 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 = 0
        while (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

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之间的执行顺序是怎样的呢?

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

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队列
    1. 运行t1,输出t1,然后t3,t4加入下一轮的timers队列,p3加入mircotask队列,i2,i3加入setImmediate队列,n2,n3加入process.nextTick队列,
    2. 运行t2,输出t2,n4加入process.nextTick队列
  4. 切换阶段,清空nextTick队列,输出n2,n3,n4,清空mircotask队列,输出p3
  5. 进入check阶段
    1. 运行i1,输出i1,t5加入timers队列
    2. 运行i2,输出i2,t6加入timers队列,输出i2,i3,n4加入nextTick,p4加入mircok队列
    3. 运行i3,输出i3,i4加入setImmediate队列
  6. 切换阶段,运行n4,输出n4,添加n5进队列,运行n5,输出n5,清空mircotask,运行p4,p5加入队列,输出p4,p5
  7. timers阶段 运行t3,t4,t5,t6
  8. check阶段,输出i4

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

引用

Node 定时器详解