首先尝试一下吧
请先查看以下这段代码,然后按先后顺序写出系统打印出的结果。
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});
console.log('script end');
正确答案是:
script start
script end
promise1
promise2
setTimeout
为什么会这样
首先在 JS 语言中,大部分的任务都是在主线程(即在单线程)上运行,这些任务包括:
- 用户交互事件
- 渲染事件
- JS脚本执行
- 网络请求、文件读写等
为了让这些事件有条不紊地进行,JS引擎需要对之执行的顺序做一定的安排。V8引擎使用队列的方式来存储这些任务,按先进先出的方式来执行这些任务。JS将任务分成了宏任务(MacroTask)和微任务(MicroTask)两种类型。
宏任务包含了这些任务:setTimeout, setInterval, setImmediate, I/O, UI 渲染,JS脚本
微任务包含了这些任务:process.nextTick, Promises, MutationObserver,观察者观查到变化的回调。
你可以想象有两个队列,一个是宏任务队列,一个是微任务队列。
微任务主要是为了处理异步回调用的,想象如果没有微任务队列,任务一旦执行完的回调就加入到宏任务队列中,如果宏任务队列已有的任务很多,回调可能迟迟得不到执行,程序运行可能会卡顿。
所以,引入微任务队列后,事实上事件循环执行的流程是这样的:
- 一开始把一整段的JS脚本作为第一个宏任务执行
- 在执行过程中,同步代码则直接运行,过程中存在宏任务则进入到宏任务队列,微任务在微任务队列中入列。
- 在当前宏任务执行完成后,检查微任务队列,若存在微任务则按序全部执行完毕。
- 继续检查宏任务队列,执行下一个宏任务,如此反复。
现在再来看看页面给出的JS脚本代码,以打印的结果作为任务名称,把脚本当做第一个宏任务执行,首先打印同步代码script start
,script end
,然后把setTimeout
加入宏任务队列,promise1
加入微任务队列,
此时的宏任务队列:[setTimeout
]
此时的微任务队列:[promise1
]
当前宏任务把脚本内的内容已经执行完了,开始检查微任务队列中是否还有未执行的任务,发现了promise1
,然后打印promise1
,发现promise1
有一个回调promise2
,把它加入到微任务队列中,
此时的宏任务队列:[setTimeout
]
此时的微任务队列:[promise1
,promise2
]
当前微任务执行完毕,promise1
出列,继续检查微任务队列是否还有未执行的微任务,发现了promise2
,打印promise2
,继续检查微任务队列,此时微任务队列的所有任务已经全部执行完毕。
继续执行宏任务队列,发现setTimeout
,打印setTimeout
。
按此规律循环与进行,就是浏览器的事件循环机制。
再多尝试几题吧
理解了原理后,可以根据这个步骤来多看几个例子,尝试写出打印的结果。
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
});
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0);
console.log('start');
// start
// Promise1
// setTimeout1
// Promise2
// setTimeout2
具有迷惑性的题目1
setTimeout(()=>{
console.log('1')
});
new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3')
});
console.log('4');
打印的结果是2,4,3,1
Promise 传入的函数内部的同步代码是立即执行的!then中的函数才是回调哈。
具有迷惑性的题目2
async function a1() {
console.log('async1 start');
await a2();
console.log('async1 end')
}
async function a2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
}, 0);
a1();
new Promise((resolve)=>{
console.log('Promise1');
resolve();
}).then(()=>{
console.log('Promise2')
})
结果如下:
script start
async1 start
async2
Promise1
Promise2
async1 end
setTimeout
关键点在于 async1 end 为什么比 Promise2 后输出,要解释这个原因,需要将题目中的 async 和 await 转换成 promise 的写法后来判断。转换后的代码如下:
function async1(){
console.log('async1 start');
const p = async2();
return new Promise((resolve) => {
Promise.resolve().then(() => {
p.then(resolve)
})
})
.then(() => {
console.log('async1 end')
});
}
function async2(){
console.log('async2');
return Promise.resolve();
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
}, 0);
async1();
new Promise((resolve)=>{
console.log('Promise1');
resolve();
}).then(()=>{
console.log('Promise2')
})
这样看就清楚了些。当 a2 执行完毕后,即打印async2
之后,将async1 end Promise
加入到微任务队列中,然后执行外层 Promise,打印结果Promise1
,把Promise2
加入到微任务队列中,此时微任务队列是:
[async1 end Promise
,Promise2
],
此时执行async1 end Promise
,但是async1 end
第一次执行,又执行一次回调,把真正的打印async1 end
的任务,加入到新的微任务队列,此时微任务队列是:
[Promise2
,async1 end
]
然后依次打印 Promise2,async1 end。