我们在promises的使用上存在问题
来源:互联网 发布:故宫的记忆 知乎 编辑:程序博客网 时间:2024/04/29 18:56
JavaScript的开发者同事们,现在是时候承认一个事实了:我们在promises的使用上还存在问题。
但并不是promises他们本身有问题,被A+标准定义的promises是极好的。
在过去一年的课程中揭示给我的一个比较大的问题是,正如我所看到的,很多的程序员在使用PouchDB API以及与其他重promise的API的过程中存在的一个问题是:
我们一部分人在使用promises的过程中并没有真正的理解promises。
如果你觉得这不可思议,那么考虑下我最近在Twitter上的写的一个比较难的题目:
问题:下面的4个promises有什么区别呢?
doSomething().then(function () { return doSomethingElse();});doSomething().then(function () { doSomethingElse();});doSomething().then(doSomethingElse());doSomething().then(doSomethingElse);
如果你知道答案,那么恭喜你:你是一个promises武士。我觉得你可以不用再继续读这篇博客文章。
对其他的99.99%的人,你们都在很好的公司上班。但是在回复我推特的人中没有人能解决这个问题,而且我对3楼的回答感到特别惊讶。是的,尽管我写了测试案例!
答案在这篇文章的末尾,但是首先,我想在第一时间搞清楚为什么promises是如此的棘手,以及为什么我们这么多人,不管是新手还是像专家的人,都会被它们搞晕。我也将提供我认为的我对其独特的洞察力,用一个独特的技巧,使得对promises的理解成为有把握的事情。是的,在这之后我认为它们真的不是那么难。
但是在开始之前,让我们挑战一下对于promises常见的一些假设。
为什么要用promises?
事实上,回调做了很多更加险恶的事情:它们剥夺了我们的堆栈,这些是我们在编程语言中经常要考虑的。没有堆栈来书写代码在某种程度上就好比驾车没有刹车踏板:你不会知道你是多么需要它,直到你到达了却发现它并不在这。
promises的全部意义在于它给回了在函数式语言里面我们遇到异步时所丢失的return,throw和堆栈。为了更好的从中获益你必须知道如何正确的使用promises。
新手常犯的错误
一些人尝试解释promises是卡通,或者是一种名词导向的方式:“你可以传递的东西就是代表着异步值”。
我并没有发现这些这些解释多么有帮助。对于我来说,promises就是关乎于代码结构和流程。因此我认为,过一下一些常见的错误以及展示出如何修复它们是更好的方式。我把这称为“新手常犯的错误”的意义就在于,“你现在是个新手,初出茅庐,但你很快会成为专业人士”。
一点题外话:“promises”对于不同的人有不同的理解,但是这篇文章的目的在于,我只是谈论官方标准,正如在现代浏览器中所暴露的window.Promise API。尽管不是所有的浏览器都支持window.Promise,对于一个很好的补充,可以查看名为Lie的项目,它是一个实现promises的遵循规范的最小集。
新手常见错误#1:世界末日的promise金字塔
看下开发人员怎么使用拥有大量基于promise的API的PouchDB,我发现了很多不好的promise模式。最常见的不好实践是这个:
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.status == 409) { localdb.get(element.doc._id).then(function (resp) { localdb.remove(resp._id, resp._rev).then(function (resp) {// et cetera...
是的,事实证明你是能使用像回调的promises的,而且是的,那有点像使用一个很有威力的磨砂机来打磨你的指甲,但是你是可以做到的。
如果你认为这一类型的错误会仅限于绝对的新手,你会惊讶的发现上面的代码就来自于黑莓开发者的官方博客。老的回调的习惯很难改(致开发者:很抱歉拿你来举例,但是你的代码很有教育意义)。
一个更好的方式是:
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);});
这被称为组成式promises(composing promises),它是有超能力的promises之一。每一个函数都在前面的promise被resolve之后被调用,而且将前面的promise的输出作为参数被调用。稍后将详细介绍。
新手常见错误#2:尼玛,我该怎么对Promises调用forEach()呢?
这是大多数人开始理解Promises要突破的地方。尽管他们能熟悉forEach()循环(或者for循环,或者while循环),他们并不知道如何对Promises使用这些循环。此时,他们写的代码会像是这样:
// I want to remove() all docsdb.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更新前会删除掉所有的文件。这个bug可能只出现在奇怪的竞态条件,或者特定的浏览器中,此时要去做debug是不可能的。
这所有的症结其实在于forEach()/for/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) { // All docs have really been removed() now!});
Promises.all()将一个数组作为结果传给下一个函数,这是很有用的,比如你正在试图从PouchDB获取一些东西的时候。如果任意一个all()的子promise被执行了reject,all()也会被执行reject,这甚至是更有用的。
新手常见错误#3:忘记添加.catch()
为了避免这种讨厌的场景,我已经习惯了在我的promise链中简单的添加如下代码:
somePromise().then(function () { return anotherPromise();}).then(function () { return yetAnotherPromise();}).catch(console.log.bind(console)); // <-- this is badass
甚至是你从未预料到会出错,添加.catch()方法都是很精明的做法。如果你的假设曾经被证明是错误的,它会让你的生活变的更简单。
新手常见错误#4:使用deferred
这是一个我总是会看到的错误,我甚至都不愿意在这里重复,为了以防万一,像阴间大法师那样,仅仅是提到它的名字就能得到更多的实例。
长话短说,promise有个很长的传奇的历史,JavaScript社区花了很长的时间来使得它的实现是正确的。在早期,jQuery和Angular到处都在使用这个“deferred”模式,现在已被更换为ES6 Promise标准,正如一些很好的库如 Q, When, RSVP, Bluebird, Lie以及其他库所实现的那样。
如果你正在你的代码里写deferred这种模式(我不会再重复第三次),那么你做的都是错的。下面是如何来避免这种错误。
首先,大多数的promise库提供了一种方式从第三方库中导入promises。例如,Angular的$q模块允许你使用$q.when()来封装非$q的模块。因此Angular的用户可以以这种方式来封装PouchDB的promises:
$q.when(db.put(doc)).then(/* ... */); // <-- this is all the code you need
另一个策略是使用揭示构造函数,这种策略对于封装非promise的API非常有用。例如,封装基于回调的API比如Node的fs.readFile(),你可以简单的这样做:
new Promise(function (resolve, reject) { fs.readFile('myfile.txt', function (err, file) { if (err) { return reject(err); } resolve(file); });}).then(/* ... */)
完成!我们已经击败了可怕的def...我住嘴:)
为什么这是一种反模式更多的信息可以查看:the Bluebird wiki page on promise anti-patterns。
新手常见错误#5:使用其副作用而不是return
下面的代码有什么问题?
somePromise().then(function () { someOtherPromise();}).then(function () { // Gee, I hope someOtherPromise() has resolved! // Spoiler alert: it hasn't.});
这是一个很好的点来谈论你所需要知道的所有关于promise的东西。
认真一点,这是一个有点奇怪的技巧,一旦你理解了它,就会避免我所谈论的所有的错误。你准备好了吗?
正如我之前所说,promises的神奇之处在于它给回了我们之前的return和throw。但是在实际的实践中它看起来会是什么样子呢?
每一个promise都会给你一个then()方法(或者catch,它们只是then(null,...)的语法糖)。这里我们是在then()方法的内部来看:
somePromise().then(function () { // I'm inside a then() function!});
我们在这里能做什么呢?有三种事可以做:
1、返回另一个promise;
2、返回一个同步值(或者undefined);
3、抛出一个同步错误。
就是这样。一旦你理解了这个技巧,你就明白了什么是promises。让我们一条条来说。
1、返回另一个promise
getUserByName('nolan').then(function (user) { return getUserAccountById(user.id);}).then(function (userAccount) { // I got a user account!});
注意,我正在返回第二个promise-return是很关键的。如果我没有说返回,getUserAccountById()方法将会产生一个副作用,下一个函数将会接收undefined而不是userAccount。
2、返回一个同步值(或undefined)
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是同步还是异步获取的,第一个函数对于返回同步还是异步数据是自由的。
不幸的是,这存在一个很不方便的事实,在JavaScript技术里没有返回的函数默认会自动返回undefined,这也就意味着当你想返回一些东西的时候很容易不小心引入一些副作用。
为此,我把在then()函数里总是返回数据或者抛出异常作为我的个人编码习惯。我也推荐你这么做。
3、抛出一个同步错误
说到throw,promises可以做到更棒。比如为了避免用户被登出我们想抛出一个同步错误。这很简单:
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()方法将接收到一个同步错误,而且任意的promises被拒绝它都将接收到一个同步错误。再一次强调,函数并不关心错误是同步的还是异步的。
这是非常有用的,因为它能够帮助我们在开发中识别代码错误。比如,在一个then()方法内部的任意地方,我们做了一个JSON.parse()操作,如果JSON参数不合法那么它就会抛出一个同步错误。用回调的话该错误就会被吞噬掉,但是用promises我们可以轻松的在catch()方法里处理掉该错误。
高级错误
这些错误我把它们归类为高级错误,因为我只在一些对于promise非常熟悉的程序员的代码中发现。但是,如果我们想解决我在文章开头提出的疑惑的话,我们需要讨论这些高级错误。
高级错误#1:不了解Promise.resolve()
正如我上面提到的,promises在封装同步代码为异步代码上是非常有用的。然而,如果你发现自己打了这样一些代码:new Promise(function (resolve, reject) { resolve(someSynchronousValue);}).then(/* ... */);你可以使用Promise.resolve()来更简洁的表达:
Promise.resolve(someSynchronousValue).then(/* ... */);
而且这在捕捉任意的同步错误上会难以置信的有用。它是如此有用,以致于我习惯于几乎将我所有的基于promise返回的API方法以下面这样开始:
function somePromiseAPI() { return Promise.resolve().then(function () { doSomethingThatMayThrow(); return 'foo'; }).then(/* ... */);}
记住:对于被彻底吞噬的错误以致于不能debug的任意代码,做同步的错误抛出都是一个很好的选择。但是你把每个地方都封装为Promise.resolve(),你要确保后面你都会执行caotch()。
Promise.reject(new Error('some awful error'));
高级错误#2:catch()并不和then(null,...)一摸一样
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) {<a target=_blank href="http://mochajs.org/" target="_blank">点击打开链接</a> // handle error});
如果你疑惑为什么它们不是等价的,思考第一个函数抛出一个错误会发生什么:
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)格式,如果resolveHandler自己抛出一个错误rejectHandler并不能捕获。
基于这个原因,我已经形成了自己的一个习惯,永远不要对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项目有很多你可以入手的简单的测试。
高级错误#3:promises vs promise工厂
我们假定你想要一个接一个的,在一个序列中执行一系列的promise。就是说,你想要Promise.all()这样的东西,不会并行的执行promises。
你可能会单纯的这样写一些东西:
function executeSequentially(promises) { var result = Promise.resolve(); promises.forEach(function (promise) { result = result.then(promise); }); return result;}
不幸的是,它并不会按你所期望的那样工作。你传递给executeSequentially()的promises会并行执行。
之所以会这样是因为其实你并不想操作一个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直到它被要求这么做。它的工作方式和then函数相同-实际上它们是同一个东西。
如果你在看上面的executeSequentially()函数,并且假定myPromiseFactory在result.then()内部被取代,那么希望你能灵光一闪。那时,你将实现promise启蒙(译者注:其实此时就是相当于执行:onePromise.then().then()...then())。
高级错误#4:好吧,假设我想要获取两个promises的结果将会怎样?
通常,一个promise是依赖于另一个promise的,但是这里我们想要两个promise的输出。例如:
getUserByName('nolan').then(function (user) { return getUserAccountById(user.id);}).then(function (userAccount) { // dangit, I need the "user" object too!});
如果想成为优秀的JavaScript开发者并避免世界末日的金字塔,我们可能在一个更高的的作用域中存储一个user对象变量:
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"});
这也能达到目的,但是我个人觉得这有点拼凑的感觉。我推荐的做法:放手你的偏见并拥抱金字塔:
getUserByName('nolan').then(function (user) { return getUserAccountById(user.id).then(function (userAccount) { // okay, I have both the "user" and the "userAccount" });});
至少,临时先这么干。如果缩进成为一个问题,你可以做JavaScript开发者一直以来都在做的事情,提取函数为一个命名函数:
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代码变得更加复杂,你可能发现你自己在抽取越来越多的函数为命名函数。我发现这样会形成非常美观的代码,看起来会像是这样:
putYourRightFootIn() .then(putYourRightFootOut) .then(putYourRightFootIn) .then(shakeItAllAbout);
这就是promises。
高级错误#5:promises丢失
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) { console.log(result);});
如果你认为打印出bar,那你就大错特错了。它实际上会打印出foo。
Promise.resolve('foo').then(null).then(function (result) { console.log(result);});
Promise.resolve('foo').then(function () { return Promise.resolve('bar');}).then(function (result) { console.log(result);});
这次会如我们预期的那样返回bar。
解决疑惑
疑惑#1:
doSomething().then(function () { return doSomethingElse();}).then(finalHandler);
答案:
doSomething|-----------------| doSomethingElse(undefined) |------------------| finalHandler(resultOfDoSomethingElse) |------------------|
疑惑#2:
doSomething().then(function () { doSomethingElse();}).then(finalHandler);
答案:
doSomething|-----------------| doSomethingElse(undefined) |------------------| finalHandler(undefined) |------------------|
疑惑#3:
doSomething().then(doSomethingElse()) .then(finalHandler);答案:
doSomething|-----------------|doSomethingElse(undefined)|---------------------------------| finalHandler(resultOfDoSomething) |------------------|
疑惑#4:
doSomething().then(doSomethingElse) .then(finalHandler);答案:
doSomething|-----------------| doSomethingElse(resultOfDoSomething) |------------------| finalHandler(resultOfDoSomethingElse) |------------------|
如果这些答案仍然没有讲通,那么我鼓励重新阅读文章,或者去定义doSomething()以及doSomethingElse()然后在你的浏览器中自己尝试。
关于promise最后的话
虽然比回调要优越,但是promises是理解比较困难而且容易出错,我感觉有必要写这篇博客就是明证。新手和专家都会把这个东西搞的一塌糊涂,事实上,这并不是他们的错。问题是promises本身,和我们在同步代码中使用的模式类似,是一个不错的替代又不完全一样。
实际上,你不应该不得不去学习一堆晦涩难懂的规则和新的API来做这些事,在同步的世界里,你可以完美的使用像是return,catch,throw以及for循环这些熟悉的模式。在你的脑海中不应该总是保持着两套并行的体系。
异步等待/等待
- 我们在promises的使用上存在问题
- monoIntPtr在linux上存在的问题
- SQL Server CE又存在不能在RamDisk上使用的问题.
- 使用pscp在Linux和Windows上传输数据可能存在的问题
- 使用Promises
- UIActivityIndicatorView在iphone4 Device上存在的问题
- 关于在Android上检测是否存在网络的问题
- UML,OOAD,RUP在实际使用中存在的问题
- 在IDL中使用 sequence 存在的问题及解决办法
- 在母版页中使用验证控件存在的问题
- 在SQL中使用PL/SQL函数存在的问题
- 质疑我们的存在
- 我们存在的意义
- 使用 pthread_create存在的问题
- VS2010使用存在的问题
- VS2010使用存在的问题
- 使用retainCount存在的问题
- 输在我们的优势上
- Apache Shiro介绍
- 一些Coco2d-js笔记
- hive如何处理多分隔符数据
- Spring注解@Component、@Repository、@Service、@Controller区别
- 在yii中用jquery实现删除之后跳转
- 我们在promises的使用上存在问题
- 手机 微信 试玩 IOS
- 排序算法:快速排序
- Java报错与解决方案
- fmt标签的格式化日期使用
- Android学习笔记12:图像渲染(Shader)
- 计算n以内的所有素数
- log4j.properties 详解与配置步骤
- Effective C++ 条款12