Promise实现

Promise代表一个异步操作的结果,使用Promise我们可以:

  1. 摆脱回调地狱;
  2. 更易读的代码,函数参数、返回值一目了然。比较下面两个函数:
// 额外的callback参数让人对函数的输入和输出类型感到困惑
function asyncFn(argu: number, callback): void {
  let value: string = '';
   ...
   callback(err, value)
}

function asyncFn(argu: number): Promise<string> {
  return new Promise((resolve, reject) => {
    let value: string = '';
    ...
    if (err) {
      reject(err)
    }

    resolve(value)
  })
}
  1. 更容易地处理异步错误;

通过尝试实现Promise,可以对Promise的行为有更深的理解。
实现Promise先从阅读规范开始。

规范解读

Promise规范由3个部分组成:

  • Promise状态
  • then方法
  • Promise Resolution Procedure
    下面依次对这3部分进行解读,其中忽略了异常处理部分和一些细节。

Promise状态

  • promise的状态必为pendingfulfilledrejected这三个状态中的一个。
  • promisepending状态时,可能会转变为fulfilled状态或rejected状态。
  • promisefulfilled状态时,必须拥有一个value,并且不会再变化。
  • promiserejected状态时,必须拥有一个reason,并且不会再变化。
Promise 状态

then方法

  • promise必须提供一个then方法,通过then方法可获取最终的valuereason
  • then方法接收两个参数,并返回一个promise
promise2 = promise1.then(onFulfilled?, onRejected?)
  • onFulfilledonRejected是可选参数。
  • 如果onFulfilled是一个函数,它将仅在promise1变为fulfilled状态时被调用一次,value是它的第一个参数。
  • 如果onRejected是一个函数,它将仅在promise1变为rejected状态时被调用一次,reason是它的第一个参数。
  • onFulfilled不是函数,并且promise1fulfilled状态时,promise2的状态必须也是fulfilled,并且拥有和promise1一样的value。引用MDN的话说,即,当onFulfilled不是函数时,会用identity function(返回它接收的参数)替代。
  • onRejected不是函数,并且promise1rejected状态时,promise2的状态必须也是rejected,并且拥有和promise1一样的reason。引用MDN的话说,即,当onRejected不是函数时,会用thrower function(抛出它接收的参数)替代。
  • onFulfilledonRejected必须被异步调用。以避免unleash Zalgo。也就是要保证onFulfilledonRejected始终是异步调用的,避免出现有时异步有时同步的情况。比如下面这个例子,console.log始终会被异步执行。
new Promise((resolve, reject) => {
  if (Math.random() >= 0.5) {
    setTimeout(() => resolve(0), 5000)
    return;
  }
  
  resolve(1)
}).then(console.log)
  • onFulfilledonRejected必须被当作函数调用(没有this)。
  • then方法可以在同一个promise上多次调用。相应的onFulfilled/onRejected被调用的顺序和原本调用then方法的顺序相同。
  • onFulfilledonRejected任一个抛出错误epromise2会变成rejected状态,并以错误e作为reason
  • onFulfilledonRejected任一个返回xpromise2会执行Promise Resolution过程:[[Resolve]](promise2, x)

Promise Resolution过程

Promise Resolution过程即promise.resolve(x)的过程,规范中标记为[[Resolve]](promise, x)
首先这里涉及一个概念:thenable,如果xobject或者function类型,并且有then方法,那么x就是thenable,显然,promise对象是thenable
如果x不是thenablePromise Resolution过程就是将promise变为fulfilled状态,并以x作为fulfilled value
如果xthenablepromise会尝试采用x的状态。即,xpending状态时,promise也是pending状态;如果x变成fulfilled状态,promise也变成fulfilled状态,并用xfulfilled value作为promisefulfilled value;如果x变成rejected状态,promise也变成rejected状态,并用xrejected reason作为promiserejected reason
因此,会存在thenable chain的情况:

thenable chain

thenable chain的长度不会被限制,可以链任意多的thenable
但是,考虑到[[Resolve]](promise1, promise1)会导致无限循环,所以这种情况下应该抛出TypeError。如:

chaning cycle.png

以下3种场景都会执行[[Resolve]](promise, x)过程

// 1.
promise.resolve(x);

