JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(
execution context stack
)。(2)主线程之外,还存在一个
"任务队列"(task queue)
。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。(3)一旦
"执行栈"
中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。(4)主线程不断重复上面的第三步。
主线程从"任务队列"
中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。但是浏览器的Event loop
和Node
的Event loop
是两个概念。
在JavaScript中,“任务队列”任务队列被分为MacroTask(宏任务)和MicroTask(微任务)两种。它们分别包含以下内容:
MacroTask: script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering
MicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver
在浏览器中"执行栈"
中的所有同步任务执行完毕,去“任务队列”
找要执行的任务,首先找MicroTask(微任务)
队列中是否有要执行的任务,有则全部执行,然后再找MacroTask(宏任务)
队列中是否有要执行的任务,如果有则执行MacroTask(宏任务)
队列的第一个,再去执行MicroTask(微任务)
队列中的所有微任务。
同步代码—>MicroTask—>MacroTask
每执行完一个Macrotask都会检查Microtask队列是否为空(执行完一个Macrotask的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有Microtask。然后再进入下一个循环去MacroTask(宏任务)队列中取下一个MacroTask(宏任务)执行,以此类推。
console.log('开始')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function () {
console.log('promise2')
})
}, 0)
setTimeout(() => {
console.log('timer2')
}, 0)
Promise.resolve().then(function () {
console.log('promise1')
})
/*
开始
promise1
timer1
promise2
timer2
*/
解析:
1、首先主线程的执行栈开始工作,从上到下执行,输出“1、开始”,遇到第一个setTimeout,将其推到“任务队列”的“宏任务”队列中。遇到第二个setTimeout,也将其推到“任务队列”的“宏任务”队列中排在第二位。遇到Promise,将其推到“任务队列”的“微任务”队列中。至此主线程执行完毕,去“任务队列”找任务。
2、“任务队列”中“微任务”队列中有任务,先执行,输出“2、promise1”。微任务”队列中所有任务执行完,再去找“宏任务”队列。
3、执行“宏任务”队列的第一个任务(第一个setTimeout),输出“3、timer1”,将Promise推到“微任务”队列。执行完一个“宏任务”,执行所有“微任务”,输出“4、promise2”。
4、执行完所有“微任务后”,再去检查“宏任务”,执行第二个setTimeout,输出“5、timer2”。全部任务结束。
nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别如下:
timers:执行setTimeout() 和 setInterval()中到期的callback。
I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
idle, prepare:队列的移动,仅内部使用
poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
check:执行setImmediate的callback
close callbacks:执行close事件的callback,例如socket.on(“close”,func)
不同于浏览器的是,在每个阶段完成所有阶段任务后(而不是MacroTask任务完成后,每个阶段可能会有好多任务,可以把6个阶段看做宏任务(MacroTask)的排序)microTask队列中的所有任务就会被执行。这就导致了同样的代码在不同的上下文环境下会出现不同的结果。
如上面代码1.1,在node环境下运行结果为。
/*
开始
promise1
timer1
timer2
promise2
*/
解析:
1、首先主线程的执行栈开始工作,从上到下执行,输出“1、开始”,遇到第一个setTimeout,将其推到“任务队列”的“timer阶段”队列中。遇到第二个setTimeout,也将其推到“任务队列”的“timer阶段”队列中排在第二位。遇到Promise,将其推到“任务队列”的“微任务”队列中。至此主线程执行完毕,去“任务队列”找任务。
2、“任务队列”中“微任务”队列中有任务,先执行,输出“2、promise1”。微任务”队列中所有任务执行完,再去找“timer阶段”队列。
3、执行“timer阶段”队列的第一个任务(第一个setTimeout),输出“3、timer1”,将Promise推到“微任务”队列。执行“timer阶段”队列的第二个任务(即第二个setTimeout),输出“4、timer2”。至此“timer阶段”的所有任务执行完毕。
4、执行“微任务”队列的任务,输出“5、promise2”全部任务结束。
console.log('开始');
setTimeout(() => {
console.log('setTimeout');
process.nextTick(()=>{
console.log('我是setTimeout内部的process.nextTick');
})
setImmediate(()=>{
console.log('内部setImmediate');
})
}, 0);
process.nextTick(()=>{
console.log('我是外部的process.nextTick');
})
setImmediate(()=>{
console.log('setImmediate');
process.nextTick(()=>{
console.log('我是setImmediate内部的process.nextTick');
})
})
/*
开始
我是外部的process.nextTick
setTimeout
我是setTimeout内部的process.nextTick
setImmediate
内部setImmediate
我是setImmediate内部的process.nextTick
*/
解析:
(1) 先执行同步 输出“1、开始”,将setTimeout推到timer阶段的队列中,将process.nextTick推到micro-task(微任务),将setImmediate推到check 阶段的队列中**。**
(2) 同步执行完毕,执行micro-task,执行process.nextTick,输出“**2、**我是外部的process.nextTick”,此时微服务队列中没有任务了。
(3)按6个阶段顺序执行,执行timer阶段队列中的任务,此时队列中只有setTimeout一个任务执行,输出“3、setTimeout”,将其内部的process.nextTick推送到micro-task(微任务)队列中,将其内部的setImmediate推送到check阶段的队列中(注意:此时check队列中有两个任务,一个是外部的setImmediate,一个是内部的setImmediate)。setTimeout(timer阶段)执行完毕,执行micro-task(微任务)队列,输出“4、我是setTimeout内部的process.nextTick”。micro-task(微任务)队列执行完毕。
(4)进入下一个阶段,由于没有i/o读写等操作,直接到check阶段,执行check阶段队列中的任务,执行外部的setImmediate,输出“5、setImmediate”,并将其内部的process.nextTick推送到micro-task(微任务)队列中,执行check阶段队列中的第二个任务setImmediate(内部的setImmediate),输出“6、内部的setImmediate”。check阶段队列中没有任务了,执行micro-task(微任务)队列中的任务,输出“7、我是setImmediate内部的process.nextTick”
process.tick()
会优于Promise
以上仅是我个人在学习过程中总结的自己的认识,难免会有错误,有任何问题还望不吝指出。
参考资料: