1. 程式人生 > 實用技巧 >Promise和Async/await的理解和使用

Promise和Async/await的理解和使用

「長文乾貨」Promise和Async/await的理解和使用

注意:文中的程式碼比較多,手機端瀏覽起來可能比較費勁,建議在PC端瀏覽,如果程式碼排版亂了請點選文末瞭解更多連線檢視排版更友好的原文。

一、前置知識

1.1 區別例項物件與函式物件

例項物件:new 函式產生的物件, 稱為例項物件, 簡稱為物件。

函式物件: 將函式作為物件使用時, 簡稱為函式物件

function Fn() {}
const fn = new Fn() // fn為例項物件
Fn.bind({}) // Fn為函式物件

1.2 兩種型別的回撥函式

同步回撥

  • 理解:立即執行, 完全執行完了才結束, 不會放入回撥佇列中
  • 例子: 陣列遍歷相關的回撥函式 / Promise 的 excutor 函式

非同步回撥

  • 理解:不會立即執行, 會放入回撥佇列中將來執行
  • 例子:定時器回撥 / ajax 回撥 / Promise 的成功|失敗的回撥
const arr = [1, 2, 3]
arr.forEach(item => console.log(item)) // 同步回撥, 不會放入回撥佇列, 而是立即執行
console.log('forEatch()之後')
setTimeout(() => { // 非同步回撥, 會放入回撥佇列, 所有同步執行完後才可能執行
console.log('timout 回撥'
)
}, 0)
console.log('setTimeout 之後')

1.3 JS 的 error 處理

錯誤的型別

  • Error:所有錯誤的父型別
  • ReferenceError:引用的變數不存在
  • TypeError:資料型別不正確的錯誤
  • RangeError:資料值不在其所允許的範圍內
  • SyntaxError:語法錯誤
