今天来聊聊 promise 对象的一些问题。内容主要来自 We have a problem with promises 这篇文章。英语水平有限,翻译存在不当。

抛出问题

首先来看下面的这个问题。doSomethingdoSomethingElse 都是函数,返回一个 promise 对象。

问:下面的是个 promise 有什么区别?

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 新手常犯的错误。

低级错误

错误 1:promise 调用地狱

下面是人们在使用 PouchDB 的 promise API 中最常见的一些不好的实践。

1
2
3
4
5
6
7
8
9
10
11
12
13
remotedb.allDocs({
  include_docs: true,
  attachments: true
}).then(function (result) {
  var docs = result.rows;
  docs.forEach(function(element) {
    localdb.put(element.doc).then(function(response) {
      alert("Pulled doc with id " + element.doc._id + " and added to local db.");
    }).catch(function (err) {
      if (err.name == 'conflict') {
        localdb.get(element.doc._id).then(function (resp) {
          localdb.remove(resp._id, resp._rev).then(function (resp) {
// et cetera...

下面是上面例子的一个更好的写法。

1
2
3
4
5
6
7
8
9
remotedb.allDocs(...).then(function (resultOfAllDocs) {
  return localdb.put(...);
}).then(function (resultOfPut) {
  return localdb.get(...);
}).then(function (resultOfGet) {
  return localdb.put(...);
}).catch(function (err) {
  console.log(err);
});

这叫做 composing promises,每一个函数都必须等到前一个 promise 呼叫了 resolved 才会执行,拿到返回值。

错误 2:在 promise 如何实用 forEach

这里会使多数刚开始使用 promise 的人陷入疑惑,当他们在使用 promise 过程中遇到类似 forEach(), for, while 这样的循环,他们不知道该如何处理循环与 promise 的同时使用。于是会出现下面的代码:

1
2
3
4
5
6
7
8
// I want to remove() all docs
db.allDocs({include_docs: true}).then(function (result) {
  result.rows.forEach(function (row) {
    db.remove(row.doc);
  });
}).then(function () {
  // I naively believe all docs have been removed() now!
});

上面代码的问题是第一个函数实际上返回的是 undefined,这意味着第二个函数并没有等到 db.remove() 执行到所有的文档中。事实上,第二个函数并没有进行等待,而是在没有任何文档被删除的时候就执行了。

这是一个非常隐蔽的 bug 因为你可能不会注意到哪里错了,如果 PouchDB 删除这些文档足够快的话,让你更新你的 UI。

这里实际上你需要使用 Promise.all:

1
2
3
4
5
6
7
db.allDocs({include_docs: true}).then(function (result) {
  return Promise.all(result.rows.map(function (row) {
    return db.remove(row.doc);
  }));
}).then(function (arrayOfResults) {
  // All docs have really been removed() now!
});

Promise.all() 接受一个包含其他 promise 的数组作为参数,当参数中每一项 promise 状态变为 resolve 的时候 Promise.all() 会返回一个 resolve。

错误 3:忘记添加 .catch()

1
2
3
4
5
somePromise().then(function () {
  return anotherPromise();
}).then(function () {
  return yetAnotherPromise();
}).catch(console.log.bind(console)); // <-- this is badass

强烈建议使用 catch

错误 4:using ‘deferred’

可以借助一些第三方库在不支持 promise 的地方来使用 promise。比如 Bluebird, Lie 等等。也可以将一些非 promise 的 api 封装成 promise

1
2
3
4
5
6
7
8
new Promise(function (resolve, reject) {
  fs.readFile('myfile.txt', function (err, file) {
    if (err) {
      return reject(err);
    }
    resolve(file);
  });
}).then(/* ... */)

问题 5:using side effects instead of returning

使用副作用而不是返回值。。。

下面的错误代码中,第二个 then 并不会等到 someOtherPromise resolve 后执行。

1
2
3
4
5
6
somePromise().then(function () {
  someOtherPromise();
}).then(function () {
  // Gee, I hope someOtherPromise() has resolved!
  // Spoiler alert: it hasn't.
});

接下来来说一个你必须要知道的关于 promise 的东西。

promise 的魔法在于它会返回我们 return 和 throw 的值。但是这个在实践中是什么样的呢?每一个 promise 都有一个 then 方法(或者是 then(null, ...) 的语法糖 catch() 方法 )。下面是一个处于 then() 中的函数:

1
2
3
somePromise().then(function () {
  // I'm inside a then() function!
});

我们能在函数内部做什么呢?有以下三件事:

  1. return 另一个 promise
  2. return 一个同步的值或者 undefined
  3. 抛出一个同步的错误

如果你理解了以上三点,你就理解了 promise。下面我们来逐个分析每一点。

1. return 另一个 promise

下面是一个普通的例子,如同上面 composing promises 的例子。

1
2
3
4
5
getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // I got a user account!
});

注意返回的第二个 promise getUserAccountById。这里这个 return 关键字至关重要。如果没有 return,getUserAccountById() 将变为无用的,在第二个 then 中将拿到 undefined 而不是 userAccount

2. 返回一个同步的值或者 undefined

返回 undefined 通常是一个错误,但是如果返回一个同步的值通常是不错的作法在 promise 代码中传递同步的代码。例如,我们有一个缓存中的用户信息。我们可以这样做:

1
2
3
4
5
6
7
8
getUserByName('nolan').then(function (user) {
  if (inMemoryCache[user.id]) {
    return inMemoryCache[user.id];    // returning a synchronous value!
  }
  return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
  // I got a user account!
});

在第二个函数中,我们不必关心 userAccount 是来自同步的还是异步的,第一个函数中既可以同步或异步的值。

需要注意,如果没有返回值的话,将会返回 undefined。所以建议总是返回结果或抛出错误在 then() 方法中。

3. 抛出同步错误

说到错误处理,这是 promise 中非常值得称赞的地方。比如我们想再用过登出后抛出一个错误,这个十分简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
getUserByName('nolan').then(function (user) {
  if (user.isLoggedOut()) {
    throw new Error('user logged out!'); // throwing a synchronous error!
  }
  if (inMemoryCache[user.id]) {
    return inMemoryCache[user.id];       // returning a synchronous value!
  }
  return getUserAccountById(user.id);    // returning a promise!
}).then(function (userAccount) {
  // I got a user account!
}).catch(function (err) {
  // Boo, I got an error!
});

catch 将会收到一个同步的错误如果用户登出了,并且它将捕获一个异步的错误不论任何 promise rejected。另外,catch 不考虑它得到的错误是同步还是异步的。

这一点非常有用,因为它能帮助我们在开发中捕获代码错误,不论错误在哪个 then() 方法函数内抛出。

高级错误

问题 1:不知道 Promise.resolve()

正如上边代码展示,promise 在将同步代码转为异步代码中非常有用,然而,如果你发现你写了很多类似下边的代码:

1
2
3
new Promise(function (resolve, reject) {
  resolve(someSynchronousValue);
}).then(/* ... */);

你可以使用 Promise.resolve() 来更加简明的实现上边的功能:

1
Promise.resolve(someSynchronousValue).then(/* ... */);

同样非常有用在捕获任何同步错误。

1
2
3
4
5
6
function somePromiseAPI() {
  return Promise.resolve().then(function () {
    doSomethingThatMayThrow();
    return 'foo';
  }).then(/* ... */);
}

如果你使用 Promise.resolve() 包裹一切,记得要 catch 可能出现的错误。

类似的,Promise.reject() 可以立刻返回一个 rejected 的 promise 对象

1
Promise.reject(new Error('some awful error'));

问题 2:then(resolveHandler).catch(rejectHandler) 不完全等同于 then(resolveHandler, rejectHandler)

上面提到 catch() 是 then(null, …) 的语法糖,所以下面的两个片段是相同的:

1
2
3
4
5
6
7
somePromise().catch(function (err) {
  // handle error
});

somePromise().then(null, function (err) {
  // handle error
});

但是,这并不意味着下面两个片段相等。

1
2
3
4
5
6
7
8
9
10
11
somePromise().then(function () {
  return someOtherPromise();
}).catch(function (err) {
  // handle error
});

somePromise().then(function () {
  return someOtherPromise();
}, function (err) {
  // handle error
});

如果你不太清楚为什么它们不想等,考虑一下下面的代码,当第一个函数抛出错误后会发生什么:

1
2
3
4
5
6
7
8
9
10
11
somePromise().then(function () {
  throw new Error('oh noes');
}).catch(function (err) {
  // I caught your error! :)
});

somePromise().then(function () {
  throw new Error('oh noes');
}, function (err) {
  // I didn't catch your error! :(
});

正如结果显示,当你使用 then(resolveHandler, rejectHandler) 格式,rejectHandler 并不会捕获到 resolveHandler 自身抛出的错误。

所以建议永远不要使用 then() 的第二个参数,而是使用 catch()。可以使用下面的 Mocha 测试用例,来避免这个问题。

1
2
3
4
5
6
7
it('should throw an error', function () {
  return doSomethingThatThrows().then(function () {
    throw new Error('I expected an error!');
  }, function (err) {
    should.exist(err);
  });
});

问题 3:promises 对比 promise 工厂

假设你想要在一个队列中一个接一个执行一系列的 promise,也就是说,你想要一个类似 Promise.all() 的方法,但是又不是同 Promise.all() 这样并行的执行。

你也许会天真的写下下面的代码:

1
2
3
4
5
6
7
function executeSequentially(promises) {
  var result = Promise.resolve();
  promises.forEach(function (promise) {
    result = result.then(promise);
  });
  return result;
}

不幸的是,结果并不会像你预期的那样。传递给 executeSequentially() 的一系列 promises 仍然并行执行。

发生这种情况的原因是你不想立刻在一个数组之上运行。每一个 promise 都是不同的,当他一但创造,便执行了。所以你真正需要的是一个由 promise 工厂组成的数组。

1
2
3
4
5
6
7
function executeSequentially(promiseFactories) {
  var result = Promise.resolve();
  promiseFactories.forEach(function (promiseFactory) {
    result = result.then(promiseFactory);
  });
  return result;
}

一个 promise 工厂非常简单,它仅仅是一个返回 promise 的函数。

1
2
3
function myPromiseFactory() {
  return somethingThatCreatesAPromise();
}

为什么这个就生效了,因为 promise 工厂不产生 promise 直到被执行。它的工作方式如同一个 then 函数,事实上,它们是相同的东西。

上边 myPromiseFactory 实际上是 executeSequentially() 的 result.then() 的替代。

问题 4:怎么得到两个 promise 的返回值

通常,一个 promise 依赖于另一个 promise,但是我们想要得到多个 promise 的输出,比如:

1
2
3
4
5
getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // dangit, I need the "user" object too!
});

一种解决办法是将 user 对象申明在更高的作用域:

1
2
3
4
5
6
7
var user;
getUserByName('nolan').then(function (result) {
  user = result;
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // okay, I have both the "user" and the "userAccount"
});

上边的代码可以实现同时拿到 user 和 userAccount,但是它的写法看起来有些恶心,建议可以写成下面的形式:

1
2
3
4
5
getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id).then(function (userAccount) {
    // okay, I have both the "user" and the "userAccount"
  });
});

最后可以将输出放入一个命名函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function onGetUserAndUserAccount(user, userAccount) {
  return doSomething(user, userAccount);
}

function onGetUser(user) {
  return getUserAccountById(user.id).then(function (userAccount) {
    return onGetUserAndUserAccount(user, userAccount);
  });
}

getUserByName('nolan')
  .then(onGetUser)
  .then(function () {
  // at this point, doSomething() is done, and we are back to indentation 0
});

当你的 promise 代码变得越来越复杂,你会发现你会将越来越多的方法转为命名函数。这会使你的代码看起来很美观:

1
2
3
4
putYourRightFootIn()
  .then(putYourRightFootOut)
  .then(putYourRightFootIn)
  .then(shakeItAllAbout);

问题 5:promises fall through

下面的代码会输出什么呢?

1
2
3
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
  console.log(result);
});

如果你认为是 bar,那么你错了,输出结果为 foo

原因是当你给 then() 传入了一个非函数(比如 一个 promise)作为参数,实际上将解释为 then(null),所以造成了之前的 promise 的结果穿过。你可以测试下下面的代码:

1
2
3
Promise.resolve('foo').then(null).then(function (result) {
  console.log(result);
});

不论在之间添加多少 then(null),它仍输出 foo。

这个也正好解释了上面的问题 promise 对比 promise factories。换句话说,你可以直接传入一个 promise 对象给 then() 方法,但是它不会如你所想的方式运行。then() 方法应该接受一个函数作为参数,所以大多数情况下你想要干的是下面这样的:

1
2
3
4
5
Promise.resolve('foo').then(function () {
  return Promise.resolve('bar');
}).then(function (result) {
  console.log(result);
});

这次输出的是 bar,正如我们期望的。

所以,时刻提醒自己,总是给 then() 传入一个 function

揭晓答案

现在我们已经了解了关于 promise 的一切,我们应该可以解决文章开始的题目了。

问题一

1
2
3
doSomething().then(function () {
  return doSomethingElse();
}).then(finalHandler);

答案如下:

1
2
3
4
5
6
doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                                     finalHandler(resultOfDoSomethingElse)
                                     |------------------|

问题二

1
2
3
doSomething().then(function () {
  doSomethingElse();
}).then(finalHandler);

答案如下:

1
2
3
4
5
6
doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                  finalHandler(undefined)
                  |------------------|

问题三

1
2
doSomething().then(doSomethingElse())
  .then(finalHandler);

答案如下:

1
2
3
4
5
6
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
                  finalHandler(resultOfDoSomething)
                  |------------------|

问题四

1
2
doSomething().then(doSomethingElse)
  .then(finalHandler);

答案如下:

1
2
3
4
5
6
doSomething
|-----------------|
                  doSomethingElse(resultOfDoSomething)
                  |------------------|
                                     finalHandler(resultOfDoSomethingElse)
                                     |------------------|