i promisee的意思是金字塔吗

ES6带来了很多新的特性其中生成器、yield等能对之前金字塔式的异步回调做到很好地解决,而基于此封装的co框架能让我们完全已同步的方式来编写异步代码这篇文章就对生荿器函数(GeneratorFunction)及框架thunkify、co的核心代码做比较彻底的分析。co的使用还是比较广泛的除了我们日常的编码要用到外,一些知名框架也是基于co实現的比如被称为下一代的Nodejs

格式的代码,其本质也是一个函数所以它具备普通函数所具有的所有特性。除此之外它还具有以下有用特性:

  1. 执行生成器函数后返回一个生成器(Generator),且生成器具有throw()方法可手动抛出一个异常,也常被用于判断是否是生成器;
  2. 在生成器函数内蔀可以使用yield(或者yield*)函数执行到yield的时候都会暂停执行,并返回yield的右值(函数上下文如变量的绑定等信息会保留),通过生成器的next()方法會返回一个对象含当前yield右边表达式的值(value属性),以及generator函数是否已经执行完(done属性)等的信息每次执行next()方法,都会从上次执行的yield的地方往下直到遇到下一个yield并返回包含相关执行信息的对象后暂停,然后等待下一个next()的执行;
  3. 生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法的参数是上一个暂停处yield的返回值

// 返回一个i promisee实例,所以可以以下面这种方式调用co: // 如果gen是一个函数则将其置为函数的返回值 // 返回一个i promisee实例所以可以以下面这种方式调用co:

我们主要看这个i promisee内部做了什么。

首先判断co()函数的第一个参数是否是函數,是的话将除gen之外的参数传给该函数并返回给gen;在这里因为gen是一个生成器函数所以返回一个生成器;

我们这个例子gen是一个生成器,则繼续往下执行

后面我们就遇到了co的核心函数:onFulfilled。我们看下这个函数做了什么

为了防止分心,里面错误的处理我们先暂时不理

第一次執行该方法,res值为undefined然后执行生成器的next()方法,对应我们例子里就是执行:

那么ret是一个对象大概是这样:

然后将ret传给next函数。next函数是:

首先判断生成器内部是否已经执行完执行完则将执行结果resolve出去。很明显我们例子里才执行到第一个yield并没有执行完。没执行完则将ret.value转化为┅个i promisee实例,我们这里是一个thunk函数所以toi promisee真正执行的是:

执行后其实就是直接返回了一个i promisee实例。而这里面也对fn做了执行,fn是:function(cb){}对应到这裏,function(err, res){...}就是被传入到fn中的cb第一个参数就是error对象,第二个参数res就是读取文件后数据然后执行resolve,将结果传到下一个then方法的成功函数内而在這里对应的是:

其实也就是onFulFilled的参数res。根据上面第三条执行准则我们知道,res是被传入到生成器的next()方法里的其实也就是对应co内生成器函数參数里的var a = yield readFile('a.txt',{encoding:'utf8'});里的a的值,从而实现了类似于同步的变成范式

理解了co的执行逻辑,我们就能更好的掌握其用法对于后续使用koa等基于co编写的框架我们也能更快速地上手。

为了更方便快捷的理解co的执行逻辑在网络上还有一个简版的实现,如下:

但这个实现仅支持yield后面是thunk函数的凊形。使用示例:

}

本文翻译自  同时也为原文题目,翻译时重新起了一个题目并且对原文有删改

各位JavaScript程序员,是时候承认了我们在使用i promisee的时候,会写出许多有问题的i promisee代码 当然并不是i promisee夲身的问题,  规范定义的i promisee非常棒 在过去的几年中,笔者看到了很多程序员在调用PouchDB或者其他i promisee化的API时遇到了很多困难这让笔者认识到,在JavaScript程序员之中只有少数人是真正理解了i promisee规范的。如果这个事实让你难以接受那么思考一下我给出的一个题目:

Question:下面四个使用i promisee的语句之間的不同点在哪儿?

如果你知道这个问题的答案那么恭喜你,你已经是一个i promisee大师并且可以直接关闭这个网页了

但是对于不能回答这个問题的程序员中99.9%的人,别担心你们不是少数派。没有人能够在笔者的tweet上完全正确的回答这个问题而且对于第三条语句的最终答案也令峩感到震惊,即便我是出题人

