完全吃透async/await
導論:
- 首先,必須了解Promise
主要研究基本語法
對比Promise與Async
異常處理
參考:
0. 前言
ES7 提出的async
函式,終於讓 JavaScript 對於異步操作有了終極解決方案。No more callback hell。async
函式是 Generator
函式的語法糖。使用 關鍵字 async
來表示,在函式內部使用 await
來表示異步。
想較於 Generator,Async
函式的改進在於下面四點:
- 內置執行器。Generator 函式的執行必須依靠執行器,而
Aysnc
函式自帶執行器,調用方式跟普通函式的調用一樣 - 更好的語義。
async
和await
相較於*
和yield
更加語義化 - 更廣的適用性。
co
模塊約定,yield
命令後面只能是 Thunk 函式或 Promise對象。而async
函式的await
命令後面則可以是 Promise 或者 原始類型的值(Number,string,boolean,但這時等同於同步操作) - 返回值是 Promise。
async
函式返回值是 Promise 對象,比 Generator 函式返回的 Iterator 對象方便,可以直接使用then()
方法進行調用
1. 基本語法
基本語法是 方法頭 添加關鍵字async
,在異步前 添加await
1. API
核心 API 就async 與 await,具體 直接將MDN中解釋拿來用
async function
聲明將定義一個返回AsyncFunction
對象的異步函式。異步函式是指通過事件循環異步執行的函式,它會通過一個隱式的Promise
返回其結果。但是如果你的代碼使用了異步函式,它的語法和結構會更像是標準的同步函式。 MDN白話:async 返回一個
Promise
,也就是 最後return是不是 Promise 最終都會被包裝成promise-
await
操作符用於等待一個Promise
對象。它只能在異步函式async function
中使用。MDN//語法 [return_value] = await expression; //注意,返回並不是一個Promise對象,而是結果
表達式:
一個 Promise 對象或者任何要等待的值。
返回值: (注意,返回並不是一個Promise對象,而是結果)
返回 Promise 對象的處理結果。
如果等待的不是 Promise 對象,則返回該值本身。描述:
await 表達式會暫停當前
async function
的執行,等待 Promise 處理完成。若 Promise 正常處理(fulfilled),其回調的resolve函式參數作為 await 表達式的值,繼續執行async function
。 若 Promise 處理異常(rejected),await 表達式會把 Promise 的異常原因拋出。
另外,如果 await 操作符後的表達式的值不是一個 Promise,那麼該值將被轉換為一個已正常處理的 Promise。
2. 實踐—簡單用法
這會使 async 函式暫停執行,等待表達式中的 Promise 解析完成後繼續執行 async 函式並返回解決結果。
//用法1
/*
async 返回一個 Promise
1. return 值(value),則返回 Promise.resolve(value)
2. 異常,則是 Promise.reject(err);
*/
async function testAsync() {
return "hello async";
}
const result = testAsync();
console.log(result);//返回一個promise對象
//用法2
//async 函式中可能會有 await 表達式,這會使 async 函式暫停執行,等待表達式中的 Promise 解析完成後繼續執行 async 函式並返回解決結果。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
return value; //類似 return Promise.resolve(value)
}
//async 返回一個promise
asyncPrint('hello world', 50).then(function(d){
console.log('then',d);
});
/** 打印
hello world
then hello world
*/
//await 必須的在 async方法內,否則會報錯
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
return value; //類似 return Promise.resolve(value)
}
//async 返回一個promise
asyncPrint('hello world', 50).then(function(d){
console.log('then',d);
});
//Uncaught SyntaxError: await is only valid in async function
2. Async對比Promise優勢
1. 解決then 多層回調
參考:理解 JavaScript 的 async/await
假設:假設一個業務,分多個步驟完成,每個步驟都是異步的,而且依賴於上一個步驟的結果。我們仍然用 setTimeout
來模擬異步操作:
/**
* 傳入參數 n,表示這個函式執行的時間(毫秒)
* 執行的結果是 n + 200,這個值將用於下一步驟
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
//Promise方案
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
//async 寫法
//對比 promise寫法,
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
2. 帶catch
//promise 版本
function getProcessedData(url) {
return downloadData(url) // returns a promise
.catch(e => {
return downloadFallbackData(url) // 返回一個 promise 對象
.then(v => {
return processDataInWorker(v); // 返回一個 promise 對象
});
})
.then(v => {
return processDataInWorker(v); // 返回一個 promise 對象
});
}
//Async 版本
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
}
return processDataInWorker(v);
}
//注意,在上述示例中,return 語句中沒有 await 操作符,因為 async function 的返回值將隱式傳遞給 Promise.resolve。
3.Async並行
0. 背景
對比 promise 並行處理
前面解決都是 一個promise執行完後,再執行新的promise;而下面討論是,兩個Promise如何並行
1. 基本並行處理
// 方法 1
let [res1, res2] = await Promise.all([func1(), func2()])
// 方法 2
let func1Promise = func1()
let func2Promise = func2()
let res1 = await func1Promise
let res2 = await func2Promise
2. 深入理解並行
上文基本的並行,並不是 正在的並行
參考:MDN 下面代碼參考自 MDN
var resolveAfter2Seconds = function() {
console.log("starting slow promise");
return new Promise(resolve => {
setTimeout(function() {
resolve(20);
console.log("slow promise is done");
}, 2000);
});
};
var resolveAfter1Second = function() {
console.log("starting fast promise");
return new Promise(resolve => {
setTimeout(function() {
resolve(10);
console.log("fast promise is done");
}, 1000);
});
};
var sequentialStart = async function() {
console.log('==SEQUENTIAL START==');
// 如果 await 操作符後的表達式不是一個 Promise 對象, 則它會被轉換成一個 resolved 狀態的 Promise 對象
const slow = await resolveAfter2Seconds();
const fast = await resolveAfter1Second();
console.log(slow);
console.log(fast);
}
var concurrentStart = async function() {
console.log('==CONCURRENT START with await==');
const slow = resolveAfter2Seconds(); // 立即啟動計時器
const fast = resolveAfter1Second();
console.log(await slow);
console.log(await fast); // 等待 slow 完成, fast 也已經完成。
}
var stillSerial = function() {
console.log('==CONCURRENT START with Promise.all==');
Promise.all([resolveAfter2Seconds(), resolveAfter1Second()]).then(([slow, fast]) => {
console.log(slow);
console.log(fast);
});
}
var parallel = function() {
console.log('==PARALLEL with Promise.then==');
resolveAfter2Seconds().then((message)=>console.log(message)); // in this case could be simply written as console.log(resolveAfter2Seconds());
resolveAfter1Second().then((message)=>console.log(message));
}
sequentialStart(); // sequentialStart 總共花了 2+1 秒
// 等到 sequentialStart() 完成
setTimeout(concurrentStart, 4000); // concurrentStart 總共花了 2 秒
// 等到 setTimeout(concurrentStart, 4000) 完成
setTimeout(stillSerial, 7000); // stillSerial 總共花了 2 秒
// 等到 setTimeout(stillSerial, 7000) 完成
setTimeout(parallel, 10000); // 真正的並行運行
上面代碼是4中,不同處理promise並行方式。但核心是不管怎樣 await
執行都會有順序,會等待執行。
4. 並行——循環
1. for-of
//不推薦,因為是串行解決
function fetch(d){
return new Promise((resolve)=>{
console.log('start:',d);
setTimeout(()=>{resolve(d)},Math.random()*1000);
})
}
var args = [1,2,3,4,5];
async function test(args){
for(const arg of args){
const res = await fetch(arg);
console.log('end:',res);
}
}
test(args);
/**
start: 1
23:00:11.331 bundle.9303569f0937a02f1c80.js:4 end: 1
23:00:11.332 bundle.9303569f0937a02f1c80.js:4 start: 2
23:00:12.009 bundle.9303569f0937a02f1c80.js:4 end: 2
23:00:12.009 bundle.9303569f0937a02f1c80.js:4 start: 3
23:00:12.248 bundle.9303569f0937a02f1c80.js:4 end: 3
23:00:12.248 bundle.9303569f0937a02f1c80.js:4 start: 4
23:00:12.984 bundle.9303569f0937a02f1c80.js:4 end: 4
23:00:12.984 bundle.9303569f0937a02f1c80.js:4 start: 5
23:00:13.184 bundle.9303569f0937a02f1c80.js:4 end: 5
*/
2. 使用map並行執行
function fetch(d){
return new Promise((resolve)=>{
console.log('start:',d);
setTimeout(()=>{resolve(d)},Math.random()*1000);
})
}
var args = [1,2,3,4,5];
async function test3(args){
const promises = args.map(async arg=>{//map 執行 可以並行執行
const re = await fetch(arg);
return re;
})
for(const p of promises){
p.then((d)=>{
console.log('end',d);
})
}
}
test3(args);
/**
start: 1
22:56:44.421 VM6500:3 start: 2
22:56:44.421 VM6500:3 start: 3
22:56:44.422 VM6500:3 start: 4
22:56:44.422 VM6500:3 start: 5
22:56:44.436 Promise {<resolved>: undefined}
22:56:44.462 VM6500:15 end 2
22:56:44.552 VM6500:15 end 1
22:56:44.569 VM6500:15 end 5
22:56:44.974 VM6500:15 end 3
22:56:44.993 VM6500:15 end 4
*/
Array.prototype.map
與Array.prototype.forEach
執行promise陣列,是並行。
但for-in
for-of
for
都是串行的
5. 異常處理
0.異常分類
參考:Promise異常分類
異常簡單分為分為 執行異常和異步異常(通過是否能try-catch捕獲來區分);
1. 基本套路
//套路1
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
await Promise.resolve('1')
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
//套路2
async function f() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
} catch(e) {
console.log(e)
}
}
f()
//為什麼 Promise 無法使用try-catch捕獲異常,但 async中,可以捕獲?
//猜測可能是,await返回是一個值,執行上下文應該是同一個
2. 鏈式處理
//在基本套路1 基礎上處理 與promise 鏈式異常處理對比
async function f() {
await new Promise(function (resolve, reject) {
console.log('1')
throw new Error('出錯了');
});
await new Promise(function(resolve, reject){
console.log('2');//沒有打印
resolve(2);
})
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
/**
1
Error: 出錯了
at <anonymous>:4:11
at new Promise (<anonymous>)
at f (<anonymous>:2:9)
at <anonymous>:12:1
*/
重點: 第二個 await沒有執行,(‘2’沒有打印);也就證明,async
遇到異常 就會中斷鏈,與Promise鏈式異常對比
//在基本套路2 基礎上處理 對比
async function f() {
try {
await new Promise(function (resolve, reject) {
console.log('222');
throw new Error('出錯了');
});
await new Promise(function (resolve, reject) {
console.log('222');//不會打印
resolve(222);
})
} catch(e) {
console.log(e)
}
}
f()
//與上例 是一樣 不會執行第二個 await;
3. 使用第三方模塊bounce
參見:github
6. 實現Async——todo
async 函式的實現原理,就是將 Generator 函式和自動執行器,包裝在一個函式里。
async function fn(args) {
// ...
}
// 等同於
function fn(args) {
return spawn(function* () {
// ...
});
}
//generator 沒有搞明白,直接是copy代碼
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
7. 容易出錯
1. await 不是在async function內
//await 與 async 中間隔了一個function
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯
docs.forEach(function (doc) {
// 改成 docs.forEach( await function (doc) 就沒毛病
await db.post(doc);
});
}
8. Async 與其他異步操作的對比
先定義一個 Fetch 方法用於獲取 github user 的信息:
function fetchUser() {
return new Promise((resolve, reject) => {
fetch('https://api.github.com/users/superman66')
.then((data) => {
resolve(data.json());
}, (error) => {
reject(error);
})
});
Promise 方式
/**
* Promise 方式
*/
function getUserByPromise() {
fetchUser()
.then((data) => {
console.log(data);
}, (error) => {
console.log(error);
})
}
getUserByPromise();
Promise 的方式雖然解決了 callback hell,但是這種方式充滿了 Promise的 then()
方法,如果處理流程複雜的話,整段代碼將充滿 then
。語義化不明顯,代碼流程不能很好的表示執行流程。
Generator 方式
/**
* Generator 方式
*/
function* fetchUserByGenerator() {
const user = yield fetchUser();
return user;
}
const g = fetchUserByGenerator();
const result = g.next().value;
result.then((v) => {
console.log(v);
}, (error) => {
console.log(error);
})
Generator 的方式解決了 Promise 的一些問題,流程更加直觀、語義化。但是 Generator 的問題在於,函式的執行需要依靠執行器,每次都需要通過 g.next()
的方式去執行。
async 方式
/**
* async 方式
*/
async function getUserByAsync(){
let user = await fetchUser();
return user;
}
getUserByAsync()
.then(v => console.log(v));
async
函式完美的解決了上面兩種方式的問題。流程清晰,直觀、語義明顯。操作異步流程就如同操作同步流程。同時 async
函式自帶執行器,執行的時候無需手動加載。
個人博客
更多前端技術文章
、美術設計
、wordpress外掛、優化教程
、學習筆記
盡在我的個人博客喵容 – 和你一起描繪生活,歡迎一起交流學習,一起進步:https://www.miaoroom.com
站內文章推薦:
記錄一次基於vue、typescript、pwa的項目由開發到部署
原文鏈接:「前端進階」完全吃透async/await,深入JavaScript異步