事件循环
JavaScript 是一种单线程的执行机制,同一时间只能做一件事,如果前一个任务非常耗时,就会卡住程序的运行,也叫做线程堵塞。这使得所有的任务必须排队执行,为了防止这种情况,事件循环就应用而生,这使得 JavaScript 可以用同步去模拟异步
为什么是单线程
JavaScript 主要用途是操作 DOM,这样只能是单线程,否则就会引发复杂的同步问题
先看一个例子:
setTimeout(()=>{
console.log('async task');
}, 1000);
console.log('sync task');
在这个简单的代码中,很明显setTimeout
中的回调是最后执行的。即使将时间设置为0
,为了验证一下,对同步任务进行耗时改造
setTimeout(()=>{
console.log('async task');
}, 0);
for (let i = 0; i < 1000000000; i++) {
if(i === 999999999){
console.log('sync task');
}
}
这个循环在一些不好的 CPU 中执行时间肯定是大于 1 秒的,虽然setTimeout
等待时间已经到了,但仍然会等待耗时的循环执行完成后再执行其中的回调,所以不能单纯的看延迟时间来判断回调函数是在何时执行的
JavaScript 将需要等待的任务定义为异步任务,不需要等待的任务定义为同步任务,同步任务都在主线程上进行,形成一个执行栈。在这个领域外还有一个任务队列,异步任务都将会添加到这个队列,一旦执行栈中的任务执行完毕后,就开始读取任务队列中的任务,放到执行栈中执行
现在可以试着来解释一下上面的代码了,setTimeout
产生了一个异步任务被添加到任务队列中,JavaScript 在执行过程中又发现了for
中的同步任务,处理完其中的同步任务后,开始读取任务队列,将回调放到执行栈中执行,这非常容易
现在继续改造一下代码,证明一下这个问题,添加两个异步任务
setTimeout(() => {
console.log('1');
}, 0);
for (let index = 0; index < 1000000000; index++) {
if (index === 9999999) {
setTimeout(() => {
console.log('2');
}, 0);
}
if (index === 999999999) {
console.log('3');
}
}
/*
结果:
3
1
2
*/
这个示例中,两个异步任务被依次加入到任务队列,等待执行栈中的任务执行完毕,再开始依次读取队列中的任务并执行
现在继续改造一下代码,增加 Promise
setTimeout(() => {
console.log('1');
}, 0);
for (let index = 0; index < 100000000; index++) {
if (index === 9999999) {
setTimeout(() => {
console.log('2');
}, 0);
}
if (index === 99999999) {
console.log('3');
}
}
new Promise((resolve, reject) => {
console.log('4');
resolve(5);
}).then(res=>{
console.log(res); // 5
})
/*
执行结果:
3
4
5
1
2
*/
new Promise()
不是一个异步任务,而then
是一个异步任务。而按照异步任务队列的先进先处理的原则,为什么then
却优先于其他的异步任务进行处理呢?
显然靠刚刚讲的概念已经无法支撑下去了,这里就要抛出宏任务和微任务的概念。可以理解的是,每一次之执行栈中正在执行的任务就是一个宏任务,且包括任务队列中的任务。注意,不同来源的任务所处的队列是不同的,就拿这个代码来说,setTimeout
产生的任务和Promise.then
是处于不同队列的,Promise.then
所处的队列执行优先级比前者高,这就是微任务队列,在一个宏任务执行完成后,会先检查是否有微任务,如果有就执行完所有的微任务,然后继续执行下一个宏任务
这是一个代码示例:
setTimeout(() => {
console.log('1');
new Promise((resolve, reject) => {
console.log('6');
resolve(7);
}).then(res=>{
console.log(res);
})
}, 0);
for (let index = 0; index < 100000000; index++) {
if (index === 9999999) {
setTimeout(() => {
console.log('2');
}, 0);
}
if (index === 99999999) {
console.log('3');
}
}
new Promise((resolve, reject) => {
console.log('4');
resolve(5);
}).then(res=>{
console.log(res);
})
/*
执行结果:
3
4
5
1
6
7
2
*/
来看一下 JavaScript 依次做了啥:
1-9
行产生了一个异步任务,放入队列11-20
行中的异步任务放入队列,执行同步任务打印出3
,此时队列中有了 2 个任务22-27
行中的new Promise
是同步的,所以打印出4
,直到此时,当前所有的宏任务都已经执行完成,现在调用.then
产生了异步任务,它是一个微任务,先执行它,打印出5
,微任务处理完毕后,开始读取任务队列中下一个宏任务- 第一个异步任务的回调放入执行栈中执行,打印出
1
,接下来的new Promise
打印出6
,最后执行.then
这个微任务,打印7
,那么这个回调就已经执行完成了 - 读取最后一个队列中的宏任务,打印
2
,引擎开始空闲,等待下一个宏任务
至此可以得到一个总结:
- 执行一个宏任务,先执行其中的同步任务,将遇到的异步任务添加到队列,没有就从任务队列中读取
- 如果遇到微任务,就把微任务添加到微任务队列
- 当宏任务中的同步任务执行完后,立即依次执行当前微任务队列中的所有微任务
- 等待下一个宏任务,如果有就从第 1 步循环
那么再次回到事件循环中,它的概念也很简单,就是在等待任务、执行任务、进入休眠状态等待更多任务这几个状态中无限的循环。所以设置一个任务,等待处理,然后等待更多任务,当一个任务到来时,可能上一个任务还在执行中,那么这个任务就会加入到队列,当多个任务组成了一个队列,就是所谓的“宏任务队列”
了解这些有什么用呢?这对于处理一些 CPU 过载的任务非常有用,可以通过将大任务拆分成小任务来避免这个问题,在这期间可以执行一些其它操作,否则就会让程序处于挂起状态,在这期间无法处理其它的任务,这令人无法接受,这样处理相当于给 JavaScript 执行缓一口气并执行其它操作,毕竟 JavaScript 引擎也得吃喝拉撒干点别的
还有一个好处就是可以显示进度提示,在每一个小任务执行完成后,就更新一下进度,这看起来非常好