JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(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(宏任务)执行,以此类推。
浏览器示例 1.1
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') })
结果:
1. 开始
2. promise1
3. timer1
4. promise2
5. timer2
解析:
1、首先主线程的执行栈开始工作,从上到下执行,输出“1、开始”,遇到第一个setTimeout,将其推到“任务队列”的“宏任务”队列中。遇到第二个setTimeout,也将其推到“任务队列”的“宏任务”队列中排在第二位。遇到Promise,将其推到“任务队列”的“微任务”队列中。至此主线程执行完毕,去“任务队列”找任务。
2、“任务队列”中“微任务”队列中有任务,先执行,输出“2、promise1”。微任务”队列中所有任务执行完,再去找“宏任务”队列。
3、执行“宏任务”队列的第一个任务(第一个setTimeout),输出“3、timer1”,将Promise推到“微任务”队列。执行完一个“宏任务”,执行所有“微任务”,输出“4、promise2”。
4、执行完所有“微任务后”,再去检查“宏任务”,执行第二个setTimeout,输出“5、timer2”。全部任务结束。
在node中
nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别如下:
1. timers:执行setTimeout() 和 setInterval()中 到期 的callback。
2. I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
3. idle, prepare:队列的移动,仅内部使用
4. poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
5. check:执行setImmediate的callback
6. close callbacks:执行close事件的callback,例如socket.on(“close”,func)
不同于浏览器的是,在每个阶段完成所有阶段任务后(而不是MacroTask任务完成后,每个阶段可能会有好多任务,可以把6个阶段看做宏任务(MacroTask)的排序)microTask队列中的所有任务就会被执行。这就导致了同样的代码在不同的上下文环境下会出现不同的结果。
如上面浏览器中的示例1.1,在node环境下运行结果为。
1. 开始
2. promise1
3. timer1
4. timer2
5. 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”全部任务结束。
示例1.2
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'); }) })
结果:
1. 开始
2. 我是外部的process.nextTick
3. setTimeout
4. 我是setTimeout内部的process.nextTick
5. setImmediate
6. 内部setImmediate
7. 我是setImmediate内部的process.nextTick
解释:node中运行:
(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”
总结:
- 同一个上下文下,MicroTask会比MacroTask先运行
- 然后浏览器按照一个MacroTask任务,所有MicroTask的顺序运行,Node按照六个阶段的顺序运行,并在每个阶段后面都会运行MicroTask队列
- 同个MicroTask队列下
process.tick()
会优于Promise
以上仅是我个人在学习过程中总结的自己的认识,难免会有错误,有任何问题还望不吝指出。
参考资料: