译者: 辣椒炒肉html
原文地址:pouchdb.com/...git
JavaScript 开发者们,认可一个事实吧:咱们也许并不了解Promise。程序员
众所周知,A+规范所定义的Promise,很是棒。github
有个很大的问题是,在我过去使用Promise的一年中,看到不少开发者们,在用 PouchDB API或者是其余的Promise API,可是却不理解其中的原理。编程
不相信么?那请看我最近发布的这个推特。(惋惜连接失效了,辣椒本人也没看到)api
doSomething().then(function () {
return doSomethingElse();
});
doSomething().then(function () {
doSomethingElse();
});
doSomething().then(doSomethingElse());
doSomething().then(doSomethingElse);
复制代码
若是你知道答案,那么恭喜你!你是一个Promise大神!下面的内容能够不看了。数组
至于其余99.99%的人,也恭喜大家了,大家上对车了。我在推特上发布的那个问题,尚未人给我一个完美的回答。我对本身的#3回答也感到难以想象,即便我写了测试。promise
答案在这篇文章的最后。但首先,我想探讨一下,为何Promise如此棘手?为何这么多人都为它所困惑?我也将提供一些解释,相信会让Promise不那么难理解。浏览器
首先咱们尝试一些假设。bash
若是你读过一些Promise的文章,你确定会找到不少回调地狱的引用,它们稳定地延伸到屏幕的右边,这很糟糕!
Promise 确实能够解决这个问题,但它不只仅是起到缩进的做用。正如它被盛誉的那样:回调地狱的救赎。回调函数带来的问题就是,剥夺了咱们对return和throw的掌控,并且还有一个反作用,一个函数意外地调用了另一个函数。
事实上,回调函数还作了更加让人讨厌的事情:它丢失了原来的栈,这是咱们 在编程语言中一般认为理所固然的事情。编写代码丢失了对栈的掌控,就像驾驶一辆没有刹车的汽车那样,你不知道它会驶向哪里。
Promise的重点是,把异步所丢失的return, throw, 和栈还给咱们。可是你必须知道如何正确使用promises才能利用它们。
有的人试图将Promise解释为卡通,或者这样形容:“哦,这个返回值就是异步回来的结果”
我以为这种解释不是颇有帮助。对我来讲,Promises都是关于代码结构和流程的。因此我认为,最好回顾一些常见的错误而且想一想怎么修复它们。我称之为“菜鸟错误”的意思是:“你如今是一个菜鸟,但你很快会成为一个职业选手”
Promise对不少人来讲意味着不一样的东西,但就本文而言,我只谈论官方规范,在现代浏览器中暴露为window.Promise
基于Promise的PouchDB,看看下面一个糟糕的例子:
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) {
// ...
复制代码
若是你认为这样的写法只限于初学者,那你错了。我在官方的BlackBerry开发者博客中发现了这样的代码!旧的回调习惯很难消亡。(致上面代码的做者:抱歉,但您的代码颇有借鉴意义)
更好的例子是这样:
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);
});
复制代码
这是Promise链式写法,只有前一个promise执行完后面一个才会执行,并将前一个的返回值做为参数。稍后会详细介绍。
这是大多数人对Promise的理解开始崩溃的地方。一旦用到他们熟悉的forEach和while循环时,他们就不知道怎么和Promise一块儿使用。因此他们这样写:
// 我想删除所有的doc
db.allDocs({include_docs: true}).then(function (result) {
result.rows.forEach(function (row) {
db.remove(row.doc);
});
}).then(function () {
// 我天真地觉得我删除了所有的doc
});
复制代码
这段代码有什么问题?其实第一个函数返回undefined。这意味着第二个函数不会等待所有执行完db.remove(), 事实上它啥也不用等待。
这是一个很隐蔽的错误,由于PouchDB若是足够快删除这些文档并更新UI, 你可能不会注意到任何错误。这个错误可能会在奇怪的条件下或者某些浏览器中暴露。这时候来调试几乎是不可能的。
全部这些for/forEach/while都不是合适的解决办法,这时候你须要Promise.all()
db.allDocs({include_docs: true}).then(function (result) {
return Promise.all(result.rows.map(function (row) {
return db.remove(row.doc);
}));
}).then(function (arrayOfResults) {
// 如今这些doc真的所有被删除了!
});
复制代码
这里发生了什么?Promise.all接受一个promise数组做为参数,而后当每一个promsie都resolve了,返回一个新的promise,包括了每一个promise的resolve结果。它是for循环的异步等价物。
Promise.all()还将一个结果数组传递给下一个函数,这可能很是有用。例如,当你试图从PouchDB中获取多个东西, 若是任何一个子promise被rejected,那么all()的promise也会被拒绝,这更有用。
这也是一个常见的错误。自信地认为他们的代码不会发生异常。不幸的是,这意味着任何的错误都会被吞下,你甚至都不会在控制台中看到它们,这才是最痛苦的。
为了不这种状况,我已经习惯在promise链中添加这样的代码:
somePromise().then(function () {
return anotherPromise();
}).then(function () {
return yetAnotherPromise();
}).catch(console.log.bind(console)); // <-- this is badass
复制代码
即便你很是肯定不会发生任何错误,最好仍是添加一个catch(),让生活更美好。
【辣椒没看懂这段。大概是,用Promise封装异步的操做吧(这不是很常规的操做么)】
new Promise(function (resolve, reject) {
fs.readFile('myfile.txt', function (err, file) {
if (err) {
return reject(err);
}
resolve(file);
});
}).then(/** */);
复制代码
【辣椒不喜欢上面这样写。我本身会封装起来这段,return这个promise在别的地方await 这个promise获取返回。我知道我在说es7的async/await, 我就是看不惯这种写法。】
下面这段代码有什么问题?
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// 哎呀,我但愿someOtherPromise()已经resolved了!
// 剧透:并无
});
复制代码
正如我以前所说,Promise的魔力在于它们将咱们的return和throw带回来。 但这在实践中其实是什么样的?
每一个promise都会给你一个then()方法(或者catch(),是语法糖,能够在then的第二个参数处理错误then(null,...))。 这里咱们在then()函数内:
somePromise().then(function () {
// 我在then里面
});
复制代码
咱们在这儿能够作三件事:
返回另外一个promise
返回一个同步值(或者是undefined)
抛出一个同步异常
一旦你理解了这个技巧,你就理解了Promise。 下面咱们具体说说这三点:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// 我拿到了一个用户帐号!
});
复制代码
请注意,我正在返回第二个promise。 return相当重要!! 若是我没有写return,那么getUserAccountById()其实是effect,而下一个函数将接收undefined而不是userAccount。
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // 返回一个同步值!
}
return getUserAccountById(user.id); // 返回一个promise!
}).then(function (userAccount) {
// 我拿到了一个userAccount!
});
复制代码
是否是很棒!第二个函数不关心userAccount是同步仍是异步获取的。第一个函数能够自由返回同步或异步值。
不幸的是,有一个事实是,JavaScript中的非返回函数在技术上返回undefined,这意味着当你想要返回一些内容时,很容易意外地引入effect。
出于这个缘由,我老是习惯在then()函数内return或throw,建议你也这样作。
getUserByName('nolan').then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!'); // 抛出一个同步异常!
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // 返回一个同步值!
}
return getUserAccountById(user.id); // 返回一个promise!
}).then(function (userAccount) {
// 我拿到了userAccount!
}).catch(function (err) {
// 砰! 我拿到一个异常!
});
复制代码
若是用户注销,咱们的catch()将收到同步错误,若是任何promise被拒绝,它将收到异步错误。 一样,该函数不关心它得到的错误是同步仍是异步。
这特别有用,由于它能够帮助识别开发过程当中的编码错误。 例如,若是在then()函数内部的任何一点,咱们执行JSON.parse(),若是JSON无效,它可能会抛出同步错误。 有了回调,这个错误就会被吞噬,可是使用promise,咱们能够在catch()函数中简单地处理它。
好的,如今你已经学会了一些基本的promise技巧,那咱们就聊聊边缘状况吧。
我将这些错误归类为“高级”,由于我只看到了那些已经至关擅长Promise的程序员犯的错误。 可是,若是咱们但愿可以解决我在本文开头提出的问题,咱们还须要继续讨论一下。
上面我已经讲过,promises对于将异步代码包装为同步代码很是有用。 可是,若是你发现本身会这样写:
new Promise(function (resolve, reject) {
resolve(/** 同步值*/);
}).then(/* ... */);
复制代码
你可使用Promise.resolve()更简洁地这样写:
Promise.resolve(/** 同步值*/).then(/* ... */);
复制代码
这对于捕获任何同步错误也很是有用。 它很是有用,我养成了几乎全部promise-api都写return的习惯:
function somePromiseAPI() {
return Promise.resolve().then(function () {
doSomethingThatMayThrow();
return 'foo';
}).then(/* ... */);
}
复制代码
请记住,任何可能throw同步错误的代码,均可能会发生“难以调试”的吞噬错误。若是你将全部的代码都包装在Promise.resolve()中,那么就老是能够确保稍后捕获到。
相似地,有一个Promise.reject()可用于返回当即拒绝的promise:
Promise.reject(new Error('some awful error'));
复制代码
我上面说过catch()只是语法糖。 因此这两个片断是等价的:
somePromise().catch(function (err) {
// handle error
});
somePromise().then(null, function (err) {
// handle error
});
复制代码
可是,这并不意味着如下两个片断是等价的:
somePromise().then(function () {
return someOtherPromise();
}).catch(function (err) {
// handle error
});
somePromise().then(function () {
return someOtherPromise();
}, function (err) {
// handle error
});
复制代码
若是您想知道为何它们不相同,请想一下,若是第一个函数抛出错误会发生什么:
somePromise().then(function () {
throw new Error('oh noes');
}).catch(function (err) {
// 我捕获了一个异常
});
somePromise().then(function () {
throw new Error('oh noes');
}, function (err) {
// 我没有捕获到异常
});
复制代码
事实证实,当您使用then(resolveHandler,rejectHandler)格式时,若是由resolveHandler自己抛出,则rejectHandler实际上不会捕获错误。【辣椒我的os:这好理解,resolveHandler和rejectHandler是同一级的,捕获不到应该是合理的,rejectHandler只能捕获somePromise发生的异常。因此,你能够用catch啊!】
出于这个缘由,我已经习惯于永远不要使用then()的第二个参数,而且老是更喜欢catch()。 例外的状况是我在编写异步Mocha测试时,我可能会编写一个测试来确保抛出错误:
it('should throw an error', function () {
return doSomethingThatThrows().then(function () {
throw new Error('I expected an error!');
}, function (err) {
should.exist(err);
});
});
复制代码
说到这一点,Mocha和Chai是测试Promise-API的很好的组合。 pouchdb-plugin-seed项目有一些示例测试能够帮助你入门。
假设你想按顺序依次执行一系列的promises。 也就是说,你想要像Promise.all()这样的东西,但它不会并行执行promises。
你可能天真地写这样的东西:
function executeSequentially(promises) {
var result = Promise.resolve();
promises.forEach(function (promise) {
result = result.then(promise);
});
return result;
}
复制代码
不幸的是,这不会按照你的想法执行。 传递给executeSequentially()的promise仍然会并行执行。
发生这种状况的缘由是你根本不想操做数组里的promise。 根据promise规范,一旦建立了promise,它就会开始执行。 因此你须要的是数组promise工厂:
function executeSequentially(promiseFactories) {
var result = Promise.resolve();
promiseFactories.forEach(function (promiseFactory) {
result = result.then(promiseFactory);
});
return result;
}
复制代码
我知道你在想什么:“这个Java程序员究竟是谁,为何他在谈论工厂呢?” promise工厂很简单,它只是一个返回promise的函数:
function myPromiseFactory() {
return somethingThatCreatesAPromise();
}
复制代码
为何这样写就有用?它起做用是由于promise工厂在被调用以前不会建立promise。 它的工做方式与当时的功能相同,实际上,它是一个东西!
若是你看一下上面的executeSequentially()函数,而后想象myPromiseFactory在result.then(...)中被替换,那么但愿你会灵光一闪,获得promise启蒙。
一般,一个promise将取决于另外一个promise,但咱们但愿获得两个promise的输出。 例如:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// 我也须要“user”对象!
});
复制代码
想要成为优秀的JavaScript开发人员并避免厄运的金字塔,咱们可能只是将用户对象存储在更高范围的变量中:
var user;
getUserByName('nolan').then(function (result) {
user = result;
return getUserAccountById(user.id);
}).then(function (userAccount) {
// 好的,我拿到了user和userAccount
});
复制代码
这是能够的,但我我的以为它有点笨拙。 我推荐的策略:放下你的先入之见,使用金字塔写法:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id).then(function (userAccount) {
// 好的,我拿到了user和userAccount
});
});
复制代码
或者你这样写:
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 () {
// 在这一点上,doSomething()完成了,咱们又回到了缩进0
});
复制代码
随着你的promise代码变得愈来愈复杂,可能会发现本身将愈来愈多的函数提取到命名函数中。 我发现这样的代码很是美观,像这样:
putYourRightFootIn()
.then(putYourRightFootOut)
.then(putYourRightFootIn)
.then(shakeItAllAbout);
复制代码
这就是promise的所有。
最后,当我介绍上面的promise难题时,这就是我提到的错误。 这是一个很是深奥的用例,你可能永远不会遇到,但它确实让我感到惊讶。
你认为此代码打印出来的是什么?
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
console.log(result);
});
复制代码
若是你认为它打印出来bar,你就错了。 它实际上打印出foo!
发生这种状况的缘由是由于当你传递then()一个非函数(例如一个promise)时,它实际上将它解释为then(null),这会致使前一个promise的结果失败。 你能够本身测试一下:
Promise.resolve('foo').then(null).then(function (result) {
console.log(result);
});
复制代码
它仍会打印foo。
这实际上回到了我以前关于promise与promise工厂的观点。 简而言之,你能够将promise直接传递给then()方法,但它不会按照您的想法执行。 then()应该接受一个函数,因此极可能你打算这样作:
Promise.resolve('foo').then(function () {
return Promise.resolve('bar');
}).then(function (result) {
console.log(result);
});
复制代码
这会如咱们所指望的那样,打印bar。
因此只需提醒本身:将函数传递给then()!
如今咱们已经学会了全部关于promise的知识,咱们应该可以解决我在本文开头提出的难题。【就是推特那个】
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);
复制代码
答案:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
复制代码
doSomething().then(function () {
doSomethingElse();
}).then(finalHandler);
复制代码
答案:
doSomething
|-----------------|
doSomethingElse(undefined)
|------------------|
finalHandler(undefined)
|------------------|
复制代码
doSomething().then(doSomethingElse())
.then(finalHandler);
复制代码
答案:
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
finalHandler(resultOfDoSomething)
|------------------|
复制代码
doSomething().then(doSomethingElse)
.then(finalHandler);
复制代码
答案
doSomething
|-----------------|
doSomethingElse(resultOfDoSomething)
|------------------|
finalHandler(resultOfDoSomethingElse)
|------------------|
复制代码
若是这些答案仍然没有起到做用,那么我建议你从新阅读帖子,或者定义doSomething()和doSomethingElse()方法,并在浏览器中自行尝试。
【文章年代久远,那时候es7还没出,可是依然有些参考意义。如今异步编程的解决办法,大可能是Promise+async/await, 即:用Promise封装异步api(好比fs.readFile),在外部的async方法中await刚才封装好的方法,爽爽的!这将从新审视这篇文章的做者提出的一些异步写法】