这里,我不打算讲Promise的原理,以及回调地狱 blah, blah, blah… 之类。这样的文章网上实在太多了,google 一下一地都是。 但是,讲究竟如何实际去运用的却很少,使用上细节其实很重要。这也是很多人搞不明白这玩意怎么用的原因所在。这里我们以 bluebird 为例看下 Promise 是啥玩意。

首先,Promise它长啥样?

简单来讲,它就是一种把回调转换成链接操作的编程方法。那么什么是链式?jQuery大家都会比较熟悉吧。看下下面这段代码:

1
$('div').css('highlight').html('Hello world').show();

这就是链式操作。那么,这个链式操作是怎么完成的呢?稍微看下 jQuery 的代码或者写过 jQuery 扩展的人都知道,它是通过在每个 function 体最后返回一个 jQuery 对象完成的。在上面这段代码中,所有的调用之后其实都是一个 jQuery 对象。所有的 css,html, show 都是由 jQuery 对象提供的方法。

jQuery和Promise有什么关系?

jQuery 是链式操作的老祖宗,是不是原创的我不敢说,但是 jQuery 把这种风格推广到整个技术界这点是勿用置疑的。本质上 Promise 也是对这链式操作思想的发扬光大。链式操作的优势是很明显的,对比 jQuery 和以往传统的 javascript 可以明显地看到代码可读性不在一个层级上。我们先看下正常情况下如何读取文件:

1
2
3
4
fs.readFile('file.txt', "utf-8", function (err, data) {
  if (err) return console.error(err);
  console.log(data);
});

很常见的。Promise 完成同样的功能是怎么写呢?首先,要初始化生成 promisified function.

1
2
3
4
5
var Promise = require('bluebird');
var readAsync = Promise.promisify(fs.readFile);
var readFile = function(path) {
  return readAsync(path, 'utf-8');
}

然后是使用:

1
readFile('/etc/passwd').then(console.log).catch(console.error);

风格上是不是跟 jQuery 很像?但是好像除了短一点,其它好处不明显呐?

1
2
3
4
5
6
7
fs.readFile('file.txt', 'utf-8', function (err1, data1) {
  if (err1) return console.error(err1);
  fs.readFile(data1, 'utf-8', function(err2, data2) {
    if (err2) return console.error(err2);
    console.log(data2);
  }
});

vs

1
readFile('file.txt').then(readFile).then(console.log).catch(console.error);

结果是一样的,可读性不在一个层次上。并且,万一哪个回调中忘了写上 if(err) 这样的判断或者是忘了写 return ,除起错了可不好玩!你会不会写错我不知道,反正我不敢保证我一定不会写错!

上面的代码是怎么回事?

这里,我们确定几个术语:

  1. Promise是一种编程方法(思想),哲学上的概念。
  2. bluebird 是对 Promise 的一种实现。
  3. readFile 是 Promisified Function,它会返加一个 Promise对象。一个 Promise对象就是对一个异步任务的包装,它会产生两种结果:resolved代表成功执行下一个 then, rejected代表失败执行 catch。
  4. Promise 对象最主要就是提供了 then 方法(catch、finally 都是特殊的 then)。then 是 Promise 的核心,所以 Promise 对象也称作 Thenable。
  5. then 方法接收一个 function 。在 Promisified Function 顺利完成之后执行这个 function 。出错则执行 catch 的 function

关键的地方在于 then 是怎么运作的?理解 then 也就解决了问题的百分之六十。

  • then 接收一个 function 或一个 promise对象(这点要特别注意,后面有讲到一个示例会牵涉到这个关键点)
  • then 在异步任务完成后调用这个 function ,同时,会把异步操作的结果值传入,所以上面第一个 then 的 readFile 会接收到 file.txt 的内容。
  • then 不管你这个 function 里面返回什么东西,它对返回值只作一个判断:是不是 Promise对象
  • 是 Promise对象:把它插入当前链式操作中,后面的 then 会在这个 Promise对象 完成后继续。
  • 否 把这个返回值传入到下一个 then 中(当然,如果没有 return ,这个值就是 undefined)。
  • 若传入的是promise对象,则会等待该 promise对象执行完成,再继续执行当前的链,之前的结果会被继续传递

总的来讲,then方法执行完毕后总是返回一个 Promise对象。结果是你可以一直 then 下去。。。 好吧,then方法好像很牛逼的样子,那到底怎么牛逼法?

1
2
3
4
5
readFile('file.txt').then(function(data) {
  if (data == '')
  return readFile('file_init.txt').then(readFile); //是 Promise对象
  return data; //不是 Promise对象
}).then(console.log).catch(console.error);`

牛逼的地方就在于我们可以动态的修改链式操作的走向。这样的代码看起来既舒服又简单,试一下用传统的方法写一下?

刚才说的示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
doSomething().then(function () {
  return doSomethingElse();
});

doSomething().then(function () {
  doSomethingElse();
});

doSomething().then(doSomethingElse());

doSomething().then(doSomethingElse);

这是下面四种 Promise 的用法有何差异?, ( 从 阮一峰微博里面挪过来的)。 中修正后的代码。 原文还提到:

doSomething & doSomethingElse both promise object

我想说:

No, they are not. They are promisified function. Function and Object are quite different things.

先别往下拉,请先看下上面四种方式,有什么不同?如果明白了,说明你已经基本掌握 Promise 了。

确认结论是否正确(修正)

  1. 脱裤子放屁,多了一层 function 包装,跟4是一样的效果。

  2. 它后面再加 then 是不会等到 doSomethinElse 完成后执行,而是同时进行。

  3. 传入的其实是一个 promise object,这个 promise object 会被执行并等待其完成,但结果会被忽略,第三个 then 接收到的仍是第一个 readFile 的结果。

  4. doSomethingElse 本身就是个 function,传入后会被调用,返回一个 Promise对象 ( doSomethingElse 不是 promise object, 它的返回值才是 promise object)。

    当然,看不出来也说明不了什么问题,因为这样的示例迷惑性很强,给出的提示也不明显。正常情况下更不会存在这样的代码。顺便一提,这也是函数式语言的 side effect,很容易绕晕掉到坑里。根据 We have a problem with promises里面所讲,掉坑的人不在少数!

还需要知道其它的吗?

以上只是基础知识,实际应用时当然还有一些扩展的方法需要去了解,比如 spread, all 等。npmjs.org 上的文档那是必读的……