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

任务队列

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阶段
用代码验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

1
2
3
4
5
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
24
25
26
27
28
29
30
31
32
33
34
35
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 定时器详解

支持作者

如果我的文章对你有帮助,欢迎 关注和 star 本博客 或是关注我的 github,获取更新通知。欢迎发送邮件到hpoenixf@foxmail.com与作者交流