JS是单线程语言
进程,即资源分配的最小单位,拥有独立的堆栈空间和数据存储空间
线程,即程序执行的最小单位,一个进程可以包括多个线程
如果多进程,同时操作DOM,那么后果就不可控了
例如:对于同一个按钮,不同的进程赋予了不同的颜色,到底该怎么展示
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务(synchronous)
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务(asynchronous)
异步任务指的是,不进入主线程,而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
事件循环(Event Loop)
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
宏任务(macrotask)
当前调用栈中执行的代码成为宏任务。
包括:script 中代码、setTimeout、setInterval、I/O、UI render
微任务(microtask)
当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件
包括:process.nextTick、MutationObserver、Promise.then
运行机制
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
Promise
Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的
new Promise(function(resolve) {
console.log('promise');
resolve();
}).then(res=>{
console.log('rosolve')
})
Async/Await
而在async/await中,在await出现之前,其中的代码也是立即执行的。
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。
await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
题一
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');
解析:
1.事件循环从宏任务(macrotask)队列开始,宏任务队列中,只有一个script(整体代码)任务,执行整个代码块
2.遇到2个定义的async函数,继续往下走,遇到console语句,直接输出 'script start'
3.script任务继续向下执行,遇到async1()函数,async函数中在await之前的代码是立即执行的,所以输出 'async1 start',然后执行async2()函数,输出 'async2',将 ‘console.log('async1 end')’分配到microtask队列中
4.script任务继续往下执行,遇到Promise,Promise中的代码是被当做同步任务立即执行的,所以输出 'promise1',然后执行 resolve,将 'promise2' 分配到microtask队列中
5.script任务继续往下执行,最后只有一句输出了'script end',至此,全局任务就执行完毕了
6.每次执行完一个宏任务之后,会去检查是否存在 microtask;如果有,则执行 microtask 直至清空 microtask Queue。因此在script任务执行完毕之后,开始查找清空微任务队列
7.microtask队列中,Promise 队列中有两个任务async1 end和promise2,因此依次输出async1 end 、promise2,所有的 microtask 执行完毕之后,表示第一轮的循环就结束了
8.第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束
题二
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
解析:
和题一不同的是async2()函数内部也写成了Promise,Promise中的代码是被当做同步任务立即执行的,所有会输出 'promise1','promise2'则被分配到microtask队列中,因此输出完 'script end'后,会相继输出 promise2, async1 end ,promise4,setTimeout
题三
sync function async1() {
console.log('async1 start');
await async2();
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
解析:
在输出为promise2之后,接下来会按照加入setTimeout队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1,所以会按3,2,1的顺序来输出。
题四
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
解析:
1. Promise.resolve().then(() => {
console.log("promise1");
});
a1();
'promise1'也是被分配到microtask队列中的,而且是在a1()函数执行前,先分配的,所以在 'script end'输出后,会先输出 'promise1' 然后在依次输出
2. let promise2 = new Promise((resolve) => {
resolve("promise2.then");
console.log("promise2");
});
这里会直接输出"promise2", 'resolve'不同于'await',不会阻止后面的执行
参考文章