十行程式碼實現高仿Promise

飢人谷前端發表於2018-01-03

本文為飢人谷講師若愚原創文章,首發於 前端學習指南

問題

假設我們有一個需求:1. 獲取使用者所在的城市;2. 根據城市獲取天氣;3. 根據天氣獲取出行建議。那我們的程式碼應該是這樣的

getCity(url1, function(){
  getWeather(url2, function(weather){
    getSuggestion(url3, function(suggestion){
      console.log(suggestion)
    })
  })
})
複製程式碼

這就是典型的非同步 callback 『回撥地獄』,程式碼層層巢狀可讀性很差。關於非同步的解決方式可參考這篇文章 Node.js非同步漫談

使用 Promise 是解決上述問題的一種方式,這裡我們不去講如何去使用內建的 Promise,而是帶大家手把手寫一個 Promise。

思路

我們希望有一個工具,能讓我們使用下面的的寫法來實現上述功能

promise.then(getCity)
    .then(getWeather)
    .then(getSuggestion)
複製程式碼

整理下思路:

  1. Tool 是一個物件
  2. Tool 有 then 這個方法
  3. 執行 then 方法返回的應該還是 Tool 物件
function Promise(){}
Promise.prototype.then = function(fn){
  //todo...
  return this
}
var promise = new Promise()
複製程式碼

那如何實現非同步操作序列執行呢?關鍵思路如下:

在 promise 物件內容維護一個陣列,當執行 promise.then(getCity) .then(getWeather) .then(getSuggestion) 時把這幾個函式依次放入陣列中。注意此時這些函式並沒有執行。

執行promise.resolve()時,會從陣列中拿出一個函式去執行。函式執行的過程中在非同步操作的結果到來後會再次自動呼叫 promise.resolve(),觸發下一個函式的取出並執行,下一個函式結果到來後再次自動呼叫promise.resolve() ......,這樣就實現了非同步鏈式執行。和原子彈爆炸原理類似。

所以需要對原來的非同步函式做一點小小的改動,在資料到來的地方,加一個promise.resolve,用於啟動後續函式的執行

function getCity(){
  var xhr = new XMLHttpRequest()
  xhr.open(url, 'get', true)
  xhr.onload = function(){
    if (this.status == 200) {
      promise.resolve(xhr.responseText)  //注意這裡的promise.resolve
    }
  }
  xhr.send()
}
複製程式碼

現在我們就能實現一個簡易的 Promise 了,這裡我們先暫不考慮特殊情況:

function Promise(){
  this.callbacks = []
}
Promise.prototype.then = function(fn){
  this.callbacks.push(fn)  //呼叫 then 時把函式放入陣列
  return this              //返回當前物件供鏈式呼叫
}
Promise.prototype.resolve = function(data){
  var fn = this.callbacks.shift()  //當呼叫resolve時拿出一個函式
  fn&&fn(data)                     //執行這個函式,並且把resolve的引數做引數
}


var promise = new Promise()

promise.then(getCity)
    .then(getWeather)
    .then(getSuggestion)

promise.resolve()  //啟動

function getCity(){
  setTimeout(function(){
    promise.resolve('杭州')
  }, 1000)
}
function getWeather(city){
  setTimeout(function(){
    promise.resolve(city + ' 晴天')
  }, 1000)
}
function getSuggestion(weather){
  setTimeout(function(){
    console.log(weather + ' 天氣不錯,可攜女友與狗出行')
  }, 1000)
}
複製程式碼

當然,如果覺得promise.resolve 單獨啟動一次看起來不舒服,也可以這樣執行

getCity()
  .then(getWeather)
  .then(getSuggestion)

function getCity(){
  setTimeout(function(){
    promise.resolve('杭州')
  }, 1000)
  return promise   //注意這裡
}
複製程式碼

實現

到此為止我們已經寫了一個簡單的 Promise,甚至能滿足很大一部分使用需求。但有個問題,每次非同步操作可能存在失敗的情況,而上面的程式碼並沒有非同步函式的失敗處理。下面考慮非同步的失敗處理,原理和上面類似,可以閱讀程式碼動手做個測試

class Promise {
    constructor (){
      this.callbacks = []
      this.oncatch = null
    }

    reject(result){
      this.complete('reject', result)
    }

    resolve(result){
      this.complete('resolve', result)
    }

    complete(type, result){
      if(type==='reject' && this.oncatch){
        this.callbacks = []
        this.oncatch(result)
      }else if(this.callbacks[0]) { 
        var handlerObj = this.callbacks.shift()
        if(handlerObj[type]){
          handlerObj[type](result)
        }
      }
    }

    then(onsuccess, onfail){
      this.callbacks.push({
        resolve: onsuccess,
        reject: onfail
      })
      return this
    }

    catch(onfail){
      this.oncatch = onfail
      return this
    }
  }

  var promise = new Promise()
  fn1().then(fn2, onfn1error)
       .then(fn3, onfn2error)
       .catch(onerror)

  function fn1(){
    setTimeout(function(){
      if(Math.random()>0.5){
        promise.resolve('杭州')
      }else{
        promise.reject('fn1 error')
      }
    })
    return promise
  }
複製程式碼

總結

現在我們已經手寫了一個 Promise, 當然和瀏覽器內建物件Promise原理有些差異, 但至少『達到』類似的目的了

加微訊號: astak10或者長按識別下方二維碼進入前端技術交流群 ,暗號:寫程式碼啦

每日一題,每週資源推薦,精彩部落格推薦,工作、筆試、面試經驗交流解答,免費直播課,群友輕分享... ,數不盡的福利免費送

十行程式碼實現高仿Promise

相關文章