答案在这篇博文的底部,但是首先笔者必须先介绍为何i promisee显得难以理解,为什么我们当中无论是新手或者昰很接近专家水准的人都有被i promisee折磨的经历同时,笔者也会给出自认为能够快速、准确理解i promisee的方法而且笔者确信读过这篇文章之后,理解i promisee不会那么难了

在此之前,我们先了解一下有关i promisee的一些基本设定

如果你读过有关i promisee的文章,你会发现文章中一定会提到  不说别的,在視觉上回调金字塔会让你的代码最终超过屏幕的宽度。

i promisee是能够解决这个问题的但是它解决的问题不仅仅是缩进。在讨论到如何  的时候我们遇到真正的难题是回调函数剥夺了程序员使用return和throw的能力。而程序的执行流程的基础建立于一个函数在执行过程中调用另一个函数时產生的副作用(译者注:个人对这里副作用的理解是,函数调用函数会产生函数调用栈而回调函数是不运行在栈上的,因此不能使用return和throw)

事实上,回调函数会做一些更邪恶的事情它们剥夺我们在栈上执行代码的能力,而在其他语言当中我们始终都能够在栈上执行代码。编写不在栈上运行的代码就像驾驶没有刹车的汽车一样在你真正需要它之前,你是不会理解你有多需要它

i promisee被设计为能够让我们重新使用那些编程语言的基本要素:return,throw栈。在想要使用i promisee之前我们首先要学会正确使用它。

一些人尝试使用  的方式解释i promisee或者是像是解释名詞一样解释它:它表示同步代码中的值,并且能在代码中被传递

笔者并没有觉得这些解释对理解i promisee有用。笔者自己的理解是:i promisee是关于代码結构和代码运行流程的因此,笔者认为展示一些常见错误并告诉大家如何修正它才是王道。

扯远一点对于i promisee不同的人有不同的理解,為了本文的最终目的我在这里只讨论i promisee的官方  ,在较新版本的浏览器会作为window对象的一个属性被暴露出来然而并不是所有的浏览器都支持這一特性,但是到目前为止有许多对于规范的实现比如这个有着很嚣张的名字的i promisee库:  ,同时它还非常精简

新手错误No.1:回调金字塔

PouchDB有许哆i promisee风格的API,程序员在写有关PouchDB的代码的时候常常将i promisee用的一塌糊涂。下面给出一种很常见的糟糕写法

你确实可以将i promisee当做回调函数来使用,泹这却是一种杀鸡用牛刀的行为不过这么做也是可行的。 你可能会认为这种错误是那些刚入行的新手才会犯的但是笔者在黑莓的 上曾經看到类似的代码。过去的书写回调函数的习惯是很难改变的

下面给出一种代码风格更好的实现:

这就是i promisee的链式调用,它体现i promisee的强大之處每个函数在上一个i promisee的状态变为resolved的时候才会被调用,并且能够得到上一个i promisee的输出结果稍后还有详细的解释。

这个问题是大多数人掌握i promisee嘚拦路虎当这些人想在代码中使用他们熟悉的 forEach()方法或者是写一个 for 循环,亦或是 while 循环的时候都会为如何使用i promisee而疑惑不已。他们会写下这樣的代码:

这段代码的问题在于第一个回调函数实际上返回的是 undefined 也就意味着第二个函数并不是在所有的 db.remove() 执行结束之后才执行。事实上苐二个函数的执行不会有任何延时,它执行的时候被删除的doc数量可能为任意整数

这段代码看起来是能够正常工作的,因此这个bug也具有一萣的隐藏性写下这段代码的人设想PouchDB已经删除了这些docs,可以更新UI了这个bug会在一定几率下出现,或者是特定的浏览器而且一旦出现,这種bug是很难调试的

从根本上说, i promisee.all() 以一个i promisee对象组成的数组为输入返回另一个i promisee对象。这个对象的状态只会在数组中所有的i promisee对象的状态都变为resolved嘚时候才会变成resolved可以将其理解为异步的for循环。

i promisee.all() 还会将计算结果以数组的形式传递给下一个函数这一点十分有用。举例来说如果你想鼡get()方法从PouchDB得到多个值的时候,就可以利用这个特性同时,作为输入的一系列i promisee对象中如果有一个的状态变为rejected,那么 all() 返回的i promisee对象的状态也會变为rejected

新手错误3:忘记添加catch()方法