// 2.
promise = new Promise(function(resolve, reject) {
  resolve(x);
})

// 3.
promise = promise1.then(function() {
  return x;
})

实现

为了便于理解,将promise实现拆分成若干步骤,逐步实现。

状态机

由于Promise本身是一个状态机,所以从这里开始。Promise初始状态是pending,在fulfillreject时,更新状态。

function Promise() {
  var value, reason, state = 'pending';
  
  function beFulfilled(result) {
    state = 'fulfilled';
    value = result;
  }

  function beRejected(error) {
    state = 'rejected';
    reason = error;
  }
}

Promise Resolution过程

Promise Resolution Procedure是一个递归的过程,直至所有thenable都完成。

function Promise(fn) {
  var value, reason, state = 'pending';
  
  function beFulfilled(result) {
    state = 'fulfilled';
    value = result;
  }

  function beRejected(error) {
    state = 'rejected';
    reason = error;
  }


  function doResolution(x) {
    var type = typeof x
    if ((type === 'object' || type === 'function') && typeof x.then === 'function') {
      x.then(function(val) {
        doResolution(val);
      }, function(err) {
        beRejected(err)
      })
    }

    beFulfilled(x)
  }

  fn(doResolution, beRejected)
}

then方法onFulfilled/onRejected执行

then方法可能在fulfilled/rejected之前或者之后被调用,也可以被多次调用。如果调用then时,promise还处于pending状态,应该先将onFulfilled/onRejected暂存起来。在promise变为fulfilled/rejected之后,依次执行暂存的onFulfilled/onRejected

function Promise(fn) {
  var value,
      reason,
      state = 'pending',
      handlers = [];
  
  function beFulfilled(result) {
    state = 'fulfilled';
    value = result;
    handlers.forEach(handle);
    handlers = [];
  }

  function beRejected(error) {
    state = 'rejected';
    reason = error;
    handlers.forEach(handle);
    handlers = [];
  }

  function doResolution(x) {
    var type = typeof x
    if ((type === 'object' || type === 'function') && typeof x.then === 'function') {
      x.then(function(val) {
        doResolution(val);
      }, function(err) {
        beRejected(err)
      })
    }

    beFulfilled(x)
  }

  function handle(handler) {
    if (state === 'pending') {
      handlers.push(handler);
      return;
    } else if (state === 'fulfilled') {
      handler.onFulfilled.call(null, value)
    } else if (state === 'rejected') {
      handler.onRejected.call(null, reason)
    }
  }

  this.then = function(onFulfilled, onRejected) {
    handle({
       onFulfilled,
       onRejected,
    });
  }

  fn(doResolution, beRejected)
}

then方法返回promise

then方法需要返回一个promise,返回的promise依赖onFulfilled/onRejected执行结果,所以将返回的promiseresolve/rejectthen方法的onFulfilled/onRejected保存在一起,在onFulfilled/onRejected执行后调用resolve/reject

function Promise(fn) {
  var value,
      reason,
      state = 'pending',
      handlers = [];
  
  function beFulfilled(result) {
    state = 'fulfilled';
    value = result;
    handlers.forEach(handle);
    handlers = [];
  }

  function beRejected(error) {
    state = 'rejected';
    reason = error;
    handlers.forEach(handle);
    handlers = [];
  }

  function doResolution(x) {
    var type = typeof x
    if ((type === 'object' || type === 'function') && typeof x.then === 'function') {
      x.then(function(val) {
        doResolution(val);
      }, function(err) {
        beRejected(err)
      })
    }

    beFulfilled(x)
  }

  function handle(handler) {
    if (state === 'pending') {
      handlers.push(handler);
      return;
    } else if (state === 'fulfilled') {
      try {
        handler.resolve(handler.onFulfilled.call(null, value))
      } catch(e) {
        handler.reject(e)
      }
    } else if (state === 'rejected') {
      try {
         handler.resolve(handler.onRejected.call(null, reason))
      } catch(e) {
         handler.reject(e);
      }
    }
  }

  this.then = function(onFulfilled, onRejected) {
    return new Promise(function(resolve, reject) {
      handle({
         onFulfilled,
         onRejected,
         resolve,
         reject,
      });
    })
  }
  fn(doResolution, beRejected)
}

