完全理解Promise對象——用es5語法實現一個本身的Promise(上篇)

本文同步自個人我的博客: http://mly-zju.github.io/javascript

衆所周知javascript語言的一大特點就是異步,這既是它的優勢,同時在某些狀況下也帶來了一些的問題。最大的問題之一,就是異步操做過多的時候,代碼內會充斥着衆多回調函數,乃至造成回調金字塔。爲了解決回調函數帶來的問題,Promise做爲一種更優雅的異步解決方案被提出,最初只是一種實現接口規範,而到了es6,則是在語言層面就原生支持了Promise對象。java

最初接觸Promise的時候,我以爲它是比較抽象而且使人困惑的,相信不少人也有一樣的感受。可是在後來的熟悉過程當中,我慢慢體會到了它的優雅,並開始思考Promise對象實現的原理,最終用es5語法實現了一個具有基本功能的本身的Promise對象。在這篇文章中,會把本身實現的過程和思路按部就班的記錄一下,相信你們看完以後,也可以完全理解Promise對象運行的原理,並在之後的開發中,能更熟練的使用它。git

github源碼地址: https://github.com/mly-zju/Js-practicees6

1. 回到過去: resolve, reject和then

首先來看一個Promise的使用實例:github

var fn=function(resolve, reject){
  console.log('begin to execute!');
  var number=Math.random();
  if(number<=0.5){
    resolve('less than 0.5');
  }else{
    reject('greater than 0.5');
  }
}

var p=new Promise(fn);
p.then(function(data){
  console.log('resolve: ', data);
}, function(data){
  console.log('reject: ', data);
})

這個例子當中,在fn當中產生一個0~1的隨機數,若是小於等於0.5, 則調用resolve函數,大於0.5,則調用reject函數。函數定義好以後,用Promise包裹這個函數,返回一個Promise對象,而後調用對象的then方法,分別定義resolve和reject函數。這裏resolve和reject比較簡單,就是把傳來的參數加一個前綴而後打印輸出。less

這裏咱們須要注意,當運行 p=new Promise(fn)這條語句的時候,fn函數就已經在執行了,然而,p.then這個方法是在後面才定義了resolve和reject,那麼爲什麼fn函數可以知道resolve和reject函數是什麼呢?dom

換句話說,resolve和reject函數是如何回到過去,出如今先執行的fn函數當中的呢?這是Promise當中最重要的一個概念之一。異步

其實想要實現這個「黑科技」,方法也很是簡單,主要運用的就是setTimeout這個方法,來延遲fn當中resolve和reject的執行。利用這個思路,咱們能夠初步寫出一個本身的初級版Promise,這裏咱們命名爲MyPromise:函數

function MyPromise(fn) {
  this.value;
  this.resolveFunc = function() {};
  this.rejectFunc = function() {};
  fn(this.resolve.bind(this), this.reject.bind(this));
}

MyPromise.prototype.resolve = function(val) {
  var self = this;
  self.value=val;
  setTimeout(function() {
    self.resolveFunc(self.value);
  }, 0);
}

MyPromise.prototype.reject = function(val) {
  var self=this;
  self.value=val;
  setTimeout(function() {
    self.rejectFunc(self.value);
  }, 0);
}

MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
  this.resolveFunc = resolveFunc;
  this.rejectFunc = rejectFunc;
}

var fn=function(resolve, reject){
  console.log('begin to execute!');
  var number=Math.random();
  if(number<=0.5){
    resolve('less than 0.5');
  }else{
    reject('greater than 0.5');
  }
}

var p = new MyPromise(fn);
p.then(function(data) {
  console.log('resolve: ', data);
}, function(data) {
  console.log('reject: ', data);
});

能夠看出, MyPromise接收fn函數,並將本身的this.resolve和this.reject方法做爲fn的resolve和reject參數傳給fn並執行。而咱們觀察MyPromise的resolve方法,即可以發現,其主要操做,就是使用setTimeout,延遲0秒執行resolveFunc。this

而再來觀察then方法,能夠看到,這裏比較簡單,就是接受兩個函數,並分別賦給自身的this.resolveFunc和this.rejectFunc。

