天天看点

Promise的前世今生和妙用技巧

JavaScript作为单线程运行于浏览器之中,这是每本JavaScript教科书中都会被提到的。同时出于对UI线程操作的安全性考虑,JavaScript和UI线程也处于同一个线程中。因此对于长时间的耗时操作,将会阻塞UI的响应。为了更好的UI体验,应该尽量的避免JavaScript中执行较长耗时的操作(如大量for循环的对象diff等)或者是长时间I/O阻塞的任务。所以在浏览器中的大多数任务都是异步(无阻塞)执行的,例如:鼠标点击事件、窗口大小拖拉事件、定时器触发事件、Ajax完成回调事件等。当每一个异步事件完成时,它都将被放入一个叫做”浏览器事件队列“中的事件池中去。而这些被放在事件池中的任务,将会被javascript引擎单线程处理的一个一个的处理,当在此次处理中再次遇见的异步任务,它们也会被放到事件池中去,等待下一次的tick被处理。另外在HTML5中引入了新的组件-Web Worker,它可以在JavaScript线程以外执行这些任务,而不阻塞当前UI线程。

浏览器中的事件循环模型如下图所示:

Promise的前世今生和妙用技巧

由于浏览器的这种内部事件循环机制,所以JavaScript一直以callback回调的方式来处理事件任务。因此无所避免的对于多个的JavaScript异步任务的处理,将会遇见”callback hell“(回调地狱),使得这类代码及其不可读和难易维护。

Promise的前世今生和妙用技巧

因此很多JavaScript牛人开始寻找解决这回调地狱的模式设计,随后Promise(jQuery的<code>deferred</code>也属于Promise范畴)便被引入到了JavaScript的世界。Promise在英语中语义为:”承诺“,它表示如A调用一个长时间任务B的时候,B将返回一个”承诺“给A,A不用关心整个实施的过程,继续做自己的任务;当B实施完成的时候,会通过A,并将执行A之间的预先约定的回调。而deferred在英语中语义为:”延迟“,这也说明promise解决的问题是一种带有延迟的事件,这个事件会被延迟到未来某个合适点再执行。

Promise 对象有三种状态: Pending – Promise对象的初始状态,等到任务的完成或者被拒绝;Fulfilled – 任务执行完成并且成功的状态;Rejected – 任务执行完成并且失败的状态;

Promise的状态只可能从“Pending”状态转到“Fulfilled”状态或者“Rejected”状态,而且不能逆向转换,同时“Fulfilled”状态和“Rejected”状态也不能相互转换;

Promise对象必须实现then方法,then是promise规范的核心,而且then方法也必须返回一个Promise对象,同一个Promise对象可以注册多个then方法,并且回调的执行顺序跟它们的注册顺序一致;

then方法接受两个回调函数,它们分别为:成功时的回调和失败时的回调;并且它们分别在:Promise由“Pending”状态转换到“Fulfilled”状态时被调用和在Promise由“Pending”状态转换到“Rejected”状态时被调用。

如下面所示:

Promise的前世今生和妙用技巧

Promise将原来回调地狱中的回调函数,从横向式增加巧妙的变为了纵向增长。以链式的风格,纵向的书写,使得代码更加的可读和易于维护。

Promise在JavaScript的世界中逐渐的被大家所接受,所以在ES6的标准版中已经引入了Promise的规范了。现在通过Babel,可以完全放心的引入产品环境之中了。

如上所说Promise在处理异步回调或者是延迟执行任务时候,是一个不错的选择方案。下面我们将介绍一些Promise的使用技巧(下面将利用Angular的<code>$q</code>和<code>$http</code>为例,当然对于jQuery的<code>deferred</code>,ES6的Promise仍然实用):

在上文中提到的回调地狱案例,就是一种试图去将多个异步的任务串行处理的结果,使得代码不断的横向延伸,可读性和维护性急剧下降。当然我们也提到了Promise利用链式和延迟执行模型,将代码从横向延伸拉回了纵向增长。使用Angular中<code>$http</code>的实现如下:

因为Promise是可以传递的,可以继续<code>then</code>方法延续下去,也可以在纵向扩展的途中改变为其他Promise或者数据。所以在例子中的$http也可以换为其他的Promise(如$<code>timeout</code>,<code>$resource</code> …)。

在有些场景下,我们所要处理的多个异步任务之间并没有像上例中的那么强的依赖关系,只需要在这一系列的异步任务全部完成的时候执行一些特定逻辑。这个时候为了性能的考虑等,我们不需要将它们都串行起来执行,并行执行它们将是一个最优的选择。如果仍然采用回调函数,则这是一个非常恼人的问题。利用Promise则同样可以优雅的解决它:

这样就可以等到一堆异步的任务完成后,在执行特定的业务回调了。在Angular中的路由机制<code>ngRoute</code>、<code>uiRoute</code>的resolve机制也是采用同样的原理:在路由执行的时候,会将获取模板的Promise、获取所有resolve数据的Promise都拼接在一起,同时并行的获取它们,然后等待它们都结束的时候,才开始初始化<code>ng-view</code>、<code>ui-view</code>指令的scope对象,以及compile模板节点,并插入页面DOM中,完成一次路由的跳转并且切换了View,将静态的HTML模板变为动态的网页展示出来。

Angular路由机制的伪代码如下:

有了Promise的处理,因为在前端代码中最多的异步处理就是Ajax,它们都被包装为了Promise .then的风格。那么对于一部分同步的非异步处理呢?如localStorage、setTimeout、setInterval之类的方法。在大多数情况下,博主仍然推荐使用Promise的方式包装,使得项目Service的返回接口统一。这样也便于像上例中的多个异步任务的串行、并行处理。在Angular路由中对于只设置template的情况,也是这么处理的。

对于setTimeout、setInterval在Angular中都已经为我们内置了$timeout和$interval服务,它们就是一种Promise的封装。对于localStorage呢?可以采用$q.when方法来直接包装localStorage的返回值的为Promise接口,如下所示:

整个项目的Service层的返回值都可以被封装为统一的风格使用了,项目变得更加的一致和统一。在需要多个Service同时并行或者串行处理的时候,也变得简单了,一致的使用方式。

在前面已经提到Promise是延迟到未来执行某些特定任务,在调用时候则给消费者返回一个”承诺“,消费者线程并不会被阻塞。在消费者接受到”承诺“之后,消费者就不用再关心这些任务是如何完成的,以及督促生产者的任务执行状态等。直到任务完成后,消费者手中的这个”承诺“就被兑现了。

对于这类延迟机制,在前端的UI交互中也是极其常见的。比如模态窗口的显示,对于用户在模态窗口中的交互结果并不可提前预知的,用户是点击”ok“按钮,或者是”cancel“按钮,这是一个未来将会发生的延迟事件。对于这类场景的处理,也是Promise所擅长的领域。在Angular-UI的Bootstrap的modal的实现也是基于Promise的封装。

这是因为modal在open方法的返回值中给了我们一个Promise的result对象(承诺)。等到用户在模态窗口中点击了ok按钮,则Bootstrap会使用<code>$q</code>的<code>defer</code>来<code>resolve</code>来执行ok事件;相反,如果用户点击了cancel按钮,则会使用<code>$q</code>的<code>defer</code>来<code>reject</code>执行cancel事件。

这样就很好的解决了延迟触发的问题,也避免了<code>callback的地狱</code>。我们仍然可以进一步将其返回值语义化,以业务自有的术语命名而形成一套DSL API。

则我们可以如下方式来访问它:

是不是感觉更具有语义呢?在Angular中$http的返回方法success、error也是同样逻辑的封装。将success的注册函数注册为.then方法的成功回调,error的注册方法注册为then方法的失败回调。所以success和error方法只是Angular框架为我们在Promise语法之上封装的一套语法糖而已。

Angular的success、error回调的实现代码:

在Angular中同样也内置了一些AOP的设计思想,便于实现程序通用功能与业务模块的分离、解耦、统一处理和维护。$http中的拦截器(interceptors)和装饰器($provide.decorator)是Angular中两类常见的AOP切入点。前者以管道式执行策略实现,而后者则通过运行时的Promise管道动态实现的。

首先回顾一下Angular的拦截器实现方式:

这样就可以实现对Angular中的<code>$http</code>或者是<code>$resource</code>的Ajax请求拦截了。但在Angular内部是是如何实现这种拦截方式的呢?Angular使用的就是Promise机制,形成异步管道流,将真实的Ajax请求放置在request、requestError和response、responseError的管道中间,因此就产生了对Ajax请求的拦截。

其源码实现如下:

在上面紧接着在<code>$get</code>注入方法之后,Angular会将<code>interceptors</code>和<code>responseInterceptors</code>反转合并到一个<code>reversedInterceptors</code>的拦截器内部变量中保存。最后在$http函数中以<code>[serverRequest, undefined]</code>(<code>serverRequest</code>是Ajax请求的Promise操作)为中心,将<code>reversedInterceptors</code>中的所有拦截器函数依次加入到chain链式数组中。如果是request或requestError,那么就放在链式数组起始位置;相反如果是response或responseError,那么就放在链式数组最后。

注意添加在chain的request和requestError或者response和responseError都一定是成对的,换句话说可能注册一个非空的request与一个为undefined的requestError,或者是一个为undefined的request与非空的requestError。就像chain数组的声明一样<code>(var chain = [serverRequest, undefined];)</code>,成对的放入serverRequest和undefined对象到数组中。因为后面的代码将利用Promise的机制注册这些拦截器函数,实现管道式AOP拦截机制。

在Promise中需要两个函数来注册回调,分别为成功回调和失败回调。在这里request和response会被注册成Promise的成功回调,而requestError和responseError则会注册成Promise的失败回调。所以在chain中添加的request和requestError,response或responseError都是成对出现的,这是为了能在接下来的循环中简洁地注册Promise回调函数。 这些被注册的拦截器链,会通过<code>$q.when(config)</code> Promise启动,它会首先传入<code>$http</code>的config对象,并执行所有的request拦截器,依次再到<code>serverRequest</code>这个Ajax请求,此时将挂起后边所有的response拦截器,直到Ajax请求响应完成,再依次执行剩下的response拦截器回调; 如果在request过程中有异常失败则会执行后边的requestError拦截器,对于Ajax请求的失败或者处理Ajax的response拦截器的异常也会被后面注册的responseError拦截器捕获。

从最后两段代码也能了解到关于<code>$http</code>服务中的success方法和error方法,是Angular为大家提供了一种Promise的便捷写法。success方法是注册一个传入的成功回调和为undefined的错误回调,而error则是注册一个为null的成功回调和一个传入的失败回调。

另外,同时也欢迎关注博主的微信公众号[破狼](微信二维码位于博客右侧),这里将会为大家地时间推送博主的最新博文,谢谢大家的支持和鼓励。

本文转自破狼博客园博客,原文链接:http://www.cnblogs.com/whitewolf/p/promise-best-practice.html,如需转载请自行联系原作者

继续阅读