script
来执行(宏任务)promise
click
事件…不会立即执行,需要等待当前代码全部执行完毕script
执行完毕后,会清空所有的微任务例子
自动执行的情况 会输出 listener1 listener2 task1 task2
如果手动点击click 会一个宏任务取出来一个个执行,先执行click的宏任务,取出微任务去执行。会输出 listener1 task1 listener2 task2
console.log(1)
async function asyncFunc(){
console.log(2)
// await xx ==> promise.resolve(()=>{console.log(3)}).then()
// console.log(3) 放到promise.resolve或立即执行
await console.log(3)
// 相当于把console.log(4)放到了then promise.resolve(()=>{console.log(3)}).then(()=>{
// console.log(4)
// })
// 微任务谁先注册谁先执行
console.log(4)
}
setTimeout(()=>{
console.log(5)})
const promise = new Promise((resolve,reject)=>{
console.log(6)
resolve(7)
})
promise.then(d=>{
console.log(d)})
asyncFunc()
console.log(8)
// 输出 1 6 2 3 8 7 4 5
1. 浏览器事件循环
涉及面试题:异步代码执行顺序?解释一下什么是
Event Loop
?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变
js代码执行过程中会有很多任务,这些任务总的分成两类:
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。,我们用导图来说明:
我们解释一下这张图:
那主线程执行栈何时为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数
以上就是js运行的整体流程
面试中该如何回答呢? 下面是我个人推荐的回答:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
setTimeout
,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise
立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick
,同样将回调函数扔到微任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick
两个微任务,先执行哪个呢?process.nextTick
指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。
JS
在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到Task
(有多种task
) 队列中。一旦执行栈为空,Event
Loop
就会从Task
队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说JS
中的异步还是同步行为
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('script end');
不同的任务源会被分配到不同的
Task
队列中,任务源可以分为 微任务(microtask
) 和 宏任务(macrotask
)。在ES6
规范中,microtask
称为jobs
,macrotask
称为task
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
以上代码虽然
setTimeout
写在Promise
之前,但是因为Promise
属于微任务而setTimeout
属于宏任务
微任务
宏任务
script
setTimeout
setInterval
setImmediate
I/O
网络请求完成、文件读写完成事件UI rendering
宏任务中包括了
script
,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务
所以正确的一次 Event loop 顺序是这样的
Event loop
,执行宏任务中的异步代码通过上述的
Event loop
顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作DOM
的话,为了更快的响应界面响应,我们可以把操作DOM
放入微任务中
总结起来就是:
一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务
。
2. Node 中的 Event loop
当 Node.js 开始启动时,会初始化一个 Eventloop,处理输入的代码脚本,这些脚本会进行 API 异步调用,
process.nextTick()
方法会开始处理事件循环。下面就是 Node.js 官网提供的Eventloop
事件循环参考流程
Node
中的 Event loop
和浏览器中的不相同。Node
的 Event loop
分为6
个阶段,它们会按照顺序反复运行process.nextTick
node中的微任务,当前执行栈的底部,优先级比promise
要高整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。我们来分别看下这六个阶段都做了哪些事情。
setTimeout
和 setInterval
的回调函数,简单理解就是由这两个函数启动的回调函数。poll
阶段是一个重要且复杂的阶段,几乎所有 I/O
相关的回调,都在这个阶段执行(除了setTimeout
、setInterval
、setImmediate
以及一些因为 exception
意外关闭产生的回调)。检索新的 I/O 事件,执行与 I/O 相关的回调
,其他情况 Node.js 将在适当的时候在此阻塞。这也是最复杂的一个阶段,所有的事件循环以及回调处理都在这个阶段执行。这个阶段的主要流程如下图所示。setImmediate()
回调函数在这里执行,setImmediate
并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分
,如下代码所示。const fs = require('fs');
setTimeout(() => {
// 新的事件循环的起点
console.log('1');
}, 0);
setImmediate( () => {
console.log('setImmediate 1');
});
/// fs.readFile 将会在 poll 阶段执行
fs.readFile('./test.conf', {
encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('poll callback');
});
// 首次事件循环执行
console.log('2');
在这一代码中有一个非常奇特的地方,就是 setImmediate
会在 setTimeout
之后输出。有以下几点原因:
setTimeout
如果不设置时间或者设置时间为0
,则会默认为1ms
- 主流程执行完成后,超过
1ms
时,会将setTimeout
回调函数逻辑插入到待执行回调函数poll
队列中;- 由于当前
poll
队列中存在可执行回调函数,因此需要先执行完,待完全执行完成后,才会执行check:setImmediate
。
因此这也验证了这句话,
先执行回调函数,再执行 setImmediate
socket.on('close', ...)
除了把 Eventloop 的宏任务细分到不同阶段外。node 还引入了一个新的任务队列
Process.nextTick()
可以认为,Process.nextTick()
会在上述各个阶段结束时,在进入下一个阶段之前立即执行
(优先级甚至超过 microtask
队列)
事件循环的主要包含微任务和宏任务。具体是怎么进行循环的呢
process.nextTick
和 Promise
。微任务在事件循环中优先级是最高的
,因此在同一个事件循环中有其他任务存在时,优先执行微任务队列。并且process.nextTick 和 Promise
也存在优先级,process.nextTick
高于 Promise
setTimeout
、setInterval
、setImmediate
和 I/O
。宏任务在微任务执行之后执行,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列我们可以看到有一个核心的主线程,它的执行阶段主要处理三个核心逻辑。
const fs = require('fs');
// 首次事件循环执行
console.log('start');
/// 将会在新的事件循环中的阶段执行
fs.readFile('./test.conf', {
encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
setTimeout(() => {
// 新的事件循环的起点
console.log('setTimeout');
}, 0);
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('Promise callback');
});
/// 执行 process.nextTick
process.nextTick(() => {
console.log('nextTick callback');
});
// 首次事件循环执行
console.log('end');
分析下上面代码的执行过程
Promise.resolve 和 process.nextTick
,宏任务队列包含:fs.readFile 和 setTimeout
;process.nextTick 再执行 Promise.resolve
,所以先输出 nextTick callback
再输出 Promise callback
;宏任务插入先后顺序执行 setTimeout 再执行 fs.readFile
,这里需要注意,先执行 setTimeout
由于其回调时间较短,因此回调也先执行,并非是 setTimeout
先执行所以才先执行回调函数,但是它执行需要时间肯定大于 1ms
,所以虽然 fs.readFile
先于setTimeout
执行,但是 setTimeout
执行更快,所以先输出 setTimeout
,最后输出 read file success
。// 输出结果
start
end
nextTick callback
Promise callback
setTimeout
read file success
当微任务和宏任务又产生新的微任务和宏任务时,又应该