這裏邏輯就很清楚了,雖然fn函數首先執行,可是因爲在調用resolve和reject的時候,使用了setTimeout。雖然是延遲0秒執行,可是咱們知道js是單線程+消息隊列,必須等主線程代碼執行完畢才能開始執行消息隊列當中的代碼。所以,會首先執行then這個方法,給resolveFunc和rejectFunc賦值。then執行完畢後,再執行setTimeout裏面的方法,這個時候,resolveFunc和rejectFunc已經被賦值了,因此就能夠順利執行。這就是「回到過去」的奧祕所在。

2. 加入狀態: pending, resolved, rejected

上一節,初步實現了看起來彷佛可以運行的MyPromise,可是問題不少。咱們看一下下面代碼:

var fn=function(resolve, reject){
  resolve('hello');
  reject('hello again');
}

var p1=new Promise(fn);
p1.then(function(data){
  console.log('resolve: ',data)
}, function(data){
  console.log('reject: ',data)
});
//'resolve: hello'

var p2=new MyPromise(fn);
p2.then(function(data){
  console.log('resolve: ',data)
}, function(data){
  console.log('reject: ',data)
});
//'resolve: hello '
//'reject: hello again'

p1是原生Promise,p2是咱們本身寫的,能夠看出,當調用resolve以後再調用reject,p1只會執行resolve,咱們的則是兩個都執行。事實上在Promise規範當中,規定Promise只能從初始pending狀態變到resolved或者rejected狀態,是單向變化的,也就是說執行了resolve就不會再執行reject,反之亦然。

爲此,咱們須要在MyPromise中加入狀態,並在必要的地方進行判斷,防止重複執行:

function MyPromise(fn) {
  this.value;
  this.status = 'pending';
  this.resolveFunc = function() {};
  this.rejectFunc = function() {};
  fn(this.resolve.bind(this), this.reject.bind(this));
}

MyPromise.prototype.resolve = function(val) {
  var self = this;
  if (this.status == 'pending') {
    this.status = 'resolved';
    this.value=val;
    setTimeout(function() {
      self.resolveFunc(self.value);
    }, 0);
  }
}

MyPromise.prototype.reject = function(val) {
  var self = this;
  if (this.status == 'pending') {
    this.status = 'rejected';
    this.value=val;
    setTimeout(function() {
      self.rejectFunc(self.value);
    }, 0);
  }
}

MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
  this.resolveFunc = resolveFunc;
  this.rejectFunc = rejectFunc;
}

這樣,再次運行上面的實例,就不會出現resolve和reject都執行的狀況了。

3. 鏈式調用

在Promise的使用中,咱們必定注意到,是能夠鏈式調用的:

var fn=function(resolve, reject){
  resolve('hello');
}

var p1=new Promise(fn);
p1.then(function(data){
  console.log(data);
  return 'hello again';
}).then(function(data){
  console.log(data);
});
//'hello'
//'hello again'

很顯然,要實現鏈式調用,then方法的返回值也必須是一個Promise對象,這樣才能再次在後面調用then。所以咱們修改MyPromise的then方法:

MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
  var self = this;
  return new MyPromise(function(resolve_next, reject_next) {
    function resolveFuncWrap() {
      var result = resolveFunc(self.value);
      resolve_next(result);
    }
    function rejectFuncWrap() {
      var result = rejectFunc(self.value);
      resolve_next(result);
    }

    self.resolveFunc = resolveFuncWrap;
    self.rejectFunc = rejectFuncWrap;
  })
}

這裏能夠看出,then返回了一個MyPromise對象。在這個MyPromise當中,包裹了一個函數,這個函數會當即執行,主要作的事情,就是對resolveFunc和rejectFunc進行封裝,而後再賦值給前一個MyPromise的resolveFunc和rejectFunc。這裏難點是看懂封裝的目的。

