什麼是async?
async
函式是 Generator
函式的語法糖。使用 關鍵字 async
來表示,在函式內部使用 await
來表示非同步。相較於 Generator
,async
函式的改進在於下面四點:
內建執行器。
Generator
函式的執行必須依靠執行器,而async
函式自帶執行器,呼叫方式跟普通函式的呼叫一樣更好的語義。
async
和await
相較於*
和yield
更加語義化更廣的適用性。
co
模組約定,yield
命令後面只能是 Thunk 函式或 Promise物件。而async
函式的await
命令後面則可以是 Promise 或者 原始型別的值(Number,string,boolean,但這時等同於同步操作)返回值是 Promise。
async
函式返回值是 Promise 物件,比 Generator 函式返回的 Iterator 物件方便,可以直接使用then()
方法進行呼叫
此處總結參考自:理解async/await
async
是ES7新出的特性,表明當前函式是非同步函式,不會阻塞執行緒導致後續程式碼停止執行。
怎麼用
申明之後就可以進行呼叫了
async function asyncFn() {
return 'hello world';
}
asyncFn();複製程式碼
這樣就表示這是非同步函式,返回的結果
async 表示函式裡有非同步操作await 表示緊跟在後面的表示式需要等待結果。
返回的是一個promise
物件,狀態為resolved
,引數是return
的值。那再看下面這個函式
async function asyncFn() {
return '我後執行'
}
asyncFn().then(result => {
console.log(result);
})
console.log('我先執行');複製程式碼
上面的執行結果是先列印出'我先執行'
,雖然是上面asyncFn()
先執行,但是已經被定義非同步函式了,不會影響後續函式的執行。
現在理解了async
基本的使用,那還有什麼特性呢?
async
定義的函式內部會預設返回一個promise
物件,如果函式內部丟擲異常或者是返回reject
,都會使函式的promise
狀態為失敗reject
。
async function e() {
throw new Error('has Error');
}
e().then(success => console.log('成功', success))
.catch(error => console.log('失敗', error));複製程式碼
我們看到函式內部丟擲了一個異常
,返回reject
,async
函式接收到之後,判定執行失敗進入catch
,該返回的錯誤列印了出來。
async function throwStatus() {
return '可以返回所有型別的值'
}
throwStatus().then(success => console.log('成功', success))
.catch(error => console.log('失敗', error));複製程式碼
//列印結果
成功 可以返回所有型別的值
複製程式碼
async
函式接收到返回的值,發現不是異常
或者reject
,則判定成功,這裡可以return
各種資料型別的值,false
,NaN
,undefined
...總之,都是resolve
但是返回如下結果會使async
函式判定失敗reject
- 內部含有直接使用並且未宣告的變數或者函式。
- 內部丟擲一個錯誤
throw new Error
或者返回reject
狀態return Promise.reject('執行失敗')
- 函式方法執行出錯(?:Object使用push())等等...
還有一點,在async
裡,必須要將結果return
回來,不然的話不管是執行reject
還是resolved
的值都為undefine
,建議使用箭頭函式。
其餘返回結果都是判定resolved
成功執行。
//正確reject方法。必須將reject狀態return出去。
async function PromiseError() {
return Promise.reject('has Promise Error');
}
//這是錯誤的做法,並且判定resolve,返回值為undefined,並且Uncaught報錯
async function PromiseError() {
Promise.reject('這是錯誤的做法');
}
PromiseError().then(success => console.log('成功', success))
.catch(error => console.log('失敗', error));複製程式碼
我們看到第二行多了個Promise
物件列印,不用在意,這個是在Chrome
控制檯的預設行為,我們平常在控制檯進行賦值也是同樣的效果。如果最後執行語句
或者表示式
沒有return
返回值,預設undefined
,做個小實驗。
var a = 1;
//undefined
------------------------------------------------------------
console.log(a);
//1
//undefined
------------------------------------------------------------
function a(){ console.log(1) }
a();
//1
//undefined
------------------------------------------------------------
function b(){ return console.log(1) }
b();
//1
//undefined
------------------------------------------------------------
function c(){ return 1}
c();
//1
------------------------------------------------------------
async function d(){
'這個值接收不到'
}
d().then(success => console.log('成功',success));
//成功 undefined
//Promise { <resolved>: undefined }
-----------------------------------------------------------
async function e(){
return '接收到了'
}
e().then(success => console.log('成功',success));
//成功 接收到了
//Promise { <resolved>: undefined }複製程式碼
最後一行Promise { <resolved> : undefined }
是因為返回的是console.log
執行語句,沒有返回值。
d().then(success => console.log('成功',success)}
等同於
d().then(function(success){
return console.log('成功',success);
});
複製程式碼
js本身是單執行緒的,通過v8我們可以擁有"非同步"的能力
認識完了async,來講講await。
await是什麼?
await
意思是async wait(非同步等待)。這個關鍵字只能在使用async
定義的函式裡面使用。任何async
函式都會預設返回promise
,並且這個promise
解析的值都將會是這個函式的返回值,而async
函式必須等到內部所有的 await
命令的 Promise
物件執行完,才會發生狀態改變。
打個比方,await是學生,async是校車,必須等人齊了再開車。
await
函式執行完畢後,才會告訴promise
我成功了還是失敗了,執行then
或者catch
async function awaitReturn() {
return await 1
};
awaitReturn().then(success => console.log('成功', success))
.catch(error => console.log('失敗',error))複製程式碼
在這個函式裡,有一個await
函式,async會等到await 1
這一步執行完了才會返回promise
狀態,毫無疑問,判定resolved
。
很多人以為await
會一直等待之後的表示式執行完之後才會繼續執行後面的程式碼,實際上await
是一個讓出執行緒的標誌。await
後面的函式會先執行一遍(比如await Fn()的Fn ,並非是下一行程式碼),然後就會跳出整個async
函式來執行後面js棧的程式碼。等本輪事件迴圈執行完了之後又會跳回到async
函式中等待await後面表示式的返回值,如果返回值為非promise
則繼續執行async
函式後面的程式碼,否則將返回的promise
放入Promise
佇列(Promise的Job Queue)
來看個簡單點的例子
const timeoutFn = function(timeout){
return new Promise(function(resolve){
return setTimeout(resolve, timeout);
});
}
async function fn(){
await timeoutFn(1000);
await timeoutFn(2000);
return '完成';
}
fn().then(success => console.log(success));複製程式碼
這裡本可以用箭頭函式寫方便點,但是為了便於閱讀本質,還是換成了ES5寫法,上面執行函式內所有的await函式才會返回狀態,結果是執行完畢3秒後才會彈出'完成
'。
正常情況下,await 命令後面跟著的是 Promise ,如果不是的話,也會被轉換成一個 立即 resolve 的 Promise。
也可以這麼寫
function timeout(time){
return new Promise(function(resolve){
return setTimeout(function(){
return resolve(time + 200)
},time);
})
}
function first(time){
console.log('第一次延遲了' + time );
return timeout(time);
}
function second(time){
console.log('第二次延遲了' + time );
return timeout(time);
}
function third(time){
console.log('第三次延遲了' + time );
return timeout(time);
}
function start(){
console.log('START');
const time1 = 500;
first(time1).then(time2 => second(time2) )
.then(time3 => third(time3) )
.then(res => {
console.log('最後一次延遲' + res );
console.timeEnd('END');
})
};
start();複製程式碼
這樣用then鏈式回撥的方式執行resolve
//列印結果
START
第一次延遲了500
第二次延遲了700
第三次延遲了900
最後一次延遲1100
END
複製程式碼
用async/await呢?
async function start() {
console.log('START');
const time1 = 500;
const time2 = await first(time1);
const time3 = await second(time2);
const res = await third(time3);
console.log(`最後一次延遲${res}`);
console.log('END');
}
start();複製程式碼
達到了相同的效果。但是這樣遇到一個問題,如果await
執行遇到報錯呢
async function start() {
console.log('START');
const time1 = 500;
const time2 = await first(time1);
const time3 = await Promise.reject(time2);
const res = await third(time3);
console.log(`最後一次延遲${res}`);
console.log('END');
}
start();複製程式碼
返回reject後,後面的程式碼都沒有執行了,以此遷出一個例子:
let last;
async function throwError() {
await Promise.reject('error');
last = await '沒有執行';
}
throwError().then(success => console.log('成功', last))
.catch(error => console.log('失敗',last))複製程式碼
其實
async
函式不難,難在錯處理上。上面函式,執行的到await
排除一個錯誤後,就停止往下執行,導致last
沒有賦值報錯。
async
裡如果有多個await函式的時候,如果其中任一一個丟擲異常或者報錯了,都會導致函式停止執行,直接reject
;怎麼處理呢,可以用try/catch
,遇到函式的時候,可以將錯誤丟擲,並且繼續往下執行。
let last;
async function throwError() {
try{
await Promise.reject('error');
last = await '沒有執行';
}catch(error){
console.log('has Error stop');
}
}
throwError().then(success => console.log('成功', last))
.catch(error => console.log('失敗',last))複製程式碼
這樣的話,就可以繼續往下執行了。
來個?練習下
function testSometing() {
console.log("testSomething");
return "return testSomething";
}
async function testAsync() {
console.log("testAsync");
return Promise.resolve("hello async");
}
async function test() {
console.log("test start...");
const testFn1 = await testSometing();
console.log(testFn1);
const testFn2 = await testAsync();
console.log(testFn2);
console.log('test end...');
}
test();
var promiseFn = new Promise((resolve)=> {
console.log("promise START...");
resolve("promise RESOLVE");
});
promiseFn.then((val)=> console.log(val));
console.log("===END===")複製程式碼
執行結果
我們一步步來解析
首先test()
列印出test start...
然後 testFn1 = await testSomething();
的時候,會先執行testSometing()
這個函式列印出“testSometing
”的字串。
之後因為await
會讓出執行緒就會去執行後面的。testAsync()
執行完畢返回resolve
,觸發promiseFn
列印出“promise START...
”。
接下來會把返回的Promiseresolve("promise RESOLVE")
放入Promise佇列(Promise的Job Queue),繼續執行列印“===END===
”。
等本輪事件迴圈執行結束後,又會跳回到async
函式中(test()
函式),等待之前await
後面表示式的返回值,因為testSometing()
不是async
函式,所以返回的是一個字串“return
testSometing
”。
test()
函式繼續執行,執行到testFn2()
,再次跳出test()
函式,列印出“testAsync
”,此時事件迴圈就到了Promise的佇列,執行promiseFn.then((val)=> console.log(val));
列印出“promise RESOLVE
”。
之後和前面一樣 又跳回到test函式繼續執行console.log(testFn2)
的返回值,列印出“hello async
”。
最後列印“test end...
”。
加點料,讓testSomething()
變成async
async function testSometing() {
console.log("testSomething");
return "return testSomething";
}
async function testAsync() {
console.log("testAsync");
return Promise.resolve("hello async");
}
async function test() {
console.log("test start...");
const testFn1 = await testSometing();
console.log(testFn1);
const testFn2 = await testAsync();
console.log(testFn2);
console.log('test end...');
}
test();
var promiseFn = new Promise((resolve)=> {
console.log("promise START...");
resolve("promise RESOLVE");
});
promiseFn.then((val)=> console.log(val));
console.log("===END===")複製程式碼
執行結果
promiseFn.then((val)=> console.log(val));
先於console.log(testFn1)
執行。原因是因為現在的版本async函式會被await resolve,
function testSometing() {
console.log("testSomething");
return "return testSomething";
}
console.log(Object.prototype.toString.call(testSometing)) // [object Function]
console.log(Object.prototype.toString.call(testSometing())) // [object String]
async function testSometing() {
console.log("testSomething");
return "return testSomething";
}
console.log(Object.prototype.toString.call(testSometing)) // [object AsyncFunction]
console.log(Object.prototype.toString.call(testSometing())) // [object Promise]複製程式碼
testSomething()
已經是async函式,返回的是一個Promise物件需要等它 resolve 後將當前Promise 推入佇列,隨後先清空呼叫棧,所以會"跳出" test() 函式執行後續程式碼,隨後才開始執行該 Promise。
更新
今天是2019-11-06 12:03:46,收到幾位掘友的反饋,最後一個例子的執行時機不符合現有版本的result,原因可能是在文章完結後至今的一年半內 Chrome V8 更新導致,如果有 Opear 或者是其他使用了 Chrome 核心的瀏覽器,或者到 這個地址 下載舊版本 Chrome,筆者測試使用的是 Opera,在 Chrome V8 release 文件或裡並未找到相關說明。
之前的版本中,await 的函式需要3次tick才會被執行,在當前的版本,v8團隊為了提升效能,違反了規範,沒有嚴格按照 promise-resolve-functions 的第13步執行,導致3次tick減少2次。原文
在Node11版本以上,行為已經和瀏覽器統一,從表面上,你大可以理解 await AsyncFunction 返回的 Promise 為同步任務。
後話
越來越多的人正在研究據說是非同步終極程式設計解決方案的async/await,但是大部分人對這個方法內部怎麼執行的還不是很瞭解,整理了await之後js的執行順序,希望對你們有所幫助
- 是一種編寫非同步程式碼的新方法。之前非同步程式碼的方案是callback和promise。
- 建立在 promise 的基礎上,與promise一樣也是非阻塞的。
- async/await 讓非同步程式碼看起來、表現起來更像同步程式碼。這正是其威力所在。
參考文獻:理解 JavaScript 的 async/await