至此,基本实现了promise,最后,补充完整错误处理和其他细节,代码如下:

function Promise(fn) {
  var value,
      reason,
      state = 'pending',
      handlers = [],
      /**
       * 当resolve/reject被多次调用时,只有第一次调用有效。
       * 如:
       * new Promise((resolve, reject) => {
       *   resolve(new Promise(r => setTimeout(() => r(1), 5000)))
       *   resolve(new Promise(r => setTimeout(() => r(2), 3000)))
       * }).then(console.log)
       **/
      invoked = false,
      self = this; 

  // 考虑typeof null === 'object',所以判断类型是否为object或function,不能简单通过typeof判断
  function typeOf(obj) {
    return Object.prototype.toString.call(obj).match(/^\[object\s+(\w+)\]$/)[1].toLowerCase();
  }

  function once(func) {
    return function() {
      if (invoked) return;
      invoked = true;
      func.apply(null, arguments);
    }
  }
  
  function beFulfilled(result) {
    state = 'fulfilled';
    value = result;
    handlers.forEach(handle);
    handlers = [];
  }

  function beRejected(error) {
    state = 'rejected';
    reason = error;
    handlers.forEach(handle);
    handlers = [];
  }

  function doResolution(x) {
    /**
     * 如果onFulfilled/onRejected被多次调用,只有第一次调用有效。
     * 如:
     * new Promise((resolve, reject) => {
     *   resolve({
     *     then(onFulfilled, onRejected) {
     *       onFulfilled(new Promise(r => setTimeout(() => r(1), 5000)));
     *       onFulfilled(new Promise(r => setTimeout(() => r(2), 3000)));
     *     }
     *   })
     * }).then(console.log)
     */
    var called = false;
    if (self === x) {
      beRejected(new TypeError('Chaining cycle detected'));
      return;
    }
    var type = typeOf(x);

    if (type === 'object' || type === 'function') {
      try {
        var then = x.then;
        if (typeOf(then) === 'function') {
          try {
            then.call(x, function(val) {
              if (called) return;
              called = true;
              doResolution(val);
            }, function(err) {
              if (called) return;
              called = true;
              beRejected(err);
            })
          } catch(e) {
            if (called) return;
            called = true;
            beRejected(e);
          }
        } else {
          beFulfilled(x);
        }
      } catch(e) {
        beRejected(e);
      }
    } else {
      beFulfilled(x);
    }
  }

  function handle(handler) {
    if (state === 'pending') {
      handlers.push(handler);
      return;
    } else if (state === 'fulfilled') {
      var x, onFulfilled = typeOf(handler.onFulfilled) === 'function' ? handler.onFulfilled : function(v) {
        return v;
      };
      setTimeout(function() {
        try {
          x = onFulfilled(value);
        } catch(e) {
          handler.reject(e);
          return;
        }

        handler.resolve(x);
      });
    } else if (state === 'rejected') {
       var x, onRejected = typeOf(handler.onRejected) === 'function' ? handler.onRejected : function(r) {
         throw r;
       };
       setTimeout(function() {
         try {
           x = onRejected(reason);
         } catch(e) {
           handler.reject(e);
           return;
         }

         handler.resolve(x);
       });
    }
  }

  this.then = function(onFulfilled, onRejected) {
    return new Promise(function(resolve, reject) {
      handle({
         onFulfilled,
         onRejected,
         resolve,
         reject,
      });
    });
  };

  fn(once(doResolution), once(beRejected));
}

完整代码已经通过Promises/A+ Compliance Test Suite全部测试用例。

// 测试文件,我将上面实现的Promise取名Qromise
const Qromise = require('../Qromise')

describe("Promises/A+ Tests", function () {
  require("promises-aplus-tests").mocha({
    resolved(value) {
      return new Qromise(resolve => resolve(value));
    },
    rejected(reason) {
      return new Qromise((...[, reject]) => reject(reason));
    },
    deferred() {
      const obj = {}

      obj.promise  = new Qromise((resolve, reject) => {
        obj.resolve = value => resolve(value);
        obj.reject = reason => reject(reason);
      })

      return obj
    },
  });
});
通过全部872个测试用例
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容