這裏以上面一個例子來講明。在上面的鏈式調用例子中,出現了兩個Promise,第一個是咱們經過new Promise顯式定義的,咱們叫它Promise 1,而第二個Promise,是Promise 1的then方法返回的一個新的,咱們叫它Promise 2 。在Promise 1的resolve方法執行以後,resolve的返回值,會傳遞給Promise 2的resolve做爲參數,這也是爲何上面第二個then中打印出了第一個then返回的字符串。

而咱們封裝的目的,就是爲了讓Promise 1的resolve或者reject在執行後,將其返回值傳遞給Promise 2的resolve。在咱們本身的實現中,Promise 2的resolve咱們命名爲resolve_next,在Promise 1的resolveFunc執行以後,咱們拿到返回值result,而後調用resolve_next(result),傳遞參數給Promise 2的resolve。這裏值得注意的是,不管Promise 1執行的是resolveFunc仍是rejectFunc,其以後調用的,都是Promise 2的resolve,至於Promise 2的reject用來幹嗎,在下面的章節裏面咱們會詳細描述。

至此,咱們的MyPromise看起來就可使用鏈式調用了。

然而咱們再回去觀察Promise規範,會發現鏈式調用的狀況也分兩種。一種狀況下,前一個Promise的resolve或者reject的返回值是普通的對象,這種狀況下咱們目前的MyPromise能夠正確處理。但還有一種狀況,就是前一個Promise的resolve或者reject執行後,返回的值自己又是一個Promise對象,舉個例子:

var fn=function(resolve, reject){
  resolve('hello');
}

var p1=new Promise(fn);
p1.then(function(data){
  console.log(data);
  return 'hello again';
}).then(function(data){
  console.log(data);
  return new Promise(function(resolve){
    var innerData='hello third time!';
    resolve(innerData);
  })
}).then(function(data){
  console.log(data);
});
//'hello'
//'hello again'
//'hello third time!'

在這個例子當中出現了兩次鏈式調用,第一個then返回的是一個'hello again'字符串,在第二個then的resolve中會打印處理。而後咱們注意第二個then當中,返回的是一個Promise對象,調用了resolve。那麼問題來了,這個resolve哪裏來呢?答案就是在第三個then當中定義!這個例子中第三個then定義的resolve也比較簡單,就是直接打印傳給resolve的參數。

所以,這裏咱們的MyPromise也須要修改,針對前一個resolve或者reject的返回值作判斷,看是否是Promise對象,若是是,就作不一樣的處理,修改的代碼以下:

MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
  var self = this;
  return new MyPromise(function(resolve_next, reject_next) {
    function resolveFuncWrap() {
      var result = resolveFunc(self.value);
      if (result && typeof result.then === 'function') {
        //若是result是MyPromise對象,則經過then將resolve_next和reject_next傳給它
        result.then(resolve_next, reject_next);
      } else {
        //若是result是其餘對象,則做爲參數傳給resolve_next
        resolve_next(result);
      }
    }
    function rejectFuncWrap() {
      var result = rejectFunc(self.value);
      if (result && typeof result.then === 'function') {
        //若是result是MyPromise對象,則經過then將resolve_next和reject_next傳給它
        result.then(resolve_next, reject_next);
      } else {
        //若是result是其餘對象,則做爲參數傳給resolve_next
        resolve_next(result);
      }
    }
    self.resolveFunc = resolveFuncWrap;
    self.rejectFunc = rejectFuncWrap;
  })
}

能夠看到在代碼中,對於resolveFunc或者rejectFunc的返回值,咱們會判斷是否含有.then方法,若是含有,就認爲是一個MyPromise對象,從而調用該MyPromise的then方法,將resolve_next和reject_next傳給它。不然,正常對象,result就做爲參數傳給resolve_next。

這樣修改以後,咱們的MyPromise就能夠在鏈式調用中正確的處理普通對象和MyPromise對象了。

如此,在這篇文章中,咱們就首先實現了Promise的經常使用基本功能,主要是then的調用,狀態的控制,以及鏈式調用。而在後面的文章中,還會進一步講解如何實現Promise的錯誤捕獲處理等等(好比Promise當中的.catch方法原理),從而讓咱們的MyPromise真正健壯和可用!

相關文章
相關標籤/搜索