整个文章是我在不断学习的时候不断更新的,因此有些知识点可能重复,由于一直在复习不同知识点,因此短期内没时间检查整个文章,如发现有错误,希望能留言提醒我,感激不尽!
async / await 是ES7新增的语法糖,被称为异步的终极解决方案。
废话不多说,直接上菜。
目录:
1.async / await 特点
2.await 和 async 在JS执行中的顺序
3.async 并发和继发执行
4.页面加载时 defer 属性和 async 属性的区别
async / await 特点
1.await 必须在 async 函数中(nodejs环境下)
2.await 后面可以是任意值
3.async 函数返回的一定是 Promise 对象。如果 async 未返回Promise对象,那么会执行立即完成的Promise.resolve(value)
,无返回值则执行Promise.resolve(undefined)
4.await 后的 Promise 对象如果不是fulfilled
状态,则 async 函数立即结束并返回该 Promise
function a(){
return new Promise((res, rej) => {console.log(1); [rej(4);]});
//返回 pending/rejected状态的Promise
//没有 rej(1)时是 pending,有 rej(1)时是 rejected
}
async function b(){
await a();
console.log(2);
}
b();
console.log(3);
//1
//3
//[error: Uncaught (in promise) 4]
解决办法:把 await 放在 try ... catch 结构中或者在 await 后的 Promise 接一个 catch()
function a(){
return new Promise((res, rej) => {console.log(1); rej(4);}).catch(_=>_);
//返回 pending/rejected状态的Promise
//没有 rej(1)时是 pending,有 rej(1)时是 rejected
}
async function b(){
await a();
console.log(2);
}
b();
console.log(3);
//1
//3
//2
5.语义化更好(相对于 * 和 yeild )
6.内置执行器
实现方法:
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
const foo = async () => {};// 箭头函数
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache1 = await this.cachePromise;
return name; //返回的值作为then中的参数
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
await 和 async 在JS执行中的顺序
- 遇到 await 后,先执行 await 后面的表达式,然后将 await 及 async 函数体剩下的代码推入微任务队列
- Promise 本身属于宏任务
- then(),catch(),finally()等Promise原型链上的方法在执行时,会判断是否有合适的Promise状态,如果能够执行,Promise会调用该方法并则将其推入微任务队列。
- 如果多个 await 表达式,第一次 await 执行完表达式后推入微任务,当宏任务执行完并执行微任务时,在碰到第二个 await 时,执行完表达式后会再次将 async 函数体剩下的代码推入微任务队列
- 如果 await 后面返回的是 async 的 Promise,那么推入微任务队列后,下次取出队列时,还要等待resolve的结果,因此会将再次推入微任务队列。
4和5的理解直接看如下代码
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
async function async2() {
console.log('async2');
Promise.resolve(async3()).then(() => {
console.log('async2 end');
})
}
async function async3() {
console.log('async3');
Promise.resolve(async4()).then(() => {
console.log('async3 end');
})
console.log('async3 script end')//理解的关键点
}
async function async4() {
await console.log('async4');
console.log('async4 end')
}
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
async3
async4
async3 script end
promise1
script end
async4 end
async2 end
async1 end
promise2
async3 end
undefined
setTimeout
//控制台默认会输出第一次宏任务最后执行任务的返回值,如果没有就是 undefined
//第一次宏任务最后执行的任务的是console.log(),无返回值
//Promise等属于微任务,setTimeout()的回调函数是推入下一次宏任务队列
案例中备注了一个关键点,基本所有的博客中都没写这个,所以对于原理没搞透的人来说上面的例子有点难以理解:
- 为什么
async4 end
后面不是async3 end
而是async2 end
和async1 end
?
原因:Promise.resolve() 是立即执行的,但后面的 then() 在接受到Promise结果后会把自己推入微任务队列(Event Loop细节请看我的另外一篇文章)。而函数 async4 在执行完 await 后的表达式之后,会类似于 then() 函数将 async() 函数剩下的代码推入微任务队列并跳出 async() 函数体,这个时候函数 async3 中的Promise.resolve(async4())
没有返回Promise对象,因此后面的 then() 函数没有推入微任务队列,而是继续往下执行了console.log('async3 script end')
,这是我标记的关键点位置,如果没有这句话,很难理解为何console.log('async3 end');
在微任务的末尾。由于执行了关键点console.log('async3 script end')
,因此代表函数 async3() 执行完毕,函数 async2() 中的Promise.resolve(async3())
执行完毕,得到Promise对象(async函数执行完一定会返回一个 fulfilled 状态的 Promise 对象),于是 then() 函数推入微任务队列, async1() 函数同理。于是执行微任务队列时,其顺序就是'async4 end' 'async2 end' 'async1 end'
,在async 4 end
输出后,函数 async4() 才彻底结束,返回一个Promise对象,函数 async3() 中的 then() 才有机会推入微任务队列(then() 函数是在接收到合适的 Promise 对象时才会推入微任务队列,否则只是加入缓存,小知识点)。
再看一个案例:
function a() {
console.log("执行函数a"); //2
return Promise.resolve("a函数return"); //8
}
function b() {
console.log("执行函数b"); //6
return "b函数return"; //5
}
async function foo() {
console.log("函数foo开始执行"); //1
const v1 = await a();//关键点1 //2
console.log(v1); //5
const v2 = await b(); //6
console.log(v2); //8
}
foo();
var promise = new Promise((resolve)=> {
console.log("promise开始"); //3
resolve("promise的resolve");//关键点2 //7
});
promise.then((val)=> console.log(val));
console.log("宏任务队列结束"); //4
输出结果:
函数foo开始执行
执行函数a
Promise开始
宏任务队列结束
a函数return
执行函数b
Promise的resolve
b函数return
上面代码中console.log(v2);
会在promise.then((val)=> console.log(val));
之后执行,因为执行const v2 = await b();
时,b() 执行完毕后会被推入微任务队列,然后按顺序执行宏任务和微任务队列,而此时宏任务队列为空,微任务队列为(val)=> console.log(val); console.log(v2);
如果给函数a加上async:
async function a() {
console.log("执行函数a");
return Promise.resolve("a函数return");
}
输出结果:
函数foo开始执行
执行函数a
promise开始
宏任务队列结束
promise的resolve
a函数return
执行函数b
b函数return
原因是 await 后的 async 函数执行后还需要 resolve,这需要占用一次微任务流程,因此await async function a(){}
会比promise.then((val)=> console.log(val));
执行的更慢,如果函数a和函数b一样,直接返回的是常数,那么就不存在 resolve 阻塞一次进程了。
总结
1. await 后面如果是 async 函数,那么要小心该函数本身可能就可能导致异步。(异步时间可能不是 1 ticks,此只是本人也没完全搞透,先挖个坑)
var x;
async function foo1(){
x = await foo2();
}
async function foo2(){
console.log('foo2 start');
return Promise.resolve('foo2 end');
};
foo1();
Promise.resolve(1)
.then(_=>{console.log(x); console.log(_); return 2})
.then(_=>{console.log(x); console.log(_); return 3})
.then(_=>{console.log(x); console.log(_); return 4})
.then(_=>{console.log(x); console.log(_);})
正常情况 await 应该是在第一个 then() 之前运行完成,但是 async 使其需要等待 Promise 的 resolve(都这么说,我也不知道为什么,Promise.resolve应该是立即执行的)。不过即使多等待一次,也应该是在第二个 then() 执行之前运行完成,然而最终却是在第三次清空微任务队列时执行,异步时间从 1 ticks 变成了 3 ticks
2. async 里的 await 会发生异步,因此如果该函数被调用,在当前宏任务中是无返回值的。
async 并发和继发执行
继发实现:
//继发 1
async function foo1() {
var res1 = await fetch(url1);
var res2 = await fetch(url2);
var res3 = await fetch(url3);
return"whew all done";
}
//继发 2 for...of
async function foo2(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
并发实现:
//并发 1
async function foo1() {
var res = awaitPromise.all([fetch(url1), fetch(url2), fetch(url3)]);
return"whew all done";
}
//并发 2
async function foo2(urls) {
// 并发读取 url
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
//并发3 for...of
function foo3(time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(time)
}, time)
})
}
async function test () {
let arr = [foo3(2000), foo3(100), foo3(3000)] // 并发执行
for await (let item of arr) {
console.log(Date.now(), item) // 按次序输出
}
}
test()
// 1575536194608 2000
// 1575536194608 100
// 1575536195608 3000
页面加载时 defer 属性和 async 属性的区别
它们是在解析html的时候执行,还是js在执行代码时候执行?
(1)没有 defer 或 async 属性,浏览器会立即加载并执行相应的脚本。也就是说在渲染 script 标签之后的文档之前,不等待后续加载的文档元素,读到就开始加载和执行,此举会阻塞后续文档的加载;
(2)有了 async 属性,表示后续文档的加载和渲染与js脚本的加载和执行是并行进行的,即异步执行,但是当js脚本加载完毕之后会立即阻塞进程并先解析 js 脚本;
(3)有了 defer 属性,加载后续文档的过程和和渲染与 js 脚本的加载和执行是并行进行的,即异步执行,但是 js 脚本的执行需要等到文档所有元素解析完成之后,DOMContentLoaded 事件触发执行之前。
总结
(1)defer和async在网络加载过程是一致的,都是异步执行的;
(2)两者的区别在于脚本加载完成之后何时执行,可以看出defer更符合大多数场景对应用脚本加载和执行的要求;
(3)如果存在多个有defer属性的脚本,那么它们是按照加载顺序执行脚本的;而对于async,它的加载和执行是紧紧挨着的,无论声明顺序如何,只要加载完成就立刻执行,它对于应用脚本用处不大,因为它完全不考虑依赖。
本人才疏学浅,如有错误敬请指出,感激不尽!
参考:
[1].Promise-MDN
[2].【ES6基础知识】promise和await/async
[3]. async / await 执行顺序详解
[4].async和await
[5].async-并发执行和继发执行
[6].Promise与async /await异步微任务队列差异
[7].promise、async/await在任务队列中的执行顺序
[8].script标签中defer和async属性的区别