ES7 引入了 async/await,這是 JavaScript 異步編程的一個比較大的改進。我們可以像寫同步代碼一些編寫異步代碼,避免了回調地獄,同時也代碼也比 Promise 更易於閱讀。
async 和 await 也是面試經常被問到的東西,之前一直只限於會用,並不太理解內部的實現原理。今天就來好好探究探究,JavaScript 中的 async 和 await 到底是怎麼工作的。
async
async 就是異步的意思,在函數的定義前加上 async 關鍵字,表示這是一個異步函數,意味着該函數的執行不會阻塞後面代碼的執行。
先來看一個簡單的例子:
async function hello() {
console.log('hello');
}
hello();
console.log('world');
可以猜猜這段代碼的輸出順序是什麼?
輸出的順序如下:
hello
world
你可能要跳出來罵我了,前面不是說不會阻塞後面代碼的執行麼,爲什麼還是按順序輸出的?
先別急着罵,容我再進一步解釋。
我們先一起來看看 hello 函數的返回值是什麼。
async function hello() {
console.log('hello');
return 'hello';
}
console.log(hello());
console.log('world');
上面的代碼輸出如下:
hello
Promise {<resolved>: "hello"}
world
hello()
函數並沒有指定返回值,但是默認返回了一個 Promise 對象,而 Promise 的。
emm…看到這個 Promise,似乎有些清楚了。
也就是說,async 函數返回一個 Promise 對象,因此可以使用 then 方法添加回調函數。並且 async 函數內部 return 語句返回的值,會成爲 then 方法回調函數的參數。
再修改一下上面的代碼:
async function hello() {
console.log('hello before');
return 'hello after';
}
hello().then(value => console.log(value))
console.log('world');
上面的代碼輸出結果:
hello before
world
hello after
爲什麼 hello after
會在 world
後輸出呢?這是因爲 Promise 在 JavaScript 內部的運行機制,Promise 屬於「微任務」,在當前腳本代碼執行完後,立刻執行的,並沒有參與事件循環。
既然返回值是 Promise,那麼說明函數的執行也有對應的狀態,如果函數正常執行,內部調用 Promise.resolve(),如果函數有錯誤,則調用 Promise.reject() 返回一個 Promise 對象。
繼續修改上面的代碼:
async function hello(flag) {
if (flag) {
return 'hello';
} else {
throw 'failed';
}
}
console.log(hello(true));
console.log(hello(false));
代碼輸出:
Promise { resolved }
Promise { rejected }
await
await 就是等待的意思,即等待請求或者資源。await 後面可以接任何普通表達式,但一般會在 await 後面放一個返回 promise 對象的表達式。
注意 :await 關鍵字只能放到 async 函數裏面。
如果 await 等待的是一個 Promise,它就會阻塞後面的代碼,等着 Promise 對象 resolve,然後得到 resolve 的值,作爲 await 表達式的運算結果。
看到「阻塞」兩個字,心裏咯噔一下。尼瑪,這一阻塞,函數外的代碼不會也暫停執行麼。
**這個你不用擔心,這就是 await 必須用在 async 函數中的原因。**async 函數調用不會造成阻塞,就像上文所描述的。它內部所有的阻塞都被封裝在一個 Promise 對象中異步執行。
先來看下面的代碼:
function delay(seconds) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(seconds)
}, seconds);
});
}
async function hello() {
console.log('hello');
let result = await delay(2000);
console.log(result);
}
hello();
console.log('world');
上面定義了一個 delay 函數,接受一個 seconds 參數。函數返回一個 Promise 對象,這個 Promise 對象傳入的是一個定時器。
然後在 hello
函數中去調用 delay 函數,指定 await 關鍵字。當代碼執行到result
這一行是,會阻塞 2 秒鐘。等 Promise 的 resolve,返回執行結果,然後賦值給 result,代碼纔開始繼續執行,執行下一行的 console.log 語句。
輸出結果如下:
hello
world
// 兩秒後...
2000
這樣我們就以同步代碼的方式編寫了一段異步請求的函數,如果遇到多個異步的代碼,await 更能體現出寫法上的可讀性。
例如下面的代碼:
async function hello() {
console.log('hello');
let result1 = await delay(2000);
let result2 = await delay(3000);
let result3 = await delay(4000);
console.log(result1+result2+result3);
}
如果用 Promise :
delay(2000).then(res=> {
console.log(res);
return delay(3000);
}).then(res => {
console.log(res);
return delay(4000);
}).then(res => {
console.log(res);
})
可以看到,使用 await 寫異步代碼真的非常的方便,而且代碼的可讀性非常的好,也沒有回調地獄了,小夥伴們趕緊用起來。