本文阐述了浏览器端和node端的js运行机制执行的过程,还进行了两者的运行机制比较,以及同步任务和异步任务的说明,两种异步任务的必要性,以及各自有哪些回调,部分回调的优先级。
JS运行机制复述
首先js执行,会有一个函数执行栈(stack),一个任务队列(task queue),一个微任务队列(microtask queue),事件循环(event loop)。
- 主线程:函数执行栈用来存放同步任务,按照后进先出的顺序执行;
- 在任务队列中,存放的是宏任务。
- 当函数执行栈为空时,会启动事件循环机制,将任务队列放到执行栈中执行。在此之前,每从任务队列中取一个任务时,如果微任务队列中存在任务,就先把微任务执行完成,在执行任务队列中的任务。
- 依次循环,直到任务队列、微任务队列、函数执行栈均为空。
附:
同步任务:在主线程上执行的任务,只有前一个任务执行完成后才能执行下一个任务。
异步任务:不进入主线程执行,而是进入到任务队列(task queue)中执行。
Node.js中的事件循环
上段讲的是浏览器端的事件轮询,而node是多线程机制,由libuv库负责Node API的执行,将它分配给不同的线程,形成一个事件循环。
node中事件循环(event loop)大致分为六个部分。
- timer定时器:执行setTimeout以及setInterval的回调。
- I/O回调:处理网络、流、tcp错误等回调
- idle空转和prepare阶段:node内部使用
- poll轮询:执行poll中的I/O队列,检查定时器是否到时
- check检查:存放setImmediate回调
- close回调:关闭的回调
主要的是timer定时器、poll轮询、check 检查三大部分。在此我们只做了解。
事件循环过程:
- 执行全局Script的同步代码。
- 执行完同步代码调用栈清空后,执行微任务。先执行NextTick队列(NextTick Queue)中的所有任务,再执行其他微任务队列中的所有任务。
- 开始执行宏任务,上面6个阶段。从第1个阶段开始,执行相应每一个阶段的宏任务队列中所有任务。(每个阶段的宏任务队列执行完毕后,开始执行微任务),然后在开始下一阶段的宏任务,依次构成事件循环。
- timers Queue -> 执行微任务 -> I/O Queue -> 执行微任务 -> Check Queue 执行微任务 -> Close Callback Queue -> 执行微任务 ...
浏览器和Node端事件循环的差别
- 两者的运行机制完全不同,实现机制也不同。
- node.js可以理解成4个宏任务队列(timer、I/O、check、close)和2个微任务队列。但是执行宏任务时有6个阶段。
- node.js在开始宏任务6个阶段时,每个阶段都将该宏任务队列中所有任务都取出来执行,每个阶段的宏任务执行完毕后,开始执行微任务。但是浏览器中的事件循环,是只取一个宏任务执行,然后看微任务队列是否存在,存在执行微任务,然后再取一个宏任务,构成循环。
JS异步任务
js的异步任务分为两种:宏任务、微任务。一个宏任务里面可以拥有多个微任务,在执行js代码块的时候才会去执行内部的微任务。
宏任务
macrotask,也叫tasks。一些异步任务的回调会依次进入宏任务队列,等待后续背调用。
宏任务包括:
- setTimeout/setInterval
- setImmediate(Node独有)
- requestAnimationFrame(浏览器独有)
- I/O
- UI rendering(浏览器独有)
注意:
1、setTimeout延迟时间为0,与requestAnimationFrame比较:requestAnimationFrame优先级大于setTimeout。
2、setTimeout延迟时间为0,与setImmediate比较:不确定。
setTimeout(() => console.log('setTimeout'), 0) setImmediate(()=>{ console.log('setImmediate'); })
解释:
timer前的准备时间超过1ms,(loop到timer的时间大于1ms),则执行timer阶段(setTimeout)的回调函数。
timer前的准备时间小于1ms,则先执行check阶段(setTimeout)的回调函数,下次事件循环,再执行timer阶段的回调函数。
如果想要setImmediate先执行,可以使用fs文件包裹,确保在I/O回调阶段执行。这样时间循环,会先执行chack阶段,之后再执行timer阶段。
node版本中的setTimeout
setTimeout(() => { console.log(1) }) setTimeout(() => { console.log(2) Promise.resolve().then(function () { console.log('promise') }) }) setTimeout(() => { console.log(3) })
- node11以后的版本与浏览器端运行结果一致:1 2 promise 3。
- node11之前的版本,执行结果为:1 2 3 promise。它会先进入timer阶段,执行第一个setTimeout并打印。再执行第二个setTimeout并打印,并将Promise放入微任务队列中。然后执行第三个setTimeout并打印。事件循环在执行下一阶段时,先执行微任务队列,打印promise。
微任务
microtask,也叫jobs。除宏任务外的一些异步回调会依次进入微任务队列,等待后续被调用。 微任务包括:
- process.nextTick(Node独有)
- Promise.then()
- Object.observe
- MutationObserve
注意:
process.nextTick优先级高于Promise.then()。
两种异步任务的必要性
在异步任务队列中,遵循先进先出的原则。此时,在众多异步任务中,如果存在优先级较高的任务需要优先执行,那么只有一个异步任务队列是无法满足的,此时就需要引入微任务队列,将优先级较高的任务放到微任务队列中。如果微任务队列非空,则执行微任务队列,否则执行宏任务队列。
如果只有一种异步任务,那么优先级高的异步任务无法优先执行。
补充
async/await
async/await
本质上还是基于Promise
的一些封装
async函数在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,await之后的所有代码都是在Promise.then中的回调。