壓箱底筆記:Promise和Async/await的理解和使用

2021-02-19 前端全棧開發者

以前學習寫的筆記,感覺還不錯,現在發出來,希望對你有幫助。如果文章對你有所啟發和幫助,可以『一鍵三連』。哦,對了,我已經脫髮了...😭😭

目錄

1. 前置知識

1.1 區別實例對象與函數對象

1.2 兩種類型的回調函數

1.3 JS的error處理

2. Promise 是什麼?

2.1 理解

2.2 Promise的狀態改變

2.3 Promise基本流程

2.4 Promise的基本使用

3. 為什麼要用Promise?

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

3.2 支持鏈式調用,可以解決回調地獄問題

4. Promise的API說明

4.1 API 說明

4.2 Promise的幾個關鍵問題

5. async與await

1. 前置知識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處理

錯誤的類型

ReferenceError:引用的變量不存在
console.log(a) // ReferenceError: a is not defined

TypeError:數據類型不正確的錯誤
let b = null
console.log(b.xxx) // TypeError: Cannot read property 'xxx' of null

RangeError:數據值不在其所允許的範圍內
function fn() {
fn()
}
fn() // RangeError: Maximum call stack size exceeded

SyntaxError:語法錯誤
let c = """" // SyntaxError: Unexpected string

錯誤處理

error 對象的結構

2. Promise 是什麼?2.1 理解

抽象表達:Promise 是JS中進行異步編程的新的解決方案(舊的是誰?=> 純回調的形式)

具體表達:

從功能上來說:Promise 對象用來封裝一個異步操作並可以獲取其結果2.2 Promise的狀態改變

Promise的狀態改變只有這2種:

且一個 Promise 對象只能改變一次,無論變成成功還是失敗,都會有一個結果數據,成功的結果數據一般稱為 value,失敗的結果數據一般稱為 reason。

2.3 Promise基本流程

Promise基本流程2.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)
})

3. 為什麼要用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)
}
}

4. Promise的API說明4.1 API 說明

Promise 構造函數

Promise (excutor) {},excutor 會在 Promise 內部立即同步回調,異步操作在執行器中執行

excutor 函數:執行器 (resolve, reject) => {}resolve 函數:內部定義成功時我們調用的函數 value => {}reject 函數:內部定義失敗時我們調用的函數 reason => {}

Promise.prototype.then方法

(onResolved, onRejected) => {},指定用於得到成功 value 的成功回調和用於得到失敗 reason 的失敗回調返回一個新的 promise 對象

onResolved 函數:成功的回調函數 (value) => {}onRejected 函數:失敗的回調函數 (reason) => {}

Promise.prototype.catch 方法

(onRejected) => {},onRejected 函數:失敗的回調函數 (reason) => {},then() 的語法糖, 相當於:then(undefined, onRejected)

Promise.resolve方法

(value) => {},value:成功的數據或 promise 對象,返回一個成功/失敗的 promise 對象

Promise.reject方法

(reason) => {},reason:失敗的原因,返回一個失敗的 promise 對象

Promise.all方法

(promises) => {},promises:包含 n 個 promise 的數組,返回一個新的 promise, 只有所有的 promise 都成功才成功, 只要有一個失敗了就直接失敗

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)
})

4.2 Promise的幾個關鍵問題4.2.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); }
)

4.2.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

4.2.3 改變promise狀態和指定回調函數誰先誰後?

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

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

在執行器中直接調用 resolve()/reject()

什麼時候才能得到數據?

如果先指定的回調, 那當狀態發生改變時, 回調函數就會調用, 得到數據如果先改變的狀態, 那當指定回調時, 回調函數就會調用, 得到數據
// 常規:先指定回調函數,後改變狀態
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);

4.2.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
)

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

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

4.2.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的異常傳透示意圖

Promise的異常傳透4.2.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); }
)

5. async與await

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

async function 用來定義一個返回 AsyncFunction 對象的異步函數。異步函數是指通過事件循環異步執行的函數,它會通過一個隱式的 Promise 返回其結果,。如果你在代碼中使用了異步函數,就會發現它的語法和結構會更像是標準的同步函數。MDN async_functionawait  操作符用於等待一個Promise 對象。它只能在異步函數 async function 中使用。MDN await5.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() 我是錯誤
)

5.2 await表達式

await 右側的表達式一般為 promise 對象, 但也可以是其它的值:

如果表達式是 promise 對象, await 返回的是 promise 成功的值如果表達式是其它值, 直接將此值作為 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

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

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

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

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

相關焦點

  • JavaScript中的async/await的用法和理解
    昨天更新的是「JavaScript中的Promise使用詳解」,其實也就是說了下基本用法和自己對Promise
  • [完結篇] - 理解異步之美 --- promise與async await(三)
    天下沒有不散的宴席這個系列到這裡應該就是最後一節了,前兩章著重講了promise,為什麼著重講promise呢?因為在用法上promise要比async await難一些,而且promise本身又不是一個語法糖。沒有掌握的時候用起來就會有很多顧慮,async await卻沒有這種顧慮,用法簡單、語義清晰。
  • 8 張圖幫你一步步看清 async/await 和 promise 的執行順序
    ,可以選擇跳過這篇文章啦,或者如果你有興趣看看俺倆的理解有沒有區別,可以跳到後面的 「畫圖講解的部分」需要具備的前置知識promise的使用經驗瀏覽器端的eventloop不過如果是對 ES7 的 async 不太熟悉,是沒關係的哈,因為這篇文章會詳解 async。
  • 如何正確合理使用 JavaScript async/await
    很明顯,async/await 版本比 promise 版本更容易理解。如果忽略 await 關鍵字,代碼看起來就像任何其他同步語言,比如 Python。使用 try...catch 的好處:簡單,傳統。只要有Java或c++等其他語言的經驗,理解這一點就不會有任何困難。如果不需要每步執行錯誤處理,你仍然可以在一個 try ... catch 塊中包裝多個 await 調用來處理一個地方的錯誤。這種方法也有一個缺陷。
  • 如何在 JS 循環中正確使用 async 與 await
    在接下來的幾節中,我們將研究await 如何影響forEach、map和filter。在 forEach 循環中使用 await首先,使用 forEach 對數組進行遍歷。JavaScript 中的 forEach不支持 promise 感知,也不支持 async 和await,所以不能在 forEach 使用 await 。
  • 你必須了解的JavaScript關鍵字async和await
    為了能夠理解本文的內容,您需要對promises和generators有一個紮實的理解。這些資源應該能幫助你。Promise 假設我們有如下代碼。在這裡,我用一個Promise包裝一個HTTP請求。此外,async函數總是返回一個Promise。如果出現未捕獲的異常,則返回reject promise,否則將返回resolve promise作為異步函數的返回值。這使我們能夠調用一個async函數,並將其與常規的基於Promise的延續。
  • Async/Await有什麼用?
    當你使用 await 語句時,javascript 會暫停 async 函數的執行,等待 promise 返回一個值,然後繼續執行。Promise 本身迫使我們使用回調來獲取Promise 的值。我們無法在 main 的作用域內獲得 Promise 的值。下面是相同的程序,但是用了 async 函數和 await 語句。
  • javascript解決異步async、await和co庫的實現
    相信大家都聽說過js中的回調地獄給代碼維護帶來了很大的阻礙,應用而生的也給出了N解決方案,從最初的promise,到co庫,再到es規範提供的api async、await等!接下來咱們聊的話題就是async和co庫的具體實現在學習前咱們了解幾個小知識點吧!
  • async/await,了解一下?
    Async 和 await 相較於 * 和 yield 更加語義化。 async 函數返回值是 Promise 對象,比 Generator 函數返回的 Iterator 對象方便,可以直接使用 then()方法進行調用。
  • async/await 原理及執行順序分析
    基於這個原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個重大改進,提供了在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力,並且使得代碼邏輯更加清晰,而且還支持 try-catch 來捕獲異常,非常符合人的線性思維。所以,要研究一下如何實現 async/await。
  • 理解JavaScript 的 async/await
    1. async 和 await 在幹什麼任意一個名稱都是有意義的,先從字面意思來理解。async 是「異步」的簡寫,而 await 可以認為是 async wait 的簡寫。所以應該很好理解 async 用於申明一個 function 是異步的,而 await 用於等待一個異步方法執行完成。
  • 理解異步之美--- Promise與async await(一)
    如果表達有誤的地方,還望評論區指出~不多嗶嗶,坐穩扶好,發車了~你可能會放出一個怪物異步與同步相比,最難以掌控的就是異步的任務會什麼時候完成和完成之後的回調問題。言歸正傳:寫一個簡單的promise當Promise執行的內容符合你預期的成功條件的話,就調用resolve函數,失敗就調用reject函數,這兩個函數的參數會被promise捕捉到。可以在之後的回調中使用。創建一個承諾我們已經做完了,那麼如何使用承諾後的結果呢?
  • 代碼詳解:Async/Await優於基礎Promises的7大原因
    函數前有關鍵詞async。關鍵詞await 只能用於async定義的函數之內。任一async函數都能隱式返回promise,其解析值是從該函數返回的任意值(在此指字符串「done」)。2. 上一點意味著不能在代碼頂部使用await,因為它不在async定義的函數範圍之中。
  • 如何用實例掌握Async/Await
    回調函數程序回調的問題——臭名昭著的回調地獄在回調函數中嵌套回調函數很快就會變成這樣:回調地獄回調被嵌套在其他回調的幾層的情況,可能使代碼難以理解和維護。然後,我們可以調用promise函數的then()和.catch():then —在 promise完成後運行傳遞給它的回調。
  • C# 中的Async 和 Await 的用法詳解
    在本文中,我們將共同探討並介紹什麼是Async 和 Await,以及如何在C#中使用Async 和 Await。同樣本文的內容也大多是翻譯的,只不過加上了自己的理解進行了相關知識點的補充,如果你認為自己的英文水平還不錯,大可直接跳轉到文章末尾查看原文連結進行閱讀。
  • 有了Promise 和 then,為什麼還要使用 async?
    有了 Promise 和 then
  • 理解C#中的 async await
    .NET reflector (也可使用 dnSpy 等) 反編譯一下程序集,然後一步一步來探究 async await 內部的奧秘。3|0多個 async await 嵌套理解了async await的簡單使用,那你可曾想過,如果有多個 async await 嵌套,那會出現什麼情況呢?
  • 深入async/await知多少
    其實在使用async/await的有多少人真的了解它們呢?接下來詳細地講述它們和在什麼情況下需要注意的細節。為什麼需要它      如果你對async/await的需求不明顯,那只能說明你平時很少寫異步邏輯(當你很少接觸傳統異常邏輯編寫的情況下,最好也找些相關功能用一下先也了解一下傳統異常調整用情況)。
  • 如何使用Promise.race() 和 Promise.any() ?
    (err);     } })();   // 輸出- "promise 2 rejected"   // 儘管promise1和promise3可以解決,但promise2在請求數據時,顯示加載動畫使用加載動畫開發中是非常常見。當數據響應時間較長時,如果沒使用加載動畫,看起來就像沒有響應一樣。但有時,響應太快了,我們需要加載動畫時,增加一個非常小延遲時間,這樣會讓用戶覺得我是在經常請求過來的。要實現這一點,只需使用Promise.race()方法,如下所示。
  • 反正我是這樣處理async...await 錯誤的,你呢?
    /await,貌似很多人都是沒有異常處理的習慣,說的好像接口對接完100%就麼問題了似的(直接把鍋給後端,簡直就是機智如我)常規的開發中這樣的:如下情況,也個頁面顯示一個app模塊和一個熱點新聞模塊,它們之間其實是沒有依賴關係的如果獲取appp的接口getApps出現掛了的情況,那getNews