任务队列
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,在这里列出比较关键的步奏
- 检查macrotask队列,运行最前面的任务,如果队列为空,前往第二步
- 检查mircotask队列,一直运行队列中的任务直到该队列为空
- 渲染过程
- 执行resize,scroll,媒体查询,动画,全屏等步奏
- 运行animation frame回调
- 运行IntersectionObserver回调
- 渲染
- 回到第一步
因此,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
过程如下
- t1,t2进timer队列,p1进入mircotask队列,i1进入setImmediate队列
- 运行process.nextTick,输出n1,清空mircotask队列,输出p1,又加入新的promise任务,输出p2
- 进入timers阶段,清空timer队列
- 运行t1,输出t1,然后t3,t4加入下一轮的timers队列,p3加入mircotask队列,i2,i3加入setImmediate队列,n2,n3加入process.nextTick队列,
- 运行t2,输出t2,n4加入process.nextTick队列
- 切换阶段,清空nextTick队列,输出n2,n3,n4,清空mircotask队列,输出p3
- 进入check阶段
- 运行i1,输出i1,t5加入timers队列
- 运行i2,输出i2,t6加入timers队列,输出i2,i3,n4加入nextTick,p4加入mircok队列
- 运行i3,输出i3,i4加入setImmediate队列
- 切换阶段,运行n4,输出n4,添加n5进队列,运行n5,输出n5,清空mircotask,运行p4,p5加入队列,输出p4,p5
- timers阶段 运行t3,t4,t5,t6
- check阶段,输出i4
可以发现,promise和nexttick的任务是添加在本次循环,其他的是添加到下次循环。并且,是按照timers->nexttick->mircotask->check->nexttick->mircotask->timers这样的流程来运行