problem with promise
今天来聊聊 promise
对象的一些问题。内容主要来自 We have a problem with promises 这篇文章。英语水平有限,翻译存在不当。
抛出问题
首先来看下面的这个问题。doSomething
和 doSomethingElse
都是函数,返回一个 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! }); |
我们能在函数内部做什么呢?有以下三件事:
- return 另一个 promise
- return 一个同步的值或者 undefined
- 抛出一个同步的错误
如果你理解了以上三点,你就理解了 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) |------------------| |