目錄
- 1 從簡單使用著手,實現MyPromise大體框架
- 2 完善MyPromise,新增非同步處理和實現一個例項多次呼叫then方法
- 3 繼續完善,實現MyPromise的鏈式呼叫
1 從簡單使用著手,實現MyPromise大體框架
先來看一下promise使用的一個小例子:
let p = new Promise(function (resolve, reject) {
console.log('start')
resolve('data1')
})
p.then(
(v) => {
console.log('success: ' + v)
},
(v) => {
console.log('error: ' + v)
}
)
console.log('end')
複製程式碼
執行結果如下:
針對這個例子做以下幾點說明,也是需要直接記住的,因為這就好比是解答數學題的公式一樣,開始一定要記牢。
- Promise是建構函式,new 出來的例項有then方法。
- new Promise時,傳遞一個引數,這個引數是函式,又被稱為執行器函式(executor), 並執行器會被立即呼叫,也就是上面結果中start最先輸出的原因。
- executor是函式,它接受兩個引數 resolve reject ,同時這兩個引數也是函式。
- new Promise後的例項具有狀態, 預設狀態是等待,當執行器呼叫resolve後, 例項狀態為成功狀態, 當執行器呼叫reject後,例項狀態為失敗狀態。
- promise翻譯過來是承諾的意思,例項的狀態一經改變,不能再次修改,不能成功再變失敗,或者反過來也不行。
- 每一個promise例項都有方法 then ,then中有兩個引數 ,我習慣把第一個引數叫做then的成功回撥,把第二個引數叫做then的失敗回撥,這兩個引數也都是函式,當執行器呼叫resolve後,then中第一個引數函式會執行。當執行器呼叫reject後,then中第二個引數函式會執行。
那麼就目前的這些功能,或者說是規則,來著手寫一下MyPromise建構函式吧。
1 建構函式的引數,在new 的過程中會立即執行
// 因為會立即執行這個執行器函式
function MyPromise(executor){
executor(resolve, reject)
}
複製程式碼
2 new出來的例項具有then方法
MyPromise.prototype.then = function(onFulfilled, onRejected){
}
複製程式碼
3 new出來的例項具有預設狀態,執行器執行resolve或者reject,修改狀態
function MyPromise(executor){
let self = this
self.status = 'pending' // 預設promise狀態是pending
function resolve(value){
self.status = 'resolved' // 成功狀態
}
function reject(reason){
self.status = 'rejected' //失敗狀態
}
executor(resolve, reject)
}
複製程式碼
4 當執行器呼叫resolve後,then中第一個引數函式(成功回撥)會執行,當執行器呼叫reject後,then中第二個引數函式(失敗回撥)會執行
MyPromise.prototype.then = function(onFulfilled, onRejected){
let self = this
if(self.status === 'resolved'){
onFulfilled()
}
if(self.status === 'rejected'){
onRejected()
}
}
複製程式碼
5 保證promise例項狀態一旦變更不能再次改變,只有在pending時候才可以變狀態
function Promise(executor){
let self = this
self.status = 'pending' // 預設promise狀態是pending
function resolve(value){
if(self.status === 'pending'){ //保證狀態一旦變更,不能再次修改
self.value = value
self.status = 'resolved' // 成功狀態
}
}
function reject(reason){
if(self.status === 'pending'){
self.reason = reason
self.status = 'rejected' //失敗狀態
}
}
executor(resolve, reject)
}
複製程式碼
6 執行器執行resolve方法傳的值,傳遞給then中第一個引數函式中
function MyPromise(executor){
let self = this
self.value = undefined
self.reason = undefined
self.status = 'pending' // 預設promise狀態是pending
function resolve(value){
if(self.status === 'pending'){ //保證狀態一旦變更,不能再次修改
self.value = value
self.status = 'resolved' // 成功狀態
}
}
function reject(reason){
if(self.status === 'pending'){
self.reason = reason
self.status = 'rejected' //失敗狀態
}
}
executor(resolve, reject) // 因為會立即執行這個執行器函式
}
MyPromise.prototype.then = function(onFulfilled, onRejected){
let self = this
if(self.status === 'resolved'){
onFulfilled(self.value)
}
if(self.status === 'rejected'){
onRejected(self.reason)
}
}
複製程式碼
嘗試使用一下這個 MyPromise :
let p = new MyPromise(function (resolve, reject) {
console.log('start')
resolve('data2')
})
p.then(
(v) => {
console.log('success ' + v)
},
(v) => {
console.log('error ' + v)
}
)
console.log('end')
複製程式碼
執行結果如下:
小結:結果看似對了,不過和原生的promise還是有不同的,就是success那條語句的列印順序,不要急,MyPromise 還沒有寫完。
2 完善MyPromise,新增非同步處理和實現一個例項多次呼叫then方法
還是看原生promise的使用小例子
let p = new Promise(function (resolve, reject) {
console.log('start')
setTimeout(function(){
resolve('data1')
},2000)
})
p.then(
(v) => {
console.log('success: ' + v)
},
(v) => {
console.log('error: ' + v)
}
)
console.log('end')
複製程式碼
執行結果如下
例項多次呼叫then方法情況(注意不是鏈式呼叫)
let p = new Promise(function (resolve, reject) {
console.log('start')
setTimeout(function(){
resolve('data1')
},2000)
})
p.then(
(v) => {
console.log('success: ' + v)
},
(v) => {
console.log('error: ' + v)
}
)
p.then(
(v) => {
console.log('success: ' + v)
},
(v) => {
console.log('error: ' + v)
}
)
console.log('end')
複製程式碼
執行結果如下
那麼針對這種非同步的情況和例項p多次呼叫then方法,我們上述MyPromise該如何修改呢?
-
對於非同步情況,我們先來看上面的例子,當程式碼執行到了p.then() 的時候,執行器方法中的resolve('data1')被setTimeout放到了非同步任務佇列中,
-
換句話說,也就是,此時例項p的狀態還是預設狀態,沒有改變,那麼我們此時並不知道要去執行then中的第一個引數(成功回撥)還是第二個引數(失敗回撥)。
-
在不知道哪個回撥會被執行的情況下,就需要先把這兩個回撥函式儲存起來,等到時機成熟,確定呼叫哪個函式的時候,再拿出來呼叫。
-
其實就是釋出訂閱的一個變種,我們在執行一次p.then(),就會then中的引數,也就是把成功回撥和失敗回撥都儲存起來(訂閱),執行器執行了resolve方法或者reject方法時,我們去執行剛儲存起來的函式(釋出)。
此階段MyPromise升級程式碼如下
//省略其餘等待,突出增加的點,等下發完整版
function MyPromise(executor){
...
// 用來儲存then 方法中,第一個引數
self.onResolvedCallbacks = []
// 用來儲存then 方法中,第二個引數
self.onRejectedCallbacks = []
...
}
MyPromise.prototype.then = function(onFulfilled, onRejected){
...
if(self.status === 'pending'){
// 訂閱
self.onResolvedCallbacks.push(function(){
onFulfilled(self.value)
})
self.onRejectedCallbacks.push(function(){
onRejected(self.reason)
})
}
...
}
複製程式碼
小結 這樣修改後,我們執行器方法中,有非同步函式的情況時,p.then執行就會把對應的兩個引數儲存起來了。那麼在什麼時候呼叫呢?答,肯定是在執行器中的resolve執行時候或者reject執行時候。
接下來貼出這階段改動的完整程式碼。
function MyPromise(executor){
let self = this
self.value = undefined
self.reason = undefined
// 預設promise狀態是pending
self.status = 'pending'
// 用來儲存then 方法中,第一個引數
self.onResolvedCallbacks = []
// 用來儲存then 方法中,第二個引數
self.onRejectedCallbacks = []
function resolve(value){
if(self.status === 'pending'){ //保證狀態一旦變更,不能再次修改
self.value = value
self.status = 'resolved' // 成功狀態
self.onResolvedCallbacks.forEach(fn => {
fn()
})
}
}
function reject(reason){
if(self.status === 'pending'){
self.reason = reason
self.status = 'rejected' //失敗狀態
self.onRejectedCallbacks.forEach(fn => {
fn()
})
}
}
executor(resolve, reject) // 因為會立即執行這個執行器函式
}
MyPromise.prototype.then = function(onFulfilled, onRejected){
let self = this
if(self.status === 'resolved'){
onFulfilled(self.value)
}
if(self.status === 'rejected'){
onRejected(self.reason)
}
if(self.status === 'pending'){
// 訂閱
self.onResolvedCallbacks.push(function(){
onFulfilled(self.value)
})
self.onRejectedCallbacks.push(function(){
onRejected(self.reason)
})
}
}
複製程式碼
我們來測試一下這個升級版的MyPrimise吧
let p = new MyPromise(function (resolve, reject) {
console.log('start')
setTimeout(function(){
resolve('data1')
},2000)
})
p.then(
(v) => {
console.log('success: ' + v)
},
(v) => {
console.log('error: ' + v)
}
)
p.then(
(v) => {
console.log('success: ' + v)
},
(v) => {
console.log('error: ' + v)
}
)
console.log('end')
複製程式碼
執行結果如下,顯示列印start和end,兩秒後一起列印的兩個 success:data1
小結: 下面這裡,為什麼能拿到self.value的值,值得好好思考一下呦
self.onResolvedCallbacks.push(function(){
onFulfilled(self.value)
})
複製程式碼
3 繼續完善,實現MyPromise的鏈式呼叫
溫馨提示,對於鏈式呼叫,這是手寫promise中最為複雜的一個階段,在理解下面的操作之前,希望可以對上面的內容再看一下,否則很有可能造成混亂~
1 實際場景的promise化
有如下場景,第一次讀取的是檔名字,拿到檔名字後,再去讀這個名字檔案的內容。很顯然這是兩次非同步操作,並且第二次的非同步操作依賴第一次的非同步操作結果。
// 簡要說明 建立一個js檔案 與這個檔案同級的 name.txt, text.txt
// 其中name.txt內容是text.txt, 而text.txt的內容是 文字1
// node 執行這個js檔案
let fs = require('fs')
fs.readFile('./name.txt', 'utf8', function (err, data) {
console.log(data)
fs.readFile(data, 'utf8', function (err, data) {
console.log(data)
})
})
複製程式碼
執行結果如下
很顯然,上面的回撥模式不是我們想要的,那麼我們如何把上面寫法給promise化呢?為了表述的更清晰一下,我還是分步來寫:
1 封裝一個函式,函式返回promise例項
function readFile(url){
return new Promise((resolve, reject)=>{
})
}
複製程式碼
2 這個函式執行就會返回promise例項,也就是有then方法可以使用
readFile('./name.txt').then(
() => {},
() => {}
)
複製程式碼
3 完善執行器函式,並且記住執行器函式是同步執行的,即new時候,執行器就執行了
let fs = require('fs')
function readFile(url){
return new Promise((resolve, reject)=>{
fs.readFile(url, 'utf8', function (err, data) {
if(err) reject(err)
resolve(data)
})
})
}
readFile('./name.txt').then(
(data) => { console.log(data) },
(err) => { console.log(err) }
)
複製程式碼
執行一下這一小段程式碼,結果如下
4 不使用鏈式呼叫
readFile('./name.txt').then(
(data) => {
console.log(data)
readFile(data).then(
(data) => {console.log(data)},
(err) => {console.log(err)}
)
},
(err) => {console.log(err)}
)
複製程式碼
在回撥里加回撥,promise說你還不如不用我。執行結果如下:
5 使用鏈式呼叫
readFile('./name.txt')
.then(
(data) => {
console.log(data)
return readFile(data)
},
(err) => {console.log(err)}
)
.then(
(data) => { console.log(data) },
(err) => { console.log(err) }
)
複製程式碼
執行結果如下
以上就是一個簡單非同步場景的promise化。
2 回顧鏈式呼叫的常見場景
其實關於鏈式呼叫,我們也有一些類似於公式規則一樣的東西需要去記住,這是個規範,來自promise A+,傳送門在此 promisesaplus.com/,
我在這裡就先不羅列promise A+ 的翻譯了,先挑出幾個乾貨來,也是我們平時使用promise習以為常的東西。
- jquery 鏈式呼叫 是因為jquery返回了this,promise能一直then下去,是因為promise的then方法返回了promise
- 返回的是新的promise,因為上面說過,promise例項狀態一旦修改,不能再次修改,所以要返回全新的promise。
- then方法中的兩個引數,也就是那所謂的成功回撥和失敗回撥,他們的返回值如何處理?
- 以成功回撥函式(then中的第一個引數)為例,這個函式返回普通值,也就是常量或者物件,這個值會傳遞到下一個then中,作為成功的結果。 如果這個函式返回的不是普通值,那麼有兩種情況。
- 非普通值---promise:會根據返回的promise成功還是失敗,決定呼叫下一個then的第一個引數還是第二個引數。
- 非普通值---如報錯異常:會跑到下一個then中的失敗引數中,也就是then中的第二個引數。
我們先用原生promise來驗證一下這些情況,然後再把這些實現新增到MyPromise中。
驗證then中第一個回撥返回普通值情況,拿上面例子加以修改
readFile('./name.txt')
.then(
(data) => {
console.log(data)
return {'a': 100} // 1 返回引用型別
// return 100 // 2 返回基本型別
// return undefined 3 返回undefined
// 4 不寫return
},
(err) => {console.log(err)}
)
.then(
(data) => { console.log(data) },
(err) => { console.log(err) }
)
複製程式碼
上面4種情況對應 執行結果如下:
驗證第一個then中,返回promise情況,鏈式的第二個then怎麼回應
readFile('./name.txt')
.then(
(data) => {
console.log(data)
return new Promise(function(resolve, reject){
setTimeout(function(){
// resolve('ok')
reject('error')
},1000)
})
},
(err) => {console.log(err)}
)
.then(
(data) => { console.log(data) },
(err) => { console.log(err) }
)
複製程式碼
執行結果如下,分別是上面執行resolve和reject的結果
驗證第一個then中,返回錯誤情況,鏈式的第二個then怎麼相應
readFile('./name.txt')
.then(
(data) => {
console.log(data)
throw TypeError()
},
(err) => {console.log(err)}
)
.then(
(data) => { console.log(data) },
(err) => { console.log(err) }
)
複製程式碼
執行結果如下
3 基於上述完善MyPromise的鏈式呼叫
1 then返回的是全新的promise
MyPromise.prototype.then = function(onFulfilled, onRejected){
let self = this
return new MyPromise(function(resolve, reject){
if(self.status === 'resolved'){...}
if(self.status === 'rejected'){...}
if(self.status === 'pending'){...}
}
}
複製程式碼
小結:可以向上翻一下,對比上一版的MyPromise.prototype.then實現,其實只是原本的邏輯,用MyPromise的執行器函式包裹了一下,而我們又知道,執行器函式是同步執行,在new 例項的時候執行器就會執行,所以就目前來看,加上這個包裹,對原有邏輯不存在什麼影響,又實現了只要then方法執行,返回的就是promise例項,並且是全新的promise例項。
2 對於then中函式返回值的處理 普通值情況
MyPromise.prototype.then = function(onFulfilled, onRejected){
...
if(self.status === 'resolved'){
try{
let x = onFulfilled(self.value)
resolve(x)
}catch(e){
reject(e)
}
}
...
}
複製程式碼
小結 上面程式碼我只寫了 self.status === 'resolved' 這個狀態的,其餘兩個狀態也是一樣的寫法,我就先拿這一個舉例說明。onFulfilled,就是我們的promise例項,執行then方法傳的第一個引數,他執行後返回普通值的話,會直接把這個值傳遞給鏈式呼叫的下一個then的成功回撥函式中。(這個表述大家應該可以看懂吧)。
好,我們來想一下,通過第一步,已經實現了then方法返回全新的promise,那麼,這個全新的promise再去執行then的話,這個then的成功回撥和失敗回撥的引數,也就是這個then的第一個引數需要的value和第二個引數需要的reason,哪裡來?
肯定是在這個全新的promise例項的,new 過程中,那個處理器函式中的,resolve或者reject。這裡其實是有些繞的。
為了更好的理解上面說的,我再來個圖,回顧下之前的例子
輸出的是什麼呢? 大家都知道 會先輸出 success Ace 後輸出 success undefined
所以,上面圖中,第一個then返回了新的promise不假,但是沒有執行resolve和reject,這種情況就相當於 resolve(undefined) , 所以第二個then,列印的是 success undefined
所以這一小節中的,let x = onFulfilled(self.value) 這裡的原由,我囉嗦的挺多了吧~當然,這只是處理普通值的情況。附上這階段的完整程式碼。
MyPromise.prototype.then = function(onFulfilled, onRejected){
let self = this
let promise2 = new MyPromise(function(resolve, reject){
// then 函式的成功回撥函式的執行結果 與 promise2的關係
if(self.status === 'resolved'){
try{
let x = onFulfilled(self.value)
resolve(x) // 這是 x 是常量的時候,但x可能是一個新的promise,
}catch(e){
reject(e)
}
}
if(self.status === 'rejected'){
try{
let x = onRejected(self.reason)
resolve(x)
}catch(e){
reject(e)
}
}
if(self.status === 'pending'){
self.onResolvedCallbacks.push(function(){
try{
let x = onFulfilled(self.value)
resolve(x)
}catch(e){
reject(e)
}
})
self.onRejectedCallbacks.push(function(){
try{
let x = onRejected(self.reason)
resolve(x)
}catch(e){
reject(e)
}
})
}
})
return promise2
}
複製程式碼
測試上面程式碼示例如下
let p = new MyPromise(function (resolve, reject) {
console.log('start')
setTimeout(function(){
resolve('data1')
},500)
})
p.then(
(v) => {
console.log('success: ' + v)
// return v // 1 返回 v
// return 100 // 2 返回常量
// return {a : 100} // 3 返回物件
// return undefined // 4 返回 undefined
// 5 不寫return
},
(v) => {
console.log('error: ' + v)
}
)
.then(
(v) => {
console.log('success: ' + v)
},
(v) => {
console.log('error: ' + v)
}
)
console.log('end')
複製程式碼
對應上面1--5的結果如下
3 對於then中函式返回值的處理 非普通值情況
也就是說對於上面例子,出現了第六種情況,既,then的第一個回撥函式,返回了一個新的promise例項
p.then(
(v) => {
console.log('success: ' + v)
return new MyPromise(excutor)
},
(v) => {
console.log('error: ' + v)
}
)
複製程式碼
then的第一個回撥函式,對應MyPromise的是onFulfilled,所以我們要對MyPromise.prototype.then 再次改造
MyPromise.prototype.then = function(onFulfilled, onRejected){
let self = this
let promise2 = new MyPromise(function(resolve, reject){
// then 函式的成功回撥函式的執行結果 與 promise2的關係
if(self.status === 'resolved'){
try{
let x = onFulfilled(self.value)
// x可能是一個新的promise , 抽離一個函式來處理x的情況
resolvePromise(promise2, x, resolve, reject)
}catch(e){
reject(e)
}
}
if(self.status === 'rejected'){...}
if(self.status === 'pending'){...}
})
return promise2
}複製程式碼