關注前端小謳,閱讀更多原創技術文章
非同步函式
- ES8 新增非同步函式(
async/await
),是 ES6 期約模式在 ECMAScript 函式中的應用 - 以同步方式的程式碼執行非同步
非同步函式
- ES8 對函式進行了擴充套件,新增 2 個關鍵字
async
和await
async
async
關鍵字用於宣告非同步函式,可用在函式宣告、函式表示式、箭頭函式和方法上
async function foo() {} // 用在函式宣告
let bar = async function () {} // 用在函式表示式
let baz = async () => {} // 用在箭頭函式
class Qux {
async qux() {} // 用在方法
}
async
關鍵字讓函式具有非同步特性,程式碼仍同步求值,引數或閉包也具有普通 JS 函式的正常行為
async function foo() {
console.log(1)
}
foo()
console.log(2)
/*
1,foo()函式先被求值
2
*/
非同步函式
return
返回的值,會被Promise.resolve()
包裝成期約物件,呼叫非同步函式始終返回該期約物件- 若
return
關鍵字返回的是實現thenable
介面的物件(callback
、期約),該物件由提供給then()
的處理程式解包 - 若
return
關鍵字返回的是常規的值,返回值被當作已解決的期約(無return
關鍵字,返回值被當作 undefined)
- 若
async function foo() {
return 'foo' // 返回原始值
}
console.log(foo()) // Promise {<fulfilled>: "foo"},被當作已解決的期約
foo().then((result) => console.log(result)) // 'foo'
async function bar2() {
return ['bar'] // 返回沒有實現thenable介面的物件
}
console.log(bar2()) // Promise {<fulfilled>: ['bar']},被當作已解決的期約
bar2().then((result) => console.log(result)) // ['bar']
async function baz2() {
const thenable = {
then(callback) {
callback('baz')
},
}
return thenable // 返回實現了thenable介面的非期約物件
}
console.log(baz2()) // Promise {<pending>}
baz2().then((result) => console.log(result)) // 'baz',由then()解包
async function qux() {
return Promise.resolve('qux') // 返回解決的期約
}
console.log(qux()) // Promise {<pending>}
qux().then((result) => console.log(result)) // 'qux',由then()解包
async function rejectQux() {
return Promise.reject('qux') // 返回拒絕的期約
}
console.log(rejectQux()) // Promise {<pending>}
rejectQux().then(null, (result) => console.log(result)) // 'qux',由then()解包
// Uncaught (in promise) qux
rejectQux().catch((result) => console.log(result)) // 'qux',由catch()解包
- 非同步函式中丟擲錯誤會返回拒絕的期約
async function foo() {
console.log(1)
throw 3
}
foo().catch((result) => console.log(result)) // 給返回的期約新增拒絕處理程式
console.log(2)
/*
1,foo()函式先被求值
2
3
*/
- 非同步函式中拒絕期約的錯誤(非“返回拒絕的期約”)不會被非同步函式捕獲
async function foo() {
Promise.reject(3) // 拒絕的期約(非返回)
}
foo().catch((result) => console.log(result)) // catch()方法捕獲不到
// Uncaught (in promise) 3,瀏覽器訊息佇列捕獲
await
- 使用
await
關鍵字可以暫停非同步函式程式碼執行,等待期約解決
let p = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 3)
})
p.then((x) => console.log(x)) // 3
// 用async/await重寫
async function foo() {
let p = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 3)
})
console.log(await p)
}
foo() // 3
await
會嘗試解包物件的值(與yield
類似),然後將該值傳給表示式,而後非同步恢復執行非同步函式
async function foo() {
console.log(await Promise.resolve('foo')) // 將期約解包,再將值傳給表示式
}
foo()
async function bar2() {
return await Promise.resolve('bar')
}
bar2().then((res) => console.log(res)) // 'bar'
async function baz2() {
await new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
console.log('baz')
}
baz2() // 'baz'(1000毫秒後)
await
根據等待的值,執行不同的操作- 若等待的值是實現
thenable
介面的物件(callback
、期約),該物件由await
來解包 - 若等待的值是常規值,該值被當作已解決的期約(然後再由
await
來解包)
- 若等待的值是實現
async function foo() {
console.log(await 'foo') // 等待原始值,被當作已解決的期約Promise.resolve('foo'),再由await解包
}
foo() // 'foo'
async function bar2() {
console.log(await ['bar']) // 等待值是沒有實現thenable介面的物件,被當作已解決的期約再由await解包
}
bar2() // ["bar"]
async function baz2() {
const thenable = {
then(callback) {
callback('baz')
},
}
console.log(await thenable) // 等待值是實現了thenable介面的非期約物件,由await解包
}
baz2() // 'baz'
async function qux() {
console.log(await Promise.resolve('qux')) // 等待值是解決的期約
}
qux() // 'qux'
- 等待會丟擲錯誤的同步操作,會返回拒絕的期約
async function foo() {
console.log(1)
await (() => {
throw 3 // 丟擲錯誤的同步操作
})()
}
foo().catch((result) => console.log(result)) // 給返回的期約新增拒絕處理程式
console.log(2)
/*
1
2
3
*/
- 對拒絕的期約使用
await
,會釋放錯誤值(將拒絕期約返回)
async function foo() {
console.log(1)
await Promise.reject(3) // 對拒絕的期約使用await,將其返回(後續程式碼不再執行)
console.log(4) // 不執行
}
foo().catch((result) => console.log(result)) // 給返回的期約新增拒絕處理程式
console.log(2)
/*
1
2
3
*/
await 的限制
- 必須在非同步函式中使用
- 不能在頂級上下文(如
<script>
標籤或模組)中使用 - 可以定義並立即呼叫非同步函式
- 非同步函式的特質不會擴充套件到巢狀函式
async function foo() {
console.log(await Promise.resolve(3)) // 必須在非同步函式中使用
}
foo() // 3
;(async function () {
console.log(await Promise.resolve(3)) // 3,立即呼叫的非同步函式表示式
})()
const syncFn = async () => {
console.log(await Promise.resolve(3)) // 在箭頭函式中使用,箭頭函式前一樣要加async
}
syncFn() // 3
function foo() {
// console.log(await Promise.resolve(3)) // 不允許在同步函式中使用
}
async function foo() {
// function bar() {
// console.log(await Promise.resolve(3)) // 錯誤:非同步函式不會擴充套件到巢狀函式
// }
async function bar() {
console.log(await Promise.resolve(3)) // 需要在bar前加async
}
}
停止和恢復執行
async/await
真正起作用的是await
(async
只是識別符號)- JS 在執行時碰到
await
關鍵字,會記錄在哪裡暫停執行 - 等到
await
右邊的值可以用時,JS 向訊息佇列推送任務,該任務恢復非同步函式的執行 - 即使
await
右邊跟著一個立即可用的值,函式也會暫停,且其餘部分會被非同步求值
- JS 在執行時碰到
// async只是識別符號
async function foo() {
console.log(2)
}
console.log(1)
foo()
console.log(3)
/*
1
2
3
*/
// 遇到await -> 記錄暫停 -> await右邊的值可用 -> 恢復執行非同步函式
async function foo() {
console.log(2)
await null // 暫停,且後續操作變為非同步
// 為立即可用的值null向訊息佇列中新增一個任務
console.log(4)
}
console.log(1)
foo()
console.log(3)
/*
1
2
3
4
*/
如果
await
後面是一個期約,則會有兩個任務被新增到訊息佇列並被非同步求值- 第一個任務是等待期約的返回值,第二個任務是拿到返回值後執行程式
- tc39 對
await
後面是期約的情況做過 1 次修改,await Promise.resolve()
不再生成 2 個非同步任務,而只是 1 個
async function foo() {
console.log(2)
console.log(await Promise.resolve(8))
console.log(9)
}
async function bar2() {
console.log(4)
console.log(await 6)
console.log(7)
}
console.log(1)
foo()
console.log(3)
bar2()
console.log(5)
/*
書本順序:1 2 3 4 5 6 7 8 9
瀏覽器順序:1 2 3 4 5 8 9 6 7(tc39做過1次修改)
*/
非同步函式策略
實現 sleep()
- 可以利用非同步函式實現類似
JAVA
中Thread.sleep()
的函式,在程式中加入非阻塞的暫停
function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay)) // 設定延遲,延遲後返回一個解決的期約
}
async function foo() {
const t0 = Date.now()
await sleep(1500) // 暫停約1500毫秒
console.log(Date.now() - t0)
}
foo() // 1507
利用平行執行
- 按順序等待 5 個隨機的超時
async function randomDelay(id) {
const delay = Math.random() * 1000 // 隨機延遲0-1000毫秒
return new Promise((resolve) =>
setTimeout(() => {
console.log(`${id} finished`)
resolve()
}, delay)
)
}
async function foo() {
const t0 = Date.now()
await randomDelay(0)
await randomDelay(1)
await randomDelay(2)
await randomDelay(3)
await randomDelay(4)
console.log(`${Date.now() - t0} ms elapsed`)
}
foo()
/*
0 finished
1 finished
2 finished
3 finished
4 finished
3279 ms elapsed
*/
// 用for迴圈重寫
async function foo() {
const t0 = Date.now()
for (let i = 0; i < 5; i++) {
await randomDelay(i)
}
console.log(`${Date.now() - t0} ms elapsed`)
}
foo()
/*
0 finished
1 finished
2 finished
3 finished
4 finished
3314 ms elapsed
*/
- 不考慮順序時,可以先一次性初始化所有期約,分別等待結果(獲得平行加速)
async function foo() {
const t0 = Date.now()
// 一次性初始化所有期約
const p0 = randomDelay(0)
const p1 = randomDelay(1)
const p2 = randomDelay(2)
const p3 = randomDelay(3)
const p4 = randomDelay(4)
// 分別等待結果,延遲各不相同
await p0
await p1
await p2
await p3
await p4
console.log(`${Date.now() - t0} ms elapsed`)
}
foo()
/*
4 finished
3 finished
1 finished
0 finished
2 finished
870 ms elapsed,大幅度降低總耗時
*/
// 用陣列和for迴圈再次包裝
async function foo() {
const t0 = Date.now()
const promises = Array(5)
.fill(null)
.map((item, i) => randomDelay(i))
for (const p of promises) {
await p
}
console.log(`${Date.now() - t0} ms elapsed`)
}
foo()
/*
1 finished
3 finished
0 finished
4 finished
2 finished
806 ms elapsed
*/
- 儘管期約未按順序執行,但
await
按順序收到每個期約的值
async function randomDelay(id) {
const delay = Math.random() * 1000 // 隨機延遲0-1000毫秒
return new Promise((resolve) =>
setTimeout(() => {
console.log(`${id} finished`)
resolve(id)
}, delay)
)
}
async function foo() {
const t0 = Date.now()
const promises = Array(5)
.fill(null)
.map((item, i) => randomDelay(i))
for (const p of promises) {
console.log(`awaited ${await p}`)
}
console.log(`${Date.now() - t0} ms elapsed`)
}
foo()
/*
1 finished
4 finished
0 finished
awaited 0
awaited 1
2 finished
awaited 2
3 finished
awaited 3
awaited 4
833 ms elapsed
*/
序列執行期約
- 使用
async/await
做期約連鎖
function addTwo(x) {
return x + 2
}
function addThree(x) {
return x + 3
}
function addFive(x) {
return x + 5
}
async function addTen(x) {
for (const fn of [addTwo, addThree, addFive]) {
x = await fn(x)
}
return x
}
addTen(9).then((res) => console.log(res)) // 19
- 將函式改成非同步函式,返回期約
async function addTwo(x) {
return x + 2
}
async function addThree(x) {
return x + 3
}
async function addFive(x) {
return x + 5
}
addTen(9).then((res) => console.log(res)) // 19
棧追蹤與記憶體管理
在超時處理執行和拒絕期約時,錯誤資訊包含巢狀函式的識別符號(被呼叫以建立最初期約例項的函式)棧追蹤資訊中不應該看到這些已經返回的函式
- JS 引擎會在建立期約時,儘可能保留完整的呼叫棧,丟擲錯誤時棧追蹤資訊會佔用記憶體,帶來一些計算和儲存成本
function fooPromiseExecutor(resolve, reject) {
setTimeout(reject, 1000, 'bar')
}
function foo() {
new Promise(fooPromiseExecutor)
}
foo()
/*
Uncaught (in promise) bar
setTimeout (async) // 錯誤資訊包含巢狀函式的識別符號
fooPromiseExecutor // fooPromiseExecutor函式已返回,不應該在棧追蹤資訊中看到
foo
*/
- 換成非同步函式,已經返回的函式不會出現在錯誤資訊中,巢狀函式(在記憶體)中儲存指向包含函式的指標,不會帶來額外的消耗
async function foo() {
await new Promise(fooPromiseExecutor)
}
foo()
/*
Uncaught (in promise) bar
foo
async function (async)
foo
*/
總結 & 問點
- async 關鍵字的用法是什麼?根據函式內返回值的不同,非同步函式的返回值有哪些情況?
- await 關鍵字的用法是什麼?根據等待值的不同,呼叫非同步函式有哪些情況?其使用有哪些限制?
- JS 執行時遇到 await 關鍵字會怎樣?函式的其餘部分會在何時恢復執行?
- 寫一段程式碼,用非同步函式實現在程式中加入非阻塞的暫停
- 寫一段程式碼,用非同步函式平行執行多個期約,隨機設定這些期約的延遲,並計算期約全部完成後的使用的時間
- 寫一段程式碼,用非同步函式做期約連鎖