1 事件循环机制
因为js 是单线程的,如果有两个线程对DOM的操作有冲突的,为了解决阻塞的情况,所以有一个event loop 机制。
当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。
当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。 这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,变量以及这个作用域的this对象。
而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是一系列的方法被排队在执行栈中。
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。
如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。
这个过程反复进行,直到执行栈中的代码全部执行完毕。
一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出。以上的过程说的都是同步代码的执行。
js引擎遇到一个异步事件后,js会将这个事件加入事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈执行完毕,它会立刻先处理微任务队列中的事件,然后再去宏任务队列中取出一个事件。
主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
2 宏任务和微任务
不同的异步任务被分为两类:
-
宏任务:整体代码,,setTimeout(), setInterval()
-
微任务(micro task):new Promise().then里面的回调
-
为什么要区分宏任务和微任务: 由于所有的任务保存在事件队列中,是先进先出的,但有些任务的优先级很高,所以引入了微任务的概念,为了保证任务完成的顺序。 js会先完成宏任务后,在完成微任务队列中的任务。
3 Node中的事件循环
宏任务执行顺序:
1 timer 定时器: 执行已经安排的setTimeout 和 setInterval 的回调函数
2 pending callback 待定回调:执行延迟到下一个循环迭代的I/O 回调
3 idle,prepare:仅系统内部使用
4 Poll: 检索新的I/O事件, 执行I/O 回调
5 check:执行setImmediate()回调函数
6 close callbacks :socket.on('close',()=>{})
微任务 和 宏任务 在 node 中的执行顺序
Node v10 及以前:
1 执行完一个阶段中的所有任务
2 执行nextTick队列里的任务
3 执行完微任务队列的内容
Node V10以后的:和浏览器的行为统一了
案例
//1 函数async1定义,但未调用,所以现在不输出
async function async1() {
// 6
console.log('async1 start')
//7
await async2()//比较重要的点,await 中执行的函数async2(),可以理解为将async2()放入 new Promise(() =>{})里,执行async2,打印async2
//8 后面的的执行语句都相对于放在了 .then()中,属于微任务,加入了微任务队列中,先不执行,执行当前的宏任务)
//13 第一个微任务,打印
console.log('async1 end')
}
//2 函数async2定义,但未调用,所以现在不输出
async function async2() {
console.log('async2')
}
//3 打印
console.log('script start')
// 4 setTimeout是宏任务,优先级低于微任务,会被移到下一次的宏任务队列中去,现在不输出
setTimeout(function () {
//15 打印
console.log('setTimeout')
},0)
// 5 这里执行async1,所以输出 'async1 start'
async1()
//9
new Promise(function (resolve) {
// 10 同步代码,打印
console.log('Promise1')
//11 .then是异步的,是微任务,先不执行
resolve()
}).then(function () {
//14 第二个微任务,打印,此时,微任务队列清空了,接下来开启下一轮的宏任务,也就是setTimeout
console.log('Promise2')
})
//12 打印,此时第一遍的宏任务已经执行完成,接下来清空微任务队列
console.log('script end')
/*打印顺序:
script start
async1 start
async2
Promise1
script end
async1 end
Promise2
setTimeout
*/
//1 打印
console.log('start')
// 2 宏任务,先不执行,放到下一轮
//8 执行第二行宏任务
setTimeout(()=>{
// 9 打印
console.log('children2')
//10 这里相对于直接将.then中的回调加入了微任务队列中,此时,第二轮宏任务结束,接下来清空微任务队列,
Promise.resolve().then(()=>{
//11 打印
console.log('children3')})
},0)
// 3
new Promise(function (resolve, reject) {
// 4 同步的,打印
console.log('children4')
// 5 宏任务,先不执行,放到下一轮
// 12 执行第三轮宏任务
setTimeout(function () {
//13 打印
console.log('children5')
// 16 resolve把Promise的状态改为fullfilled,//这里的children6会放入res中
resolve('children6')
},0)
//6微任务,先不执行。
// 7.then是需要Promise中有result,然后才可以添加到.then中,但setTimeout是一个宏任务,所以回调没有添加到微任务队列中去
}).then((res)=>{
//14 打印
console.log('children7')
// 15 宏任务执行
setTimeout(()=>{
// 17 打印
console.log(res)
},0)
})
/*
* start
* children4
* 第一轮宏任务结束,接下来清空微任务队列,但没有微任务
* 尝试执行下一轮宏任务
* children2
* 第二轮宏任务结束,接下来清空微任务队列,
* children3
* 尝试执行第三轮宏任务
* children5
* children7
* children6
* */
const p = function () {
//1 宏任务
return new Promise((resolve, reject) => {
// 2 宏任务
const p1 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(1)
},0)
resolve(2)
})
// 3 放入微任务队列
p1.then((res)=>{
console.log(res)//2
})
// 4 打印
console.log(3)
resolve(4)
})
}
// 5 放入微任务队列
p().then((res)=>{
console.log(res)//4
})
// 4 打印
console.log('end')
/*
*3
* end
* 2
* 4
* */