js 真的是一步一步手寫promise

Ace7523發表於2019-02-18

js 真的是一步一步手寫promise
前端非同步一直是老生常談,promise更是繞不過去的話題,那麼除了會使用promise之外,能否嘗試自己封裝一個promise呢?網上關於這個的封裝實現有很多,不過還是想一步一步的封裝一下,就好比是玩遊戲升級,直接給我一個滿級號,不如自己升級樂趣大。

目錄

  • 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')
複製程式碼

執行結果如下:

js 真的是一步一步手寫promise

針對這個例子做以下幾點說明,也是需要直接記住的,因為這就好比是解答數學題的公式一樣,開始一定要記牢。

  1. Promise是建構函式,new 出來的例項有then方法。
  2. new Promise時,傳遞一個引數,這個引數是函式,又被稱為執行器函式(executor), 並執行器會被立即呼叫,也就是上面結果中start最先輸出的原因。
  3. executor是函式,它接受兩個引數 resolve reject ,同時這兩個引數也是函式。
  4. new Promise後的例項具有狀態, 預設狀態是等待,當執行器呼叫resolve後, 例項狀態為成功狀態, 當執行器呼叫reject後,例項狀態為失敗狀態。
  5. promise翻譯過來是承諾的意思,例項的狀態一經改變,不能再次修改,不能成功再變失敗,或者反過來也不行。
  6. 每一個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')
複製程式碼

執行結果如下:

js 真的是一步一步手寫promise

小結:結果看似對了,不過和原生的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')
複製程式碼

執行結果如下

js 真的是一步一步手寫promise

例項多次呼叫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')
複製程式碼

執行結果如下

js 真的是一步一步手寫promise

那麼針對這種非同步的情況和例項p多次呼叫then方法,我們上述MyPromise該如何修改呢?

  1. 對於非同步情況,我們先來看上面的例子,當程式碼執行到了p.then() 的時候,執行器方法中的resolve('data1')被setTimeout放到了非同步任務佇列中,

  2. 換句話說,也就是,此時例項p的狀態還是預設狀態,沒有改變,那麼我們此時並不知道要去執行then中的第一個引數(成功回撥)還是第二個引數(失敗回撥)。

  3. 在不知道哪個回撥會被執行的情況下,就需要先把這兩個回撥函式儲存起來,等到時機成熟,確定呼叫哪個函式的時候,再拿出來呼叫。

  4. 其實就是釋出訂閱的一個變種,我們在執行一次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

js 真的是一步一步手寫promise

小結: 下面這裡,為什麼能拿到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)
  })
})
複製程式碼

執行結果如下

js 真的是一步一步手寫promise

很顯然,上面的回撥模式不是我們想要的,那麼我們如何把上面寫法給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) }
)
複製程式碼

執行一下這一小段程式碼,結果如下

js 真的是一步一步手寫promise

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說你還不如不用我。執行結果如下:

js 真的是一步一步手寫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) }
)
複製程式碼

執行結果如下

js 真的是一步一步手寫promise

以上就是一個簡單非同步場景的promise化。

2 回顧鏈式呼叫的常見場景

其實關於鏈式呼叫,我們也有一些類似於公式規則一樣的東西需要去記住,這是個規範,來自promise A+,傳送門在此 promisesaplus.com/,

我在這裡就先不羅列promise A+ 的翻譯了,先挑出幾個乾貨來,也是我們平時使用promise習以為常的東西。

  1. jquery 鏈式呼叫 是因為jquery返回了this,promise能一直then下去,是因為promise的then方法返回了promise
  2. 返回的是新的promise,因為上面說過,promise例項狀態一旦修改,不能再次修改,所以要返回全新的promise。
  3. then方法中的兩個引數,也就是那所謂的成功回撥和失敗回撥,他們的返回值如何處理?
  4. 以成功回撥函式(then中的第一個引數)為例,這個函式返回普通值,也就是常量或者物件,這個值會傳遞到下一個then中,作為成功的結果。 如果這個函式返回的不是普通值,那麼有兩種情況。
  5. 非普通值---promise:會根據返回的promise成功還是失敗,決定呼叫下一個then的第一個引數還是第二個引數。
  6. 非普通值---如報錯異常:會跑到下一個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種情況對應 執行結果如下:

js 真的是一步一步手寫promise

驗證第一個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的結果

js 真的是一步一步手寫promise

驗證第一個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) }
)
複製程式碼

執行結果如下

js 真的是一步一步手寫promise

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。這裡其實是有些繞的。

為了更好的理解上面說的,我再來個圖,回顧下之前的例子

js 真的是一步一步手寫promise

輸出的是什麼呢? 大家都知道 會先輸出 success Ace 後輸出 success undefined

js 真的是一步一步手寫promise

所以,上面圖中,第一個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的結果如下

js 真的是一步一步手寫promise

js 真的是一步一步手寫promise

js 真的是一步一步手寫promise

js 真的是一步一步手寫promise
js 真的是一步一步手寫promise

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
}複製程式碼

相關文章