「長文干貨」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 的狀態改變
- pending 變為 resolved
- 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 內部立即同步回調,異步操作在執行器中執行
- excutor 函數:執行器 (resolve, reject) => {}
- resolve 函數:內部定義成功時我們調用的函數 value => {}
- reject 函數:內部定義失敗時我們調用的函數 reason => {}
2)Promise.prototype.then 方法:(onResolved, onRejected) => {},指定用於得到成功 value 的成功回調和用於得到失敗 reason 的失敗回調返回一個新的 promise 對象
- onResolved 函數:成功的回調函數 (value) => {}
- 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/await 和 promises 一樣,都是非堵塞式的,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 對象, 但也可以是其它的值
- 如果表達式是 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
6.3 Async/await 比 Promise 更優越的表現
簡潔干凈,使用 async/await 能省去寫多少行代碼
錯誤處理,async/wait 能用相同的結構和好用的經典 try/catch 處理同步和異步錯誤,錯誤堆棧能指出包含錯誤的函數。
調試,async/await 的一個極大優勢是它更容易調試,使用 async/await 則無需過多箭頭函數,並且能像正常的同步調用一樣直接跨過 await 調用。
全文完