Promise 已是 JavaScript 中異步處置懲罰的基石,回調的場景將會越來越少,而且如今可以直接在 Node.js 運用 async/await。async/await 基於 Promise,因而須要相識 Promise 來控制 async/await。這篇文章,將引見怎樣編寫一個 Promise 庫,並演示怎樣運用 async/await。
Promise 是什麼?
在 ES6 範例中,Promise 是一個類,它的組織函數接收一個 executor
函數。Promise 類的實例有一個 then()
要領。依據範例,Promise 另有其他的一些屬性,但在這裏可以臨時疏忽,因為我們要完成的是一個精簡版的庫。下面是一個 MyPromise
類的腳手架:
class MyPromise {
// `executor` 函數接收兩個參數,`resolve()` 和 `reject()`
// 擔任在異步操縱勝利(resolved)或許失利(rejected)的時刻挪用 `resolve()` 或許 `reject()`
constructor(executor) {}
// 當 promise 的狀況是 fulfilled(完成)時挪用 `onFulfilled` 要領,
// 當 promise 的狀況是 rejected(失利)時挪用 `onRejected` 要領
// 到目前為止,可以以為 'fulfilled' 和 'resolved' 是一樣的
then(onFulfilled, onRejected) {}
}
executor
函數須要兩個參數,resolve()
和 reject()
。promise 是一個狀況機,包括三個狀況:
- pending:初始狀況,既不是勝利,也不是失利狀況
- fulfilled:意味着操縱勝利完成,返回效果值
- rejected:意味着操縱失利,返回毛病信息
如許很輕易就可以完成 MyPromise
組織函數的初始版本:
constructor(executor) {
if (typeof executor !== 'function') {
throw new Error('Executor must be a function')
}
// 初始狀況,$state 示意 promise 的當前狀況
// $chained 是當 promise 處在 settled 狀況時須要挪用的函數數組
this.$state = 'PENDING'
this.$chained = []
// 為處置懲罰器函數完成 `resolve()` 和 `reject()`
const resolve = res => {
// 只需當 `resolve()` 或 `reject()` 被挪用
// 這個 promise 對象就不再處於 pending 狀況,被稱為 settled 狀況
// 挪用 `resolve()` 或 `reject()` 兩次,以及在 `resolve()` 以後挪用 `reject()` 是無效的
if (this.$state !== 'PENDING') {
return
}
// 背面將會談到 fulfilled 和 resolved 之間存在細微差別
this.$state = 'FULFILLED'
this.$internalValue = res
// If somebody called `.then()` while this promise was pending, need
// to call their `onFulfilled()` function
for (const { onFulfilled } of this.$chained) {
onFulfilled(res)
}
}
const reject = err => {
if (this.$state !== 'PENDING') {
return
}
this.$state = 'REJECTED'
this.$internalValue = err
for (const { onRejected } of this.$chained) {
onRejected(err)
}
}
// 如範例所言,挪用處置懲罰器函數中的 `resolve()` 和 `reject()`
try {
// 假如處置懲罰器函數拋出一個同步毛病,我們以為這是一個失利狀況
// 須要注重的是,`resolve()` 和 `reject()` 只能被挪用一次
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then()
函數的完成更簡樸,它接收兩個參數,onFulfilled()
和 onRejected()
。then()
函數必需確保 promise 在 fulfilled 時挪用 onFulfilled()
,在 rejected 時挪用 onRejected()
。假如 promise 已 resolved 或 rejected,then()
函數會馬上挪用 onFulfilled()
或 onRejected()
。假如 promise 仍處於 pending 狀況,就將函數推入 $chained
數組,因而後續 resolve()
和 reject()
函數依然可以挪用它們。
then(onFulfilled, onRejected) {
if (this.$state === 'FULFILLED') {
onFulfilled(this.$internalValue)
} else if (this.$state === 'REJECTED') {
onRejected(this.$internalValue)
} else {
this.$chained.push({ onFulfilled, onRejected })
}
}
*除此之外:ES6 範例示意,假如在已 resolved 或 rejected 的 promise 挪用 .then()
, 那末 onFulfilled()
或 onRejected()
將在下一個時序被挪用。因為本文代碼只是一個教授教養示例而不是範例的準確完成,因而完成會疏忽這些細節。
Promise 挪用鏈
上面的例子特地疏忽了 promise 中最龐雜也是最有效的部份:鏈式挪用。假如 onFulfilled()
或許 onRejected()
函數返回一個 promise,則 then()
應當返回一個 “locked in” 的新 promise 以婚配這個 promise 的狀況。比方:
p = new MyPromise(resolve => {
setTimeout(() => resolve('World'), 100)
})
p
.then(res => new MyPromise(resolve => resolve(`Hello, ${res}`)))
// 在 100 ms 后打印 'Hello, World'
.then(res => console.log(res))
下面是可以返回 promise 的 .then()
函數完成,如許就可以夠舉行鏈式挪用。
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
// 確保在 `onFulfilled()` 和 `onRejected()` 的毛病將致使返回的 promise 失利(reject)
const _onFulfilled = res => {
try {
// 假如 `onFulfilled()` 返回一個 promise, 確保 `resolve()` 能正確處置懲罰
resolve(onFulfilled(res))
} catch (err) {
reject(err)
}
}
const _onRejected = err => {
try {
reject(onRejected(err))
} catch (_err) {
reject(_err)
}
}
if (this.$state === 'FULFILLED') {
_onFulfilled(this.$internalValue)
} else if (this.$state === 'REJECTED') {
_onRejected(this.$internalValue)
} else {
this.$chained.push({ onFulfilled: _onFulfilled, onRejected: _onRejected })
}
})
}
如今 then()
返回一個 promise,然則還須要完成一些事情:假如 onFulfilled()
返回一個 promise,resolve()
要可以正確處置懲罰。所以 resolve()
函數須要在 then()
遞歸挪用,下面是更新后的 resolve()
函數:
const resolve = res => {
// 只需當 `resolve()` 或 `reject()` 被挪用
// 這個 promise 對象就不再處於 pending 狀況,被稱為 settled 狀況
// 挪用 `resolve()` 或 `reject()` 兩次,以及在 `resolve()` 以後挪用 `reject()` 是無效的
if (this.$state !== 'PENDING') {
return
}
// 假如 `res` 是 thenable(帶有then要領的對象)
// 將鎖定 promise 來堅持跟 thenable 的狀況一致
if (res !== null && typeof res.then === 'function') {
// 在這類情況下,這個 promise 是 resolved,然則仍處於 'PENDING' 狀況
// 這就是 ES6 範例中說的"一個 resolved 的 promise",能夠處在 pending, fulfilled 或許 rejected 狀況
// http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects
return res.then(resolve, reject)
}
this.$state = 'FULFILLED'
this.$internalValue = res
// If somebody called `.then()` while this promise was pending, need
// to call their `onFulfilled()` function
for (const { onFulfilled } of this.$chained) {
onFulfilled(res)
}
return res
}
為了簡樸起見,上面的例子省略了一旦 promise 被鎖定用以婚配另一個 promise 時,挪用 resolve() 或許 reject() 是無效的癥結細節。在上面的例子中,你可以 resolve() 一個 pending 的 promise ,然後拋出一個毛病,然後 res.then(resolve, reject) 將會無效。這僅僅是一個例子,而不是 ES6 promise 範例的完整完成。
上面的代碼說清楚明了 resolved 的 promise 和 fulfilled 的 promise 之間的區分。這類區分是玄妙的,而且與 promise 鏈式挪用有關。resolved 不是一種真正的 promise 狀況,但它是ES6範例中定義的術語。當對一個已 resolved 的 promise 挪用 resolve()
,能夠會發作以下兩件事之一:
- 在挪用
resolve(v)
時,假如v
不是一個 promise ,那末 promise 馬上成為 fulfilled。在這類簡樸的情況下,resolved 和 fulfilled 就是一樣的。 - 在挪用
resolve(v)
時,假如v
是另一個 promise,那末這個 promise 一向處於 pending 直到v
挪用 resolve 或許 reject。在這類情況下, promise 是 resolved 但處於 pending 狀況。
與 Async/Await 一同運用
癥結字 await
會停息實行一個 async
函數,直到守候的 promise 變成 settled 狀況。如今我們已有了一個簡樸的克己 promise 庫,看看連繫運用 async/await 中時會發作什麼。向 then()
函數增加一個 console.log()
語句:
then(onFulfilled, onRejected) {
console.log('Then', onFulfilled, onRejected, new Error().stack)
return new MyPromise((resolve, reject) => {
/* ... */
})
}
如今,我們來 await
一個 MyPromise
的實例,看看會發作什麼。
run().catch(error => console.error(error.stack))
async function run() {
const start = Date.now()
await new MyPromise(resolve => setTimeout(() => resolve(), 100))
console.log('Elapsed time', Date.now() - start)
}
注重上面的 .catch()
挪用。catch()
函數是 ES6 promise 範例的中心部份。本文不會細緻報告它,因為 .catch(f)
相當於 .then(null, f)
,沒有什麼迥殊的內容。
以下是輸出內容,注重 await 隱式挪用 .then()
中的 onFulfilled()
和 onRejected()
函數,這是 V8 底層的 C++ 代碼(native code)。另外,await
會一向守候挪用 .then()
直到下一個時序。
Then function () { [native code] } function () { [native code] } Error
at MyPromise.then (/home/val/test/promise.js:63:50)
at process._tickCallback (internal/process/next_tick.js:188:7)
at Function.Module.runMain (module.js:686:11)
at startup (bootstrap_node.js:187:16)
at bootstrap_node.js:608:3
Elapsed time 102
更多
async/await 是異常壯大的特徵,但控制起來輕微有點難題,因為須要運用者相識 promise 的基本原則。 promise 有許多細節,比方捕捉處置懲罰器函數中的同步毛病,以及 promise 一旦處理就沒法轉變狀況,這使得 async/await 成為能夠。一旦對 promise 有了充足的明白,async/await 就會變得輕易很多。