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')
複製代碼

運行結果以下:node

針對這個例子作如下幾點說明,也是須要直接記住的,由於這就比如是解答數學題的公式同樣,開始必定要記牢。jquery

  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構造函數吧。promise

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時候才能夠變狀態ui

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中第一個參數函數中this

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該如何修改呢?

  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

小結: 下面這裏,爲何能拿到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習覺得常的東西。

  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種狀況對應 運行結果以下:

驗證第一個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
}複製代碼
相關文章
相關標籤/搜索