es6 快速入門 系列 —— promise

其他章節請看:

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 快速入門 系列