看了很多关于 JS 运行机制的文章,每次看的时候都会,但是没过几天又忘了,还是自己理一遍,下次如果还忘,就方便回顾了。
JS 事件循环
先来看一张图:
首先,JS 有同步和异步任务,其中同步是在主线程执行的,执行时会生成一个执行栈
。
同时,有一个任务队列
,里面主要放异步任务的事件回调的。
如果执行栈
中的同步任务执行完毕,这时执行栈为空,系统就会去看看任务队列中有没有要执行的事件,如果有,就加到执行栈中开始执行
看到这里,新手可能比较懵,没事,后面有例子,看完后面例子再来看这几句话就懂了。
宏任务和微任务
在 JS 中常见的任务如下:
宏任务
- script 中的代码块
- setTimeout
- setInterval
- setImmediate()-Node
- requestAnimationFrame()-浏览器
微任务
- Promise.then()
- await 后面的代码
- catch
- finally
- process.nextTick()-Node
- Object.observe
- MutationObserver
看下面的图:
首先执行一个宏任务,执行结束后判断是否存在微任务。
有微任务先执行所有的微任务,再渲染,没有微任务则直接渲染。
然后再接着执行下一个宏任务。
下图为完整的事件循环图:
常见的事件循环面试题
前面说这么多没有多大用,看几个例子就明白了:
1.下面代码输出什么
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
。 - 遇到
setTimeout
,将其放到任务队列的宏任务中。 - 执行
async1
,输出async1 start
,执行await async2()
,输出async2
,此时await async2()
后面的代码不执行,而是将其放入到微任务中。 - 代码继续执行,执行到
new Promise
,输出promise1
,执行resolve()
后将.then()
放到微任务中。 - 往下执行,输出
script end
。 - 这时,第一个
script
宏任务执行完毕,我们来看看在任务队列中剩下哪些宏任务和微任务,首先有一个console.log('setTimeout')
宏任务,和两个微任务,一个先被放入任务队列的console.log('async1 end')
,另一个是后被放入的console.log('promise2')
。 - 根据前几张图,我们知道,现在肯定执行微任务,那谁先被放入的就先执行谁,所以输出
async1 end
,然后输出promise2
。 - 至此,因为任务队列中已没有微任务,第一轮事件循环执行完毕。
- 接着再看看任务队列中有没有宏任务,有的,输出
setTimeout
,此时,所有任务执行完毕,退出程序。 最后的执行结果如下:
- script start
- async1 start
- async2
- promise1
- script end
- async1 end
- promise2
- setTimeout
2.下面代码输出什么
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
});
}, 0);
new Promise(function (resolve, reject) {
console.log('children4');
setTimeout(function () {
console.log('children5');
resolve('children6');
}, 0);
}).then(res => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0);
});
- 首先不用说,肯定先输出
start
。 - 遇到第一个
setTimeout
(我们暂且称他为setTimeout1
吧),由于setTimeout1
是宏任务,所以放到任务队列中的宏任务当中。 - 遇到
new Promise
,执行,输出children4
,遇到第二个setTimeout
(我们暂且称他为setTimeout2
吧),所以也把他放到任务队列中的宏任务当中。 - 由于这个现在还没有执行
resolve
,所以现在微任务中还是空的,也就是还没有then
。 - 此时,因为任务队列中已没有微任务,第一轮事件循环执行完毕。
- 执行
setTimeout1
,输出children2
,同时,出现一个then
微任务,将其放到任务队列中的微任务当中。 setTimeout1
执行完毕,看看任务队列中有没有微任务,有的,输出children3
,本次事件循环结束。- 执行
setTimeout2
,输出children5
,然后resolve('children6')
,将then
放到任务队列中的微任务当中。 setTimeout2
执行完毕,看看任务队列中有没有微任务,有的,输出children7
,然后又遇到了一个setTimeout
,将其放到任务队列中的宏任务当中,本次事件循环结束。- JS 再检查任务队列中还有没有任务,发现还有一个,拿出来执行,然后输出
children6
,此时,所有任务执行完毕,退出程序。
最后的执行结果如下:
- start
- children4
- children2
- children3
- children5
- children7
- children6
如果此时我把第一个setTimeout
的时间设置为 1000,结果会是什么呢?大家可以去试一试。
3.下面代码输出什么
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');
- 执行代码,
Promise
本身是同步的立即执行函数,.then
是异步执行函数。遇到setTimeout
,先把其放入宏任务队列中,遇到p1.then
会先放到微任务队列中,接着往下执行,输出3
。 - 遇到
p().then
会先放到微任务队列中,接着往下执行,输出end
。 - 宏任务执行完成后,开始执行微任务队列中的任务,首先执行
p1.then
,输出2
, 接着执行p().then
, 输出4
。 - 微任务执行完成后,开始执行宏任务,
setTimeout
,resolve(1)
,但是此时p1.then
已经执行完成,此时1
不会输出。 最后的执行结果如下:
- 3
- end
- 2
- 4
如果将上方的resolve(2)
删掉,结果输出什么呢?大家可以去试一试,此时输出结果为:3 end 4 1