事件循环
参考:
- 这一次,彻底弄懂 JavaScript 执行机制
- JavaScript 运行机制详解:再谈Event Loop
- js与Nodejs的单线程和异步–初探
- Node.js的事件轮询Event Loop原理解释
- JavaScript任务队列的顺序机制(事件循环)
- JS与Node.js中的事件循环
什么是事件轮循?
1. Js属于单线程,因为Js不仅关于交互,也需实现对DOM的操作。假如JS是多线程,会存在不同线程操作同一个DOM的情况,此时会产生冲突;
* JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
2. 执行任务分为:同步任务,异步任务;
3. 异步任务:宏任务/微任务;
事件轮询
- JS引擎首先从宏任务队列中取出第一个任务,执行完毕后,将微任务队列中的所有任务取出,按顺序全部执行;
- 然后再从宏任务中取下一个,执行完毕后,再次将微任务中的全部取出;
- 循环往复,直到两个队列中的任务都取完。
注意:JS引擎中,微任务的执行优先级高于宏任务,如果当前任务执行完,发现有微任务就会立即执行微任务,直到微任务队列为空,然后再回头执行宏任务;
宏任务/微任务
宏任务
- 分类: script (同步代码), setTimeout, setInterval, setImmediate, MessageChannel, postMessage, I/O, UI渲染
- 宏任务所处的队列就是宏任务队列
- 第一个宏任务队列中只有一个任务: 执行主线程的js代码
- 宏任务队列可以有多个
- 注意:当前宏任务执行完毕后,会先查看是否有微任务队列;
如果有先执行微任务队列中的所有任务,如果没有就查看是否还有有宏任务队列
微任务
- 分类: new Promise().then(回调) process.nextTick,MutationObserver
- 微任务所处的队列就是微任务队列
- 只有一个微任务队列
- 当前任务(宏任务/微任务)执行完毕,如果微任务队列不为空则继续执行,直到为空
例1
console.log('----------------- start -----------------');
setTimeout(() => {
console.log('setTimeout');
}, 0)
new Promise((resolve, reject) =>{
for (var i = 0; i < 5; i++) {
console.log(i);
}
resolve(); // 修改promise实例对象的状态为成功的状态
}).then(() => {
console.log('promise实例成功回调执行');
})
console.log('----------------- end -----------------');
/***'
*
* 输出结果为:
*
* ==>----------------- start -----------------
==>0
==>1
==>2
==>3
==>4
==>----------------- end -----------------
==>promise实例成功回调执行
==>setTimeout
*/
例2
console.log('start') //------(1)
setTimeout(() => {
console.log('children2') //------(2)
Promise.resolve().then(() => {
console.log('children3') //------(3)
})
}, 0)
new Promise(function (resolve, reject) {
console.log('children4') //------(4)
setTimeout(function () {
console.log('children5') //------(5)
resolve('children6') //------(6)
}, 0)
}).then((res) => {
console.log('children7') //------(7)
setTimeout(() => {
console.log(res) //------(8)
}, 0)
})
//***************************************
//Chorme浏览器结果:【先执行完同步任务,】
// start => 同步任务
// children4 => 同步任务
// 第一轮宏任务结束,尝试清除微任务队列,发现没有微任务,直接执行宏任务setTimeout();
// 此过程中有微任务产生;
//***************************************
// children2
// children3
//=>执行宏任务,【注意】此时整个setTimout为一个宏任务,完成该宏任务就会清空
//微任务,而此时在执行该宏任务时有一个.then()的微任务,然后清空微任务队列,
//所以下个输出为children3
//***************************************
// 执行下一轮宏任务;
// children5
// children7
// children6
//***************************************
// Node输出结果: 结果不稳定,3,5都是异步操作,无法确定先后
//结果1
// start
// children4
// children2
// children3
// children5
// children7
// children6
//结果2
// start
// children4
// children2
// children5
// children3
// children7
// children6
结果1:
结果2:
**注意:**node上运行产生结果不稳定;
1.第一轮:=>(1) (4) [宏任务队列,主线程起始执行队列,执行完毕,发现无微任务]
2.第二轮:=>(2) [宏任务队列,两个setTimeout]
=>(5)(3)或(3)(5),注意此时第一个setTimeout中的微任务(3)和宏任务中的(5)都是异步执行,不能完全确定谁先执行完毕,故此会产生两种结果;然后执行resolve(6)的回调,执行完毕,发现微任务
3.第三轮:=>(7),Promise的回调微任务中先执行,微任务执行完毕,发现第三个setTimeout;
4.第四轮:=>(6),至此宏任务队列清空,没有微任务队列,完毕
综上JS事件循环的特点为:
- chrome的运行比较稳定,而node环境下运行不稳定,可能会出现两种情况。
- chrome运行的结果稳定的原因:
是Promise、process.nextTick()的微任务Event Queue运行的权限比普通宏任务Event Queue权限高,如果取事件队列中的事件的时候有微任务,就先执行微任务队列里的任务,除非该任务在下一轮的Event Loop中,微任务队列清空了之后再执行宏任务队列里的任务。
const p = function () {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res)
})
console.log(3)
resolve(4)
})
}
p().then((res) => {
console.log(res)
})
console.log('end')
//3
//end
//2
//4
注意: 注意坑点:在p1函数中,函数体中在宏任务之前已经resolve(2),于是p1函数的回调就输出了2,注意注意,此时整个回调函数执行完毕,不会再输出1了!!!
例3
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
详见:https://blog.csdn.net/qq_25257229/article/details/114540346
async function foo() {
await 1
}
//等价于
function foo() {
return Promise.resolve(1).then(() => undefined)
}
在await表达式之后的代码可以被认为是存在在链式调用的then回调中,多个await表达式都将加入链式调用的then回调中,返回值将作为最后一个then回调的返回值。
附
JS事件循环执行机制