es6 快速入門 系列 —— promise

彭加李發表於2021-06-25

其他章節請看:

es6 快速入門 系列

Promise

Promise 是一種非同步程式設計的選擇

初步認識Promise

用 Promise 來實現這樣一個功能:傳送一個 ajax,返回後輸出 json 資料。請看示例:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        let json = {success: true, data:{}}
        resolve(json);
    }, 3000);
});
const resolveFn = value => {
    console.log(value)
};
const rejectFn = () => {}

promise1.then(resolveFn, rejectFn)

// { success: true, data: {} }

三秒後輸出 json 資料。

Promise 中文翻譯是承諾。首先用 Promise 建構函式建立一個承諾,承諾非同步操作在未來的某時刻完成,接著給承諾(promise1)繫結”已完成“狀態的回撥 resolveFn,以及”已拒絕“狀態的回撥 rejectFn。3秒後返回 json 資料,將承諾的狀態改為”已完成“(resolve(json)),對應的回撥函式(resolveFn)被呼叫執行。

每個 Promise 都會經歷一個短暫的生命週期,首先是進行中(pending)的狀態,一旦非同步操作執行結束,Promise則變成已處理的狀態。在前面示例中,執行 new Promise() 建立一個 Promise,是進行中的狀態,操作結束後,Promise 可能會進入到以下兩個狀態中的其中一個:

  • 已完成(Fulfilled) Promise非同步操作成功完成
    • 呼叫 resolve() 進入此狀態
  • 已拒絕(Rejected) Promise非同步操作未能成功完成
    • 呼叫 reject() 進入此狀態

建立 Promise

通過 new Promise(executor) 可以建立一個新的 Promise。新的 Promise 在沒有 resolve 之前,這個 Promise 的狀態是進行中(或未解決)。

executor(執行器)是一個雙參函式,引數為 resolve 和 reject。Promise 構造器將會在返回新物件之前執行 executor,並傳入 resolve 和 reject 函式。請看示例:

const promise1 = new Promise((resolve, reject) => {
    console.log(11)
})
console.log(22)
// 11 22

接著看這個示例:

const p1 = new Promise((resolve, reject) => {
    resolve()
    console.log(11)
})
p1.then(() => {
    console.log('then')
})
console.log(22)

// 11 22 then

呼叫 resolve() 後會觸發一個非同步操作,所以先執行同步(輸出 11 22),最後輸出 then。類似這段程式碼:

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('then')
    })
    console.log(11)
})

console.log(22)
// 11 22 then

then() 方法的兩個引數都是可選的,可以按照任意組合方式監聽 Promise。請看示例:

const promise1 = new Promise((resolve, reject) => {
    resolve(1)   // {1}
})

promise1.then(() => {
    console.log('完成')
})

promise1.then(() => {
    console.log('完成')
}, () => {
    console.log('拒絕')
})

promise1.then(null, () => {
    console.log('拒絕')
})

promise1.catch(() => {
    console.log('catch')
})

// 完成 完成

上面前 3 次 then() 呼叫操作的是同一個 Promise。第一個只監聽了完成,錯誤是不報告;第二個同時監聽了完成和拒絕;第三個只監聽了拒絕,成功時不報告;

如果改為reject(1)({1}),則輸出“拒絕 拒絕 catch”。

無論何時都可以新增新的已完成或已拒絕處理程式。請看示例:

const promise1 = new Promise((resolve, reject) => {
    resolve();
});

promise1.then(function(cnt){
    console.log(1)
    promise1.then(function(cnt){   // {1}
        console.log(2)
    })
    console.log(3)
    return 4
}).then(v => {
    console.log(v)
})
// 1 3 2 4

這段程式碼在完成處理程式中,向同一個 Promise 新增了另一個完成處理程式(行{1})。

Tip: then() 方法指定的回撥函式將在當前指令碼所有同步任務執行完才會處理。

建立已處理的 Promise

建立未處理的 Promise 最好方法是使用 Promise 建構函式,但如果想用 Promise 表示一個已知值,可以用Promise.resolve() 或者 Promise.reject()。

Promise.resolve

Promise.resolve(value),只接受一個引數並返回一個 Promise。

let p1 = Promise.resolve(11) // {1}
p1.then(v => {
    console.log(v)
})
console.log(22)

// 輸出:22 11

行{1}等價於let p1 = new Promise(resolve => resolve(11))

如果給 Promise.resolve() 方法傳入一個 Promise,那麼這個 Promise 會被直接返回。請看示例

let p1 = new Promise((resolve, reject) => {
    resolve(1)   // {1}
})
// resolve另一個promise
let p2 = Promise.resolve(p1) 
console.log(p1 == p2)
p2.then(v => {
    console.log('resolve')
},v => {
    console.log('reject')
})

