会用不如会造 - Promise

Promise ?

为什么要自己造 Promise

  • Promise已经是Ecmascript 6标准中的内容
  • Promise是async & await的基础
  • 不造一下,怎么才能把Promise/A+的标准了解的更透彻?

本文结构

  • Promise认知 - 40%
  • Promise轮子 - 60%

对Promise有足够了解的读者,可酌情阅读跳过认知部分。

Promise的认知阶梯

最早开始使用Promise是为了不写回调形式的函数;后来发现Promise可以让nodejs里function (err, data)的写法变得更友好;后来开始用 q;再后来发现 bluebird 这样顺应 Promise/A+ 的API用起来更顺。然后发现bluebird这个词原本就是一个combinator,compose其实是它的本意;然后发现Promise也是一种Monad。

以上是笔者自己对Promise的认知之路,理解上不断产生新的变化,其中真正可以提炼出来的步骤如下:

Phase 1 - Callback Hell 救星

首先,callback是异步操作的产物,同步操作不需要回调函数。早先在纯浏览器环境中,会用到callback的有:

  • ajax请求
  • DOM事件绑定
  • setTimeout

从nodejs开始,异步IO是根本特性,因而callback变得无处不在,然后出现了nodejs的callback参数标准:function (err, data)

/*
 * callback version
 */
var sillyCopy = function (src, target, callback) {
fs.readFile(src, function (err, data) {
  if (err)  return callback(err);
  fs.write(target, data, function (err) {
    if (err) return callback(err);
    fs.write(src, 'this is silly', function (err) {
      callback(err);
    });
  });
});

sillyCopy('my/dir/src.txt', 'my/dir/target.txt', function (err) {
  if (err)  console.log(err);
  else     console.log('done');
});

/*
 * promise version
 */
var sillyCopy = function (src, target) {
  return promiseFS.readFile(src)
  .then(function (data) {
    return promiseFS.writeFile(target));
  })
  .then(function() {
    return promiseFS.writeFile(src, 'this is silly');
  });
};

sillyCopy('my/dir/src.txt', 'my/dir/target.txt')
.then(function (data) {
  console.log(data);
}, function (err) {
  console.log(err);
});

注: 以上 promise 版本的 sillyCopy 中用到了promise化的 fs。

可以看到,使用了promise之后的异步操作定义,变得更加更加清晰

Phase 2 - 串联异步操作