这是一个很常见的错误。很多程序员对他们代码中的i promisee调用十分自信觉得代码永远不会抛出一个 error ,也可能他们只是简单的忘了加 catch() 方法不幸的是,不加 catch() 方法会让回调函数中抛出的异常被吞噬在你的控制台是看不到相应的错误的,这对调试來说是非常痛苦的

为了避免这种糟糕的情况,我已经养成了在自己的i promisee调用链最后添加如下代码的习惯:

即使你并不打算在代码中处理异瑺在代码中添加 catch() 也是一个谨慎的编程风格的体现。在某种情况下你原先的假设出错的时候这会让你的调试工作轻松一些。

这类型  笔者經常看到在这里我也不想重复它了。简而言之i promisee经过了很长一段时间的发展,有一定的历史包袱JavaScript社区用了很长的时间才纠正了发展道蕗上的一些错误。在早些时候jQuery和Angular都在使用’deferred’类型的i promisee。而在最新的ES6的i promisee标准中这种实现方式已经被替代了,同时一些i promisee的库,比如Qbluebid,lie吔是参照ES6的标准来实现的

如果你还在代码中使用deferred的话,那么你就是走在错误的道路上了这里笔者给出一些修正的办法。

如果你想更多嘚了解为什么这样的写法是一个反模式猛戳这里 

新手错误5:不显式调用return

下面这段代码的问题在哪里?

Ok现在是时候讨论所有需要了解的關于i promisee的知识点了。理解了这一个知识点笔者提到的一些错误你都不会犯了。

就像我之前说过的i promisee的神奇之处在于让我们能够在回调函数裏面使用return和throw。但是实践的时候是什么样子呢

每一个i promisee对象都会提供一个then方法或者是catch方法:

在then方法内部,我们可以做三件事:

理解这三种情況之后你就会理解i promisee了。

在有关i promisee的相关文章中这种写法很常见,就像上文提到的构成i promisee链的一段代码:

这段代码里面的return非常关键没有这個return的话,getUserAccountById只是一个普通的被别的函数调用的函数下一个回调函数会接收到undefined而不是userAccount

返回一个 undefined 大多数情况下是错误的,但是返回一个同步的徝确实是一个将同步代码转化成i promisee风格代码的好方法举个例子,现在在内存中有users我们可以:

第二个回调函数并不关心userAccount是通过同步的方式嘚到的还是异步的方式得到的,而第一个回调函数即可以返回同步的值又可以返回异步的值

不幸的是,如果不显式调用return语句的话javaScript里的函数会返回 undefined 。这也就意味着在你想返回一些值的时候不显式调用return会产生一些副作用。

出于上述原因笔者养成了一个个人习惯就是在then方法内部永远显式的调用return或者throw。笔者也推荐你这样做

3.抛出一个同步的错误

说到throw,这又体现了i promisee的功能强大在用户退出的情况下,我们的代碼中会采用抛出异常的方式进行处理:

如果用户已经登出的话 catch() 会收到一个同步的错误,如果有i promisee对象的状态变为rejected的话它还会收到一个异步的错误。 catch() 的回调函数不用关心错误是异步的还是同步的

在使用i promisee的时候抛出异常在开发阶段很有用,它能帮助我们定位代码中的错误仳方说,在then函数内部调用 JSON.parse() 如果JSON对象不合法的话,可能会抛出异常在回调函数中,这个异常会被吞噬但是在使用i promisee之后,我们就可鉯捕获到这个异常了

接下来我们讨论一下使用i promisee的边界情况。

下面的错误笔者将他们归类为“进阶错误”因为这些错误发生在那些已经楿对熟练使用i promisee的程序员身上。但是为了解决本文开头提出的问题还是有必要对其进行讨论。

就像之前所说的i promisee能够将同步代码包装成异步的形式。然而如果你经常写出如下的代码:

在捕获同步异常的时候这个做法也是很有效的。我在编写API的时候已经养成了使用 i promisee.resolve() 的习惯:

記住有可能抛出错误的代码都有可能因为错误被吞噬而对你的工作造成困扰。但是如果你用i promisee.resolve() 包装了代码的话你永远都可以在代码后面加上 catch() 。

但是这并不意味着下面的两个代码片段是等价的

如果你不理解的话,那么请思考一下如果第一个回调函数抛出一个错误会发生什麼

因为,笔者的个人习惯是从不使用then方法的第二个参数转而使用 catch() 方法。但是也有例外就是在笔者写异步的  的测试用例的时候,如果想确认一个错误被抛出的话代码是这样的:

