JavaScript中的承诺——Promise
一、JavaScript单线程
JavaScript运行在浏览器中,是以单线程的方式运行的;JavaScript为什么不选择提高整个应用性能和吞吐量实现应用并行的多线程呢?
这在JavaScript的产生就决定了:当初JavaScript是用来处理用户和页面的交互,以及操作DOM树,CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果是多线程的来操作DOM树,则会发生预想不到的冲突,一个线程A想要删除DOM节点,另外一个线程B想要修改这个DOM节点,这样就会导致浏览器不知道听取哪一个操作。所以JavaScript从诞生就决定了是单线程方式运行的。
因为单线程的执行,所以在同一时间内只能执行一个特定的任务,并且会阻塞其他任务。所以对于耗时很长的任务来说,例如对于I/O设备的访问,其实并没有必要等待它的完成,完全可以在执行这项任务之前,JavaScript去执行另外一个任务,直到I/O任务执行完成后再继续执行该任务的处理就行了。所以JavaScript对于这种耗时操作都会被处理为异步操作,以及回调注册机制,等到这些任务完成后就将后续的处理操作封装为JavaScript任务放入执行任务队列中,等待JavaScript线程空闲时去执行,因此这里就有了“浏览器事件循环”的机制
二、浏览器事件循环和回调机制
JavaScript很多任务都是异步的,包括键盘、鼠标I/O输入输出事件、Ajax请求网络I/O回调等。当这些异步的任务发生时,它们都会被放入浏览器事件任务队列中。在浏览器中有一个叫做消息循环池(Event Loop),JavaScript引擎在运行时候单线程的处理这些任务,它们会被放入在这个事件循环池中,需要等到JavaScript运行时执行线程空闲时候才会按照队列先进先出的原则被一一执行。但是由于此时JavaScript主线程也许并不空闲,所以它们不会被立即执行。
虽然JavaScript是单线程执行的,但是浏览器是多线程执行的,浏览器有JavaScript的执行线程、UI节点渲染线程,图片加载线程以及Ajax请求线程等等,在Chrome设计中存在很多的进程,并利用进程间通讯来完成它们之间的同步,因此这也是Chrome快速的法宝之一。对于Ajax的请求也需要特殊线程来执行,当需要发送一个Ajax请求的时候,浏览器会开辟一个新的线程来执行HTTP的请求,它并不会阻塞JavaScript线程的执行,HTTP请求状态变更事件会被作为回调放入到浏览器的事件队列中等待被执行。

三、异步出现的问题
由上我们可以知道,JavaScript的很多任务都是异步处理的,以callback回调的方式处理事件任务,但是对于多个JavaScript异步任务的处理,将会碰到如下情况:
1 function1(data, function (data1){ 2 3 function2(data1, function (data2){ 4 5 function3(data2, function (data3){ 6 // .... 一层套一层的回调 7 }); 8 9 }); 10 11 });
我们把这种每一层的回调函数都需要依赖上一层的回调执行完,所以形成了层层嵌套的关系,这样的现象叫做“回调地狱”,这样的代码非常不易于阅读与维护,为了解决这个问题,”Promise“出现了!
四、Promise的出现
1)Promise被翻译为”承诺“,它表示如果A调用了一个长时间的B任务的时候,B将会返回一个”承诺“给A,A就不用关心整个实施的过程,继续做自己的任务;当B实施完成的时候,会通过A,并将执行A之间的预先约定好的回调函数;Promise解决的问题是一种带有延迟的事件,这个事件会被延迟到未来某个合适的时间点在执行。
2)Promise有三种状态,分别是:Pending——初始状态,等到任务的完成或者拒绝;Fullfilled——任务执行完成并且成功状态;Rejected——任务执行完成并且失败的状态
3)Promise对象必须实现then方法,then是Promise规范的核心,而且then方法也必须返回一个Promise对象,同一个Promise对象可以注册多个then方法,并且回调的执行顺序跟他们注册的顺序一致
4)then方法接受两个回调函数:分别是成功时的回调和失败时候的回调;并且它们分别在:Promise由“Pending”状态转换到“Fulfilled”状态时被调用和在Promise由“Pending”状态转换到“Rejected”状态时被调用。
所以上述代码,使用Promise可以转换为:
function1(data)
.then(function(data1){
return function2(data1);
})
.then(function(data2){
return function3(data2);
})
// 仍然可以继续then方法
Promise将原来回调地狱中的回调函数,从横向式增加变味了纵向增长。以链式的风格,使得代码更加可读和维护
五、Promise的使用
1)多个异步任务的串行处理
使用Angular中$http的实现:
$http.get('/demo1')
.then(function(data){
console.log('demo1', data);
return $http.get('/demo2', {params: data.result});
})
.then(function(data){
console.log('demo2', data);
return $http.get('/demo3', {params: data.result});
})
.then(function(data){
console.log('demo3', data.result);
});
then方法可以一直延续下去,也可以在纵向扩展的途中改变为其他Promise的数据;
2)多个异步任务的并行处理
很多场景下,我们需要处理的多个异步任务并没有那么强的依赖关系,只需要在这一系列的异步任务全部完成的时候执行一些特定的逻辑。这个时候为了性能的考虑,我们不需要将他们串行执行,选择并行是一个更好的选择。如果仍采用回调函数,则用Promise就可以解决
$q.all([$http.get('/demo1'),
$http.get('/demo2'),
$http.get('/demo3')
])
.then(function(results){
console.log('result 1', results[0]);
console.log('result 2', results[1]);
console.log('result 3', results[2]);
});
这样就可以等到一堆异步任务完成后,在执行特定的业务回调了。
PS:在Angular中的路由机制ngRoute、uiRoute的resolve机制也是采用同样的原理:在路由执行的时候,会将获取模板的Promise、获取所有resolve数据的Promise都拼接在一起,同时并行的获取它们,然后等待它们都结束的时候,才开始初始化ng-view、ui-view指令的scope对象,以及compile模板节点,并插入页面DOM中,完成一次路由的跳转并且切换了View,将静态的HTML模板变为动态的网页展示出来。
3)对于同步数据的Promise处理,统一调用接口
4)对于延迟任务的Promise DSL语义化封装
5)利用Promise来实现管道式AOP拦截
六、参考资料
1.http://greengerong.com/blog/2015/10/27/javascript-single-thread-and-browser-event-loop/
2.http://greengerong.com/blog/2015/10/22/promisede-miao-yong/

浙公网安备 33010602011771号