console.log(a) // ReferenceError: a is not defined
let b = null
console.log(b.xxx) // TypeError: Cannot read property 'xxx' of null
function fn() {
fn()
}
fn() // RangeError: Maximum call stack size exceeded
let c = """" // SyntaxError: Unexpected string

錯誤處理:

  • 捕獲錯誤:try ... catch
  • 丟擲錯誤:throw error

error 物件的結構

  • message 屬性:錯誤相關資訊
  • stack 屬性:函式呼叫棧記錄資訊

二、Promise 是什麼?

2.1 理解

抽象表達:Promise 是JS中進行非同步程式設計的新的解決方案(舊的是誰?=> 純回撥的形式)

具體表達:

  • 從語法上來說:Promise 是一個建構函式
  • 從功能上來說:Promise 物件用來封裝一個非同步操作並可以獲取其結果

2.2 Promise 的狀態改變

  1. pending 變為 resolved
  2. pending 變為rejected

說明:只有這2種,且一個 Promise 物件只能改變一次,無論變成成功還是失敗,都會有一個結果資料,成功的結果資料一般稱為 value,失敗的結果資料一般稱為 reason。

2.3 Promise 基本流程

Promise 基本流程

1.4 Promise 的基本使用

示例,如果當前時間是偶數就代表成功,否則代表失敗

// 1. 建立一個新的Promise物件
const p = new Promise((resolve, reject) => { // 執行器函式,同步執行
// 2. 執行非同步操作任務
setTimeout(() => {
const time = Date.now() // 如果當前時間是偶數就代表成功,否則代表失敗
// 3.1 如果成功了,呼叫resolve(value)
if (time % 2 === 0) {
resolve('成功的資料,value = ' + time)
} else {
// 3.2 如果失敗了,呼叫reject(reason)
reject('失敗的資料,reason = ' + time)
}

}, 1000);
})

p.then(value => {
// 接受得到成功的value資料,專業術語:onResolved
console.log('成功的回撥', value)
}, reason => {
// 接受得到失敗的reason資料,專業術語:onRejected
console.log('失敗的回撥', reason)
})

三、為什麼要用 Promise?

3.1 指定回撥函式的方式更加靈活

舊的:回撥函式必須在啟動非同步任務前指定

// 成功的回撥函式
function successCallback(result) {
console.log('處理成功:' + result)
}
function failureCallback(error) {
console.log('處理失敗:' + error)
}

// 使用純回撥函式
createAudioFileSync(audioSettings, successCallback, failureCallback)

Promise:啟動非同步任務 => 返回 Promise 物件 => 給 Promise 物件繫結回撥函式,甚至可以在非同步任務結束後指定多個

// 使用 Promise
const promise = createAudioFileSync(audioSettings)
setTimeout(() => {
promise.then(successCallback, failureCallback)
}, 3000);

3.2 支援鏈式呼叫,可以解決回撥地獄問題

什麼是回撥地獄?回撥函式巢狀呼叫,外部回撥函式非同步執行的結果是巢狀的回掉執行條件,程式碼是水平向右擴充套件

// 回撥地獄
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult)
}, failureCallback)
}, failureCallback)
},

回撥地獄的缺點:不便閱讀,不便於異常處理

解決方案:Promise 鏈式呼叫,程式碼水平向下擴充套件

doSomething().then(function(result) {
return doSomethingElse(result)
})
.then(function(newResult) {
return doThirdThing(newResult)
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult)
})
.catch(failureCallback)

終極解決方案:async/await,用同步的寫法處理非同步的操作

async function request() {
try {
const result = await doSomething()
const newResult = await doSomethingElse(result)
const finalResult = await doThirdThing(newResult)
console.log('Got the final result: ' + finalResult)
} catch (error) {
failureCallback(error)
}
}

四、Promise 的API說明

1)Promise 建構函式 Promise (excutor) {},excutor 會在 Promise 內部立即同步回撥,非同步操作在執行器中執行

  1. excutor 函式:執行器 (resolve, reject) => {}
  2. resolve 函式:內部定義成功時我們呼叫的函式 value => {}
  3. reject 函式:內部定義失敗時我們呼叫的函式 reason => {}

2)Promise.prototype.then 方法:(onResolved, onRejected) => {},指定用於得到成功 value 的成功回撥和用於得到失敗 reason 的失敗回撥返回一個新的 promise 物件

  1. onResolved 函式:成功的回撥函式 (value) => {}
  2. onRejected 函式:失敗的回撥函式 (reason) => {}

3)Promise.prototype.catch 方法:(onRejected) => {},onRejected 函式:失敗的回撥函式 (reason) => {}then() 的語法糖, 相當於: then(undefined, onRejected)。

4)Promise.resolve 方法:(value) => {},value:成功的資料或 promise 物件,返回一個成功/失敗的 promise 物件。

5)Promise.reject 方法:(reason) => {},reason:失敗的原因,返回一個失敗的 promise 物件

6)Promise.all 方法: (promises) => {},promises:包含 n 個 promise 的陣列,返回一個新的 promise, 只有所有的 promise 都成功才成功, 只要有一個失敗了就直接失敗。

7)Promise.race 方法:(promises) => {},promises: 包含 n 個 promise 的陣列,返回一個新的 promise, 第一個完成的 promise 的結果狀態就是最終的結果狀態。

// 產生一個成功值為 1 的 Promise 物件
const p1 = new Promise((resolve, reject) => {
resolve(1)
})
// 產生一個成功值為 2 的 Promise 物件
const p2 = Promise.resolve(2)
// 產生一個失敗值為 3 的 Promise 物件
const p3 = Promise.reject(3)

p1.then(value => console.log(value))
p2.then(value => console.log(value))
p3.catch(reason => console.error(reason))

// const pAll = Promise.all([p1, p2])
const pAll = Promise.all([p1, p2, p3])
pAll.then(values => {
console.log('all onResolved()', values) // all onResolved() [ 1, 2 ]
}, reason => {
console.log('all onRejected()', reason) // all onRejected() 3
})

const race = Promise.race([p1, p2, p3])
race.then(value => {
console.log('all onResolved()', value)
}, reason => {
console.log('all onRejected()', reason)
})

五、Promise 的幾個關鍵問題

5.1 如何改變 Promise 的狀態

resolve(value),如果當前是 pendding 就會變為 resolved

reject(reason),如果當前是 pendding 就會變為 rejected

丟擲異常,如果當前是 pendding 就會變為 rejected

const p = new Promise((resolve, reject) => {
// resolve(1) // Promise 變為 resolved 成功狀態
// reject(2) // Promise 變為 rejected 失敗狀態
// Promise 變為 rejected 失敗狀態,reason為丟擲的 error
throw new Error('我丟擲的異常')
// 變為 rejected 失敗狀態,reason為丟擲的 3
// throw 3
})
p.then(
value => {},
reason => { console.log('reason :', reason); }
)

5.2 當一個 promise 指定多個成功/失敗回撥函式, 都會呼叫嗎?

當 promise 改變為對應狀態時都會呼叫

const p = new Promise((resolve, reject) => {
// 變為 rejected 失敗狀態,reason為丟擲的 3
throw 3
})
p.then(
value => {},
reason => { console.log('reason :', reason); }
)
p.then(
value => {},
reason => { console.log('reason2 :', reason); }
)
// 結果:
// reason : 3
// reason2 : 3

5.3 改變 promise 狀態和指定回撥函式誰先誰後?

都有可能, 正常情況下是先指定回撥再改變狀態, 但也可以先改狀態再指定回撥。

如何先改狀態再指定回撥?

  • 在執行器中直接呼叫 resolve()/reject()
  • 延遲更長時間才呼叫 then()

什麼時候才能得到資料?

  • 如果先指定的回撥, 那當狀態發生改變時, 回撥函式就會呼叫, 得到資料
  • 如果先改變的狀態, 那當指定回撥時, 回撥函式就會呼叫, 得到資料
// 常規:先指定回撥函式,後改變狀態
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1) // 後改變狀態(同時指定資料),非同步執行回撥函式
}, 1000);
}).then( // 先指定回撥函式,儲存當前指定的回撥函式
value => {},
reason => { console.log('reason :', reason); }
)

// 先改狀態,後指定回撥函式
new Promise((resolve, reject) => {
resolve(1) // 先改變狀態(同時指定資料)
}).then( // 後指定回撥函式,非同步執行回撥函式
value => { console.log('value2:', value);},
reason => { console.log('reason2 :', reason); }
)

const p = new Promise((resolve, reject) => {
resolve(1) // 先改變狀態(同時指定資料)
})
setTimeout(() => {
p.then(
value => { console.log('value3:', value);},
reason => { console.log('reason3 :', reason); }
)
}, 1500);

5.4 promise.then()返回的新 promise 的結果狀態由什麼決定?

簡單表達:由 then() 指定的回撥函式執行的結果決定。

詳細表達:

  • 如果丟擲異常, 新 promise 變為 rejected, reason 為丟擲的異常
  • 如果返回的是非 promise 的任意值, 新 promise 變為 resolved, value 為返回的值
  • 如果返回的是另一個新 promise, 此 promise 的結果就會成為新 promise 的結果
new Promise((resolve, reject) => {
resolve(1)
}).then(
value => {
console.log('onResolved1()', value); // 1
// return 1.1
return Promise.resolve(1.1)
// return Promise.reject(1.1)
// throw 1.1
},
reason => {
console.log('onRejected1()', reason);
}
).then(
value => { console.log('onResolved2()', value); }, // 1.1
reason => { console.log('onRejected2()', reason) } // 1.1
)

5.5 promise 如何串連多個操作任務

promise 的 then() 返回一個新的 promise, 可以開成 then() 的鏈式呼叫,通過 then 的鏈式呼叫串連多個同步/非同步任務。

5.6 promise 異常傳透

當使用 promise 的 then 鏈式呼叫時, 可以在最後指定失敗的回撥,前面任何操作出了異常, 都會傳到最後失敗的回撥中處理。

下面的示例程式碼演示了異常傳透

new Promise((resolve, reject) => {
// resolve(1)
reject(1)
}).then(
value => {
console.log('onResolved1()', value);
return 2
}
).then(
value => {
console.log('onResolved2()', value);
return 3
}
).then(
value => {
console.log('onResolved3()', value);
}
).catch(
reason => {
console.log('onRejected()', reason); // onRejected() 1
}
)

程式碼會執行 .catch 中的程式碼,但實際上程式碼的執行不是執行到第 3 行就直接跳轉到 catch 裡面了,而是從第一個 then 呼叫向下一個個的執行(逐級傳遞),但是由於我們 then 裡面沒有處理異常。在 then 裡面沒寫處理異常實際上相當於預設添加了 reason => { throw reason } 或者 reason => Promise.reject(reason)

new Promise((resolve, reject) => {
reject(1)
}).then(
value => { console.log('onResolved1()', value); },
// reason => { throw reason }
// 或者
reason => Promise.reject(reason)
)

Promise的異常傳透示意圖

5.7 中斷 promise 鏈

當使用 promise 的 then 鏈式呼叫時, 在中間中斷, 不再呼叫後面的回撥函式

辦法: 在回撥函式中返回一個 pendding 狀態的 promise 物件

new Promise((resolve, reject) => {
resolve(1)
}).then(
value => {
console.log('onResolved1()', value);
return new Promise(() => {}) // 返回一個 pending 的 Promise,中斷 promise 鏈
}
).then( // 這個 then 不會執行力
value => { console.log('onResolved2()', value); }
)

六、async 與 await

Async/await 實際上只是一種基於 promises 的糖衣語法糖,Async/awaitpromises 一樣,都是非堵塞式的,Async/await 讓非同步程式碼更具同步程式碼風格,這也是其優勢所在。

async function 用來定義一個返回 AsyncFunction 物件的非同步函式。非同步函式是指通過事件迴圈非同步執行的函式,它會通過一個隱式的 Promise 返回其結果,。如果你在程式碼中使用了非同步函式,就會發現它的語法和結構會更像是標準的同步函式。

await 操作符用於等待一個 Promise 物件。它只能在非同步函式 async function 中使用。

6.1 async 函式

async 函式的返回值為 Promise 物件,async 函式返回的 Promise 的結果由函式執行的結果決定

async function fn1() {
return 1
}
const result = fn1()
console.log(result) // Promise { 1 }

在控制檯可以看見如下資訊

既然是 Promise 物件,那麼我們用 then 來呼叫,並丟擲錯誤,執行 onRejected() 且 reason 為錯誤資訊為“我是錯誤”

async function fn1() {
// return 1
// return Promise.resolve(1)
// return Promise.reject(2)
throw '我是錯誤'
}
fn1().then(
value => { console.log('onResolved()', value) },
reason => { console.log('onRejected()', reason) } // onRejected() 我是錯誤
)

6.2 await 表示式

await 右側的表示式一般為 promise 物件, 但也可以是其它的值

  1. 如果表示式是 promise 物件, await 返回的是 promise 成功的值
  2. 如果表示式是其它值, 直接將此值作為 await 的返回值
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1000)
}, 1000);
})
}
function fn4() { return 6 }
async function fn3() {
// const value = await fn2() // await 右側表示式為Promise,得到的結果就是Promise成功的value
// const value = await '還可以這樣'
const value = await fn4()
console.log('value', value)
}
fn3() // value 6

await 必須寫在 async 函式中, 但 async 函式中可以沒有 await,如果 await 的 Promise 失敗了, 就會丟擲異常, 需要通過 try...catch 捕獲處理。

function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve(1000)
reject(1000)
}, 1000);
})
}
async function fn3() {
try {
const value = await fn2()
} catch (error) {
console.log('得到失敗的結果', error)
}
}
fn3() // 得到失敗的結果 1000

6.3 Async/await 比 Promise 更優越的表現

簡潔乾淨,使用 async/await 能省去寫多少行程式碼

錯誤處理,async/wait 能用相同的結構和好用的經典 try/catch 處理同步和非同步錯誤,錯誤堆疊能指出包含錯誤的函式。

除錯,async/await 的一個極大優勢是它更容易除錯,使用 async/await 則無需過多箭頭函式,並且能像正常的同步呼叫一樣直接跨過 await 呼叫。

全文完

轉發 @杭州程式設計師小張: 「長文乾貨」Promise和Async/await的理解和使用

0/2000字

轉發