var flow = function (list) {
  return function (arg) {
    return list.reduce(function (p, fn) {
      return p.then(fn);
    }, Promise.resolve(arg));
};

var sillyProcess = flow([
  pseudo_ReadFile,
  pseudo_AjaxPostSearchFileText,
  pseudo_ReadDB,
  pseudo_WriteToLocalFile
])

这里的flow就是同步模式下用到的 compose,用于将各种异步操作进行串联,在充满异步操作充满Promise的环境中,一个简单的flow实现可以让代码变得清晰易懂

Phase 3 - 异常捕获

回看之前的样例代码,callback形式会让对异常的捕获散布在各层级的callback里,看着闹心有没有?

var sillyCopy = function (src, target, callback) {
fs.readFile(src, function (err, data) {
  if (err)  return callback(err);
  fs.write(target, data, function (err) {
    if (err) return callback(err);
    fs.write(src, 'this is silly', function (err) {
      callback(err);
    });
  });
});

// This is what we want
sillyCopy(src, target)
.catch(function (err) {
  console.log(err);
});

在实际Coding过程中,异常处理是非常重要的步骤,除了上面看到的串行的异步代码,还有并行的异步操作,各种复杂情况导致对异常的统一处理变得十分关键,仅异常处理的代码会变得更容易维护。

Phase 4 - Thenable 接口

Thenable在一般场景中我们碰到的不算特别多,但它的价值却不能被低估。Thenable的价值在于,它是一个统一的接口定义,它让我们可以把不同Library里的、使用不同Promise实现的异步操作,放在一起使用。

例如,jQuery有它自己内部对Promise的实现 (早起使用 defer 创建 Promise 对象 ,后来慢慢统一到了 Promise/A+ 的标准模式)。还有 npm 上成千上万的包,都可能使用了不同的 Promise 实现。


开始造 Promise

既然是造轮子,先看下按照什么标准来造,然后确定一下造的步骤,剩下的就是开始coding了。

标准

步骤

我们把实现过程分成3部分,类似于对Promise的认知过程

  1. 串联异步操作,忽略异常处理
  • 构造函数
  • 保留 resolve 结果
  • then,要满足 resolve 之后的 then 也能得到执行
  1. 异常处理
  • then增加reject参数
  • 捕获 resolve 和 reject 过程中的异常
  1. Thenable
  • 允许返回一个 Thenable 的对象
  • 对 Thenable.then(resolve, reject) 的整体过程进行异常捕获

串联异步操作

  • 一个Promise对象可以多次调用 then
  • 保存 resolve 结果
var MyPromise = function (executor) {
  var self = this;

  this.next = [];
  this._value = null;
  this._state = 0;

  var resolve = function (value) {
    self._state = 1;
    self._value = value;
    self.next.forEach(function (fn) {
      fn(value);
    });
  };
  executor(resolve);
};

MyPromise.prototype.then = function (onResolve) {
  var self = this;

  if (!self._state) {
    return MyPromise.resolve(onResolve(self._value));
  }

  return new Promise(function (resolve) {
    self.next.push(function (value) {
      resolve(onResolve(value));
    });
  });
};

MyPromise.resolve = function (value) {
  return new Promise(function (resolve) {
    resolve(value);
  });
};

/*
 * Demo
 */
var p = Promise.resolve(2);

p
.then(function (a) { return a * 2 })
.then(function (a) { console.log(a) });
// output: 4

setTimeout(function () {
  p
  .then(function (a) { return a * 3 })
  .then(function (a) { console.log(a) });
  // output: 6
}, 100);

至此,串联异步操作部分就算完成了,一个不带 reject 和 异常捕获的 Thenable 实例。从上面的 Demo 可以看出,我们的 Promise API 已初具雏形。但不要得意的太早,Promise 的 API 本身并不复杂:

  • Promise API
    • Promise Constructor
    • Promise.prototype.then
    • Promise.prototype.catch
    • Promise.all
    • Promise.resolve
    • Promise.reject

不过同等功能下,越是简单的 API 设计,其内部逻辑就会越复杂。jQuery.$ 就是一个很好的例子。

异常捕获

  • then 接受两个参数, onResolve & onReject
  • 对 onResolve 和 onReject 执行过程中产生的异常进行捕获
var PENDING = 0;
var RESOLVED = 1;
var REJECTED = 2;

// Note: onResolve 和 onReject 都会在这里包一层
// 为了让两者的返回值都能继续向下传递到下一个 onResolve
var wrapHandler = function (state, handler, p2) {
  return function (val) {
    var next = state == RESOLVED ? p2._resolve : p2._reject;
    var ret, then, type, p3;

    if (!handler || typeof handler !== 'function') {
      return next(val);
    }

    try {
      ret = handler(val);
    } catch (e) {
      return p2._reject(e);
    }

    p2._resolve(ret);
  };
};

var genHandler = function (state, p) {
  return function (value) {
    if (p._state !== PENDING) return;
    p._state = state;
    p._value = value;
    p.next.forEach(function (obj) {
      setTimeout(function () {
        obj[state === RESOLVED ? 'onResolve' : 'onReject'](value);
      }, 0);
    });
  };
};

var MyPromise = function (executor) {
  var self = this;

  this.next = [];
  this._value = null;
  this._state = PENDING;
  this._resolve = genHandler(RESOLVED, self);
  this._reject  = genHandler(REJECTED, self);

  executor(this._resolve, this._reject);
};

MyPromise.prototype.then = function (onResolve, onReject) {
  var self = this;
  var p2   = new MyPromise(function () {});
  var ret;

  if (self._state === PENDING) {
    self.next.push({
      onResolve: wrapHandler(RESOLVED, onResolve, p2),
      onReject:  wrapHandler(REJECTED, onReject,  p2)
    });
  } else {
    setTimeout(function () {
      wrapHandler(self._state, self._state === RESOLVED ? onResolve : onReject, p2)(self._value);
    }, 0);
  }

  return p2;
};

MyPromise.resolve = function (value) {
  return new Promise(function (resolve, reject) {
    resolve(value);
  });
};

MyPromise.reject = function (value) {
  return new Promise(function (resolve, reject) {
    reject(value);
  });
};

/*
 * Demo
 */
MyPromise.reject(1)
.then(null, function (e) {
  console.log(e);
  return e + 2;
})
.then(function (n) {
  console.log(n);
  throw n * 2;
})
.then(null, function (e) {
  console.log(e);
});
// output: 1
// output: 3
// output: 6

这里有几个非常重要的 Promise 使用方法:

  • 没有设置 onResolve 或 onReject,对应的返回值或异常将原封不动向下传递
    Promise.resolve(1)
    .then()
    .then(function (n) { console.log(n); });
    // output: 1
    
    Promise.reject(2)
    .then()
    .then(null, function (e) { console.log(e); });
    // output: 2
    
  • onReject 的返回值会继续向下传递给下一个 onResolve
    Promise.reject(1)
    .then(null, function (e) { return e + 2; })
    .then(function (n) { console.log(n); };
    // output: 3
    

Thenable 接口

  • Thenable 类型的处理
  • 出现返回Promise实例自身的情况,要抛出 TypeError
var PENDING = 0;
var RESOLVED = 1;
var REJECTED = 2;

var wrapHandler = function (state, handler, p2) {
  return function (val) {
    var next = state == RESOLVED ? p2._resolve : p2._reject;
    var ret, then, type, p3;

    if (!handler || typeof handler !== 'function') {
      return next(val);
    }

    try {
      ret = handler(val);
    } catch (e) {
      return p2._reject(e);
    }

    solve(ret, p2);
  };
};

// 处理所有可能情况的 val,包括 Thenable
var solve = function (val, p2) {
  if (val === p2) {
    return p2._reject(new TypeError('no promise circle allowed'));
  }

  type = typeof val;

  if (type === 'function' || type === 'object' && val !== null) {
    try {
      then = val && val.then;
    } catch (e) {
      return p2._reject(e);
    }

    if (typeof then === 'function') {
      try {
        return then.call(val, function (val2) {
          solve(val2, p2);
        }, p2._reject);
      } catch (e) {
        return p2._reject(e);
      }
    }
  }

  return p2._resolve(val);
}

var genHandler = function (state, p) {
  return function (value) {
    if (p._state !== PENDING) return;
    p._state = state;
    p._value = value;
    p.next.forEach(function (obj) {
      setTimeout(function () {
        obj[state === RESOLVED ? 'onResolve' : 'onReject'](value);
      }, 0);
    });
  };
};

var MyPromise = function (executor) {
  var self = this;

  this.next = [];
  this._value = null;
  this._state = PENDING;
  this._resolve = genHandler(RESOLVED, self);
  this._reject  = genHandler(REJECTED, self);

  executor(this._resolve, this._reject);
};

MyPromise.prototype.then = function (onResolve, onReject) {
  var self = this;
  var p2   = new MyPromise(function () {});
  var ret;

  if (self._state === PENDING) {
    self.next.push({
      onResolve: wrapHandler(RESOLVED, onResolve, p2),
      onReject:  wrapHandler(REJECTED, onReject,  p2)
    });
  } else {
    setTimeout(function () {
      wrapHandler(self._state, self._state === RESOLVED ? onResolve : onReject, p2)(self._value);
    }, 0);
  }

  return p2;
};

MyPromise.resolve = function (value) {
  return new Promise(function (resolve, reject) {
    resolve(value);
  });
};

MyPromise.reject = function (value) {
  return new Promise(function (resolve, reject) {
    reject(value);
  });
};

到此为止,一个满足 Promise/A+ 规范的 MyPromise 就算造完了。

完整代码在我的 github 上可以找到

https://github.com/kdepp/mash/blob/master/mash.js

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

推荐阅读更多精彩内容