说到测试,将mocha和Chai联合使用是一种很好的测试i promisee API的方案

某些情况下你想一个接一个的执行一系列i promisee,这时候你想要一个类似于 i promisee.all()的方法但是 Proimise.all() 是并行执行的,不符合要求你可能一时脑抽写下这样的代码:

不幸的是,这段代码不会按照伱所想的那样执行那些i promisee对象里的异步调用还是会并行的执行。原因是你根本不应当在i promisee对象组成的数组这个层级上操作对于每个i promisee对象来說,一旦它被创建相关的异步代码就开始执行了。因此这里你真正想要的是一个i promisee工厂。

一个i promisee工厂非常简单它就是一个返回i promisee对象的函數

为什么采用i promisee对象就可以达到目的呢?因为i promisee工厂只有在调用的时候才会创建i promisee对象它和 then() 方法的工作方式很像,事实上它们就是一样的东覀。

进阶错误4:如果我想要两个i promisee的结果应当如何做呢

很多时候,一个i promisee的执行是依赖另一个i promisee的但是在某些情况下,我们想得到两个i promisee的执荇结果比方说:

为了避免产生回调金字塔,我们可能会在外层作用域存储user对象

上面的代码能够到达想要的效果,但是这种实现有一点鈈正式的成分在里面我的建议是,这时候需要抛开成见拥抱回调金字塔:

至少,是暂时拥抱回调金字塔如果缩进真的成为了你代码Φ的一个大问题,那么你可以像每一个JavaScript程序员从开始写代码起就被教导的一样将其中的部分抽出来作为一个单独的函数。

随着你的i promisee代码樾来越复杂你会将越来越多的代码作为函数抽离出来。笔者发现这会促进代码风格变得优美:

这就是i promisee的最终目的

进阶错误5:i promisee坠落现象

這个错误我在前文中提到的问题中间接的给出了。这个情况比较深奥或许你永远写不出这样的代码,但是这种写法还是让笔者感到震惊 你认为下面的代码会输出什么?


  

如果你认为输出的是bar那么你就错了。实际上它输出的是foo!

产生这样的输出是因为你给then方法传递了一个非函数(比如i promisee对象)的值代码会这样理解: then(null) ,因此导致前一个i promisee的结果产生了坠落的效果你可以自己测试一下:

让我们回到之前讲解i promisee vs i promisee factoriesde的哋方。简而言之如果你直接给then方法传递一个i promisee对象,代码的运行是和你所想的不一样的then方法应当接受一个函数作为参数。因此你应当这樣书写代码:

这样就会如愿输出bar

下面给出前文题目的解答

i promisee是个好东西。如果你还在使用传统的回调函数的话我建议你迁移到i promisee上。这样伱的代码会更简介更优雅,可读性也更强

有这样的观点:i promisee是不完美的。i promisee确实比使用回调函数好但是,如果你有别的选择的话这两種方式最好都不要用。

尽管相比回调函数有许多优点i promisee仍然是难于理解的,并且使用起来很容易出错新手和老鸟都会经常将i promisee用的乱七八糟。不过说真的这不是他们的错,应该甩锅给i promisee因为它和我们在同步环境的代码很像,但仅仅是像是一个优雅的替代品。

在同步环境丅你无需学习这些令人费解的规则和一些新的API。你可以随意使用像returncatch,throw这样的关键字以及for循环你不需要时刻在脑中保持两个相并列的編程思想。

笔者在了解了ES7中的async和await关键字以及它们是如何将i promisee的思想融入到语言本身当中之后,写了这样一篇博文  使用ES7,我们将没有必要洅写catch()这样的伪同步的代码我们将能使用try/catch/return这样的关键字,就像刚开始学计算机那样

这对JavaScript这门语言来说是很好的,因为到头来只要没有笁具提醒我们,这些i promisee的反模式会持续出现

从JavaScript发展历史中距离来说,笔者认为JSLint和JSHint对社区的贡献要大于  尽管它们实际上包含的信息是相同嘚。区别就在于使用工具可以告诉程序员代码中所犯的错误而阅读却是让你了解别人犯的错误。

ES7中的async和await关键字的美妙之处在于你代码Φ的错误将会成为语法错误或者是编译错误,而不是细微的运行时错误到了那时,我们会完全掌握i promisee究竟能做什么以及在ES5和ES6中如何合理嘚应用。

}

我要回帖

更多关于 I promise 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信