// true resolve

如果將行{1}改為reject(1),輸出“true reject”。

利用此特性,可以將不是 Promise 的值轉為 Promise。

Promise.resolve() 方法允許呼叫時不帶引數,直接返回一個resolved 狀態的 Promise 物件。就像這樣:

let p1 = Promise.resolve() 
p1.then(v => {
    console.log(v)
})
console.log(22)
// 輸出:22 undefined

非 Promise 的 Thenable 物件

Promise.resolve() 可以接受非 Promise 的 Thenable 物件作為引數,返回的 Promise 將採用 Thenable 物件的最終狀態。請看示例:

// Thenable 物件指:擁有 then() 方法並且接受 resolve 和 reject 兩個引數的普通物件
let thenable = {
    then: function(resolve, reject){
        reject(1)
    }
}
let p1 = Promise.resolve(thenable)

p1.then(v => {
    console.log('resolve')
}).catch(v => {
    console.log('reject')
})

// reject

這段程式碼,雖然呼叫的是 Promise.resolve(),但 thenable 的狀態是已拒絕(reject(1)),所以最後輸出 reject。

Promise.reject

Promise.reject() 方法返回一個帶有拒絕原因的 Promise 物件。請看示例:

let p1 = Promise.reject(11)
p1.catch(v => {
    console.log(v)
})
console.log(22)

// 輸出:22 11

Promise.reject() 用法比 Promise.resolve() 簡單很多。

比如給 Promise.reject() 方法傳入一個 Promise,效果與 Promise.resolve() 不相同。請看示例:

let p1 = new Promise((resolve, reject) => {
    reject(1)
})
let p2 = Promise.reject(p1) 
console.log(p1 == p2) // false

再比如給 Promise.reject() 方法傳入一個 thenable,效果與 Promise.resolve() 也不相同。請看示例:

let thenable = {
    then: function(resolve, reject){
        resolve(1)
    }
}
let p1 = Promise.reject(thenable)

p1.then(v => {
    console.log('resolve')
}).catch(v => {
    console.log('reject')
})

// reject

執行器錯誤

如果執行器內部丟擲錯誤,則 Promise 的拒絕處理程式就會被呼叫,例如:

let p1 = new Promise(function(resolve, reject){
    throw new Error('fail')
})

p1.catch(v => {
    console.log(v.message) // fail
})

這段程式碼,執行器故意丟擲一個錯誤,每個執行器中都隱含一個 try-catch 塊,所以錯誤會被捕獲並傳入給已拒絕回撥。此例等價於:

let p1 = new Promise(function(resolve, reject){
    try{
        throw new Error('fail')
    }catch(e){
        reject(e)
    }
})

...

串聯 Promise

將 Promise 串聯起來能實現更復雜的非同步特徵:

let p1 = new Promise((resolve, reject) => {
    resolve('10')
})

p1.then(v => {
    console.log(v)
}).then(() => {
    console.log('finished')
})

每次呼叫 then() 方法或 catch() 方法時,實際上會建立並返回另一個 Promise,只有當第一個 Promise 完成或拒絕後,第二個才會被解決,依此類推。

將這個示例拆開,看起來像這樣:

let p1 = new Promise((resolve, reject) => {
    resolve('10')
})

let p2 = p1.then(v => {
    console.log(v)
})

p2.then(() => {
    console.log('finished')
})

捕獲錯誤

在完成或拒絕處理程式中可能發生錯誤,而 Promise 鏈可以捕獲這些錯誤。請看示例:

let p1 = new Promise((resolve, reject) => {
    resolve('10')
})

p1.then(() => {
    throw new Error('fail')
}).catch((e) => {
    console.log(e.message)
})

// 輸出:fail

這段程式碼在完成處理程式中丟擲一個錯誤。如果在拒絕處理程式中丟擲錯誤,也可以通過相同的方式接收:

let p1 = new Promise((resolve, reject) => {
    reject('10')
})

p1.catch(() => {
    throw new Error('fail')
}).catch((e) => {
    console.log(e.message)
})

// 輸出:fail

儘量在 Promise 鏈的末尾留一個拒絕處理程式,以保證能正確處理所有可能發生的錯誤。請看示例:

如果沒有拒絕處理程式,程式碼可能會這樣:

let p1 = new Promise((resolve, reject) => {
    resolve('10')
})

p1.then(() => {
    console.log(1)   // {1}
}).then(() => {
    console.log(2)   // {2}
}).then(() => {
    console.log(3)   // {3}
})

其中三個完成處理程式都有可能出錯,我們可以在末尾新增一個已拒絕處理的程式對這個鏈式統一處理,就像這樣:

let p1 = new Promise((resolve, reject) => {
    resolve('10')
})

p1.then(() => {
    throw new Error('fail')
    console.log(1)
}).then(() => {
    console.log(2)
}).then(() => {
    console.log(3)
}).catch(e => {
    console.log(e.message)
})

// 輸出:fail

這段程式碼是第一個完成處理程式報錯,由於只有末尾才有已拒絕的處理,所以只輸出 fail。

傳遞資料

Promise 鏈的另一個重要特性是可以給下游的 Promise 傳遞資料。請看示例:

let p1 = new Promise((resolve, reject) => {
    resolve(1)
})

p1.then(v => {
    console.log(v)
    return v + 1
}).then(v => {
    console.log(v)
})
// 輸出:1 2

在拒絕處理程式中也可以做相同的事:

let p1 = new Promise((resolve, reject) => {
    reject(1)
})

p1.catch(v => {
    console.log(v)
    return v + 1
}).then(v => {
    console.log(v)
})
// 輸出:1 2

拒絕處理中返回值仍然可以在下一個Promise的完成處理程式中使用,必要時,即使其中一個Promise失敗,也能恢復整條鏈的執行。

在 Promise 鏈中返回 Promise

前面我們通過返回值給下游 Promise 傳遞資料,如果返回值是 Promise 物件,則會通過一個額外的步驟來確定下一步該怎麼走。請看示例:

let p1 = new Promise((resolve, reject) => {
    reject(1)
})

let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(10) // {1}
    }, 3000)
})

p1.catch(v => {
    console.log('等待3秒')
    return p2
}).then(v => {
    console.log(`resolve: ${v}`)
}, v => {
    console.log(`reject: ${v}`)
})

/*
等待3秒
// 等待3秒後輸出
resolve: 10
*/

這段程式碼,在 Promise 鏈中返回一個 Promise(p2),由於 p2 的狀態是已完成({1}),所以下一步則進入已完成處理程式。

響應多個Promise

es6 提供了 Promise.all() 和 Promise.race() 兩個方法來監聽多個 Promise。

Promise.all()

Promise.all 只接收一個引數並返回一個Promise,該引數是含有多個受監視Promise的可迭代物件(例如陣列),只有當所有 Promise 都被解決,返回的 Promise 才會被解決。請看示例:

let p1 = new Promise((resolve, reject) => {
    resolve(1)
})

let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(2)
    }, 3000)
    
})

let p3 = Promise.all([p1, p2])

p3.then(value => {
    console.log(Array.isArray(value))   // {1}
    console.log(value)
}).catch(v => {
    console.log(Array.isArray(v))
    console.log(v)
})

// true
// [1, 2]

這段程式碼,Promise.all 監聽了兩個 Promise,其中一個需要過3秒才被置為已解決,當兩個 Promise 都被解決,才會輸出結果。其中 value({1})是陣列。

如果被 Promise.all 監聽的其中一個被拒絕,那麼不用等所有 Promise 都完成就會立即被拒絕。在上面示例的基礎上,將 resolve(1) 改為 reject(1),立即輸出false 1,無需等待另一個 Promise 解決。拒絕處理程式總是接受一個值而非陣列。

Promise.race()

Promise.race() 與 Promise.all() 類似,不同之處是隻要有一個被解決,返回的 Promise 就被解決。請看示例:

let p1 = new Promise((resolve, reject) => {
    resolve(1)
})

let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(2)
    }, 3000)
    
})

let p3 = Promise.race([p1, p2])
console.log(p3 === p1)
p3.then(v => {
    console.log(Array.isArray(v))
    console.log(`resolve, ${v}`)
}).catch(v => {
    console.log(Array.isArray(v))
    console.log(`reject, ${v}`)
})

/*
false
false
resolve, 1
*/

無需等待 p2 被解決,立刻輸出。實際上,傳給 Promise.race() 方法的 Promise 會進行競選,以決定哪一個先被解決,如果先解決的是已完成 Promise,則返回已完成的 Promise,如果先解決的是已拒絕的 Promise,則返回已拒絕的Promise。請看示例:

let p1 = new Promise((resolve, reject) => {
    reject(1)
})

let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('p2 resolve')
        resolve(2)
    }, 3000)
})

let p3 = Promise.race([p1, p2])

p3.then(v => {
    console.log(Array.isArray(v))
    console.log(`resolve, ${v}`)
}).catch(v => {
    console.log(Array.isArray(v))
    console.log(`reject, ${v}`)
})
/*
false
reject, 1
p2 resolve
*/

p2 雖然被忽略,但仍會執行。

其他章節請看:

es6 快速入門 系列

相關文章