JavaScript进阶之路——认识和使用Promise,重构你的Js代码
一转眼,这2015年上半年就过去了,差不多一个月没有写博客了,"罪过罪过"啊~~。进入了七月份,也就意味着我们上半年苦逼的单身生活结束了,从此刻起,我们要打起十二分的精神,开始下半年的单身生活。大家一起加油~~
一直以来,JavaScript处理异步都是以callback的方式,在前端开发领域callback机制几乎深入人心。在设计API的时候,不管是浏览器厂商还是SDK开发商亦或是各种类库的作者,基本上都已经遵循着callback的套路。近几年随着JavaScript开发模式的逐渐成熟,CommonJS规范顺势而生,其中就包括提出了Promise规范,Promise完全改变了js异步编程的写法,让异步编程变得十分的易于理解。今天我们就来了解一下Promise~~
1、什么是promise?
Promise可能大家都不陌生,因为Promise规范已经出来好一段时间了,同时Promise也已经纳入了ES6,而且高版本的chrome、firefox浏览器都已经原生实现了Promise,只不过和现如今流行的类Promise类库相比少些API。
所谓Promise,字面上可以理解为“承诺”,就是说A调用B,B返回一个“承诺”给A,然后A就可以在写计划的时候这么写:当B返回结果给我的时候,A执行方案S1,反之如果B因为什么原因没有给到A想要的结果,那么A执行应急方案S2,这样一来,所有的潜在风险都在A的可控范围之内了。
Promise规范如下:
- 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
- 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
- promise必须实现
then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致 - then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。
2.promise原理分析
可以看到promise的规范并不是很多,下面我们一边分析promise一边自己写一个promise的实现。Promise实现的大致思路如下:
构造函数Promise接受一个函数resolver,可以理解为传入一个异步任务,resolver接受两个参数,一个是成功时的回调,一个是失败时的回调,这两参数和通过then传入的参数是对等的。
其次是then的实现,由于Promise要求then必须返回一个promise,所以在then调用的时候会新生成一个promise,挂在当前promise的_next上,同一个promise多次调用都只会返回之前生成的_next。
由于then方法接受的两个参数都是可选的,而且类型也没限制,可以是函数,也可以是一个具体的值,还可以是另一个promise。下面是then的具体实现:
Promise.prototype.then = function(resolve, reject) {
var next = this._next || (this._next = Promise());
var status = this.status;
var x;
if('pending' === status) {
isFn(resolve) && this._resolves.push(resolve);
isFn(reject) && this._rejects.push(reject);
return next;
}
if('resolved' === status) {
if(!isFn(resolve)) {
next.resolve(resolve);
} else {
try {
x = resolve(this.value);
resolveX(next, x);
} catch(e) {
this.reject(e);
}
}
return next;
}
if('rejected' === status) {
if(!isFn(reject)) {
next.reject(reject);
} else {
try {
x = reject(this.reason);
resolveX(next, x);
} catch(e) {
this.reject(e);
}
}
return next;
}
};
这里,then做了简化,其他promise类库的实现比这个要复杂得多,同时功能也更多,比如还有第三个参数——notify,表示promise当前的进度,这在设计文件上传等时很有用。对then的各种参数的处理是最复杂的部分,有兴趣的同学可以参看其他类Promise库的实现。
在then的基础上,应该还需要至少两个方法,分别是完成promise的状态从pending到resolved或rejected的转换,同时执行相应的回调队列,即resolve()和reject()方法。
到此,一个简单的promise就设计完成了,下面简单实现下两个promise化的函数:
function sleep(ms) {
return function(v) {
var p = Promise();
setTimeout(function() {
p.resolve(v);
});
return p;
};
};
function getImg(url) {
var p = Promise();
var img = new Image();
img.onload = function() {
p.resolve(this);
};
img.onerror = function(err) {
p.reject(err);
};
img.url = url;
return p;
};
由于Promise构造函数接受一个异步任务作为参数,所以getImg还可以这样调用:
function getImg(url) {
return Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
resolve(this);
};
img.onerror = function(err) {
reject(err);
};
img.url = url;
});
};
接下来(见证奇迹的时刻),假设有一个BT的需求要这么实现:异步获取一个json配置,解析json数据拿到里边的图片,然后按顺序队列加载图片,每张图片加载时给个loading效果,
function addImg(img) {
$('#list').find('> li:last-child').html('').append(img);
};
function prepend() {
$('<li>')
.html('loading...')
.appendTo($('#list'));
};
function run() {
$('#done').hide();
getData('map.json')
.then(function(data) {
$('h4').html(data.name);
return data.list.reduce(function(promise, item) {
return promise
.then(prepend)
.then(sleep(1000))
.then(function() {
return getImg(item.url);
})
.then(addImg);
}, Promise.resolve());
})
.then(sleep(300))
.then(function() {
$('#done').show();
});
};
$('#run').on('click', run)
这里的sleep只是为了看效果加的,可猛击查看demo!
在这里,Promise.resolve(v)静态方法只是简单返回一个以v为肯定结果的promise,v可不传入,也可以是一个函数或者是一个包含then方法的对象或函数(即thenable)。
类似的静态方法还有Promise.cast(promise),生成一个以promise为肯定结果的promise;Promise.reject(reason),生成一个以reason为否定结果的promise。
我们实际的使用场景可能很复杂,往往需要多个异步的任务穿插执行,并行或者串行同在。这时候,可以对Promise进行各种扩展,比如实现Promise.all(),接受promises队列并等待他们完成再继续,再比如Promise.any(),promises队列中有任何一个处于完成态时即触发下一步操作。
3.标准的Promise
可参考html5rocks的这篇文章JavaScript Promises,目前高级浏览器如Chrome、Firefox都已经内置了Promise对象,提供更多的操作接口,比如Promise.all(),支持传入一个promises数组,当所有promises都完成时执行then,还有就是更加友好强大的异常捕获,应对日常的异步编程,应该足够了。
现今流行的各大js库,几乎都不同程度的实现了Promise,如dojo,jQuery、Zepto、when.js、Q等,只是暴露出来的大都是Deferred对象,当然还有angularJs中的$q.这里以jQuery为例,说一下:
// animate
$('.box')
.animate({'opacity': 0}, 1000)
.promise()
.then(function() {
console.log('done');
}); // ajax
$.ajax(options).then(success, fail);
$.ajax(options).done(success).fail(fail); // ajax queue
$.when($.ajax(options1), $.ajax(options2))
.then(function() {
console.log('all done.');
}, function() {
console.error('There something wrong.');
});
上面我们了解了Promise,相信大家对Promise有了一定的认识。下面我们开始动手来写代码,通过几个简单的例子,来加深理解。这里我们使用浏览器自带的Promise,首先我们要先检测一些浏览器是否支持Promise,其实很简单,如果是谷歌浏览器,按下F12,打开控制台,如图:

这里我们可以看到Promise的type是function,也就是说谷歌浏览器是支持promise的。以此为原理,我们可以写一段JavaScript代码来检测,代码如下:
if(typeof(Promise) === "function"){
alert("支持Promise");
}
else{
alert("不支持Promise");
}
经过检测,发现IE11竟然不支持promise.建议大家用谷歌浏览器来进行测试吧。
我们首先来写一个等待的方法,如下:
function wait(duration){
return new Promise(function(resolve, reject) {
setTimeout(resolve,duration);
})
}
测试这个方法的代码如下:wait(5000).then(function(){alert('hello')}),这段代码很简单,就是等待5秒以后执行一个回调,弹出一个消息。当然,你还可以这样写:
wait(5000).then(function(){alert('hello')}).then(function(){console.log('world')})
怎么样?很简单吧~~
下面来看一些我从网上收集的一些常用的JavaScript的promise的写法:
function get(uri){
return http(uri, 'GET', null);
}
function post(uri,data){
if(typeof data === 'object' && !(data instanceof String || (FormData && data instanceof FormData))) {
var params = [];
for(var p in data) {
if(data[p] instanceof Array) {
for(var i = 0; i < data[p].length; i++) {
params.push(encodeURIComponent(p) + '[]=' + encodeURIComponent(data[p][i]));
}
} else {
params.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
}
}
data = params.join('&');
}
return http(uri, 'POST', data || null, {
"Content-type":"application/x-www-form-urlencoded"
});
}
function http(uri,method,data,headers){
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method,uri,true);
if(headers) {
for(var p in headers) {
xhr.setRequestHeader(p, headers[p]);
}
}
xhr.addEventListener('readystatechange',function(e){
if(xhr.readyState === 4) {
if(String(xhr.status).match(/^2\d\d$/)) {
resolve(xhr.responseText);
} else {
reject(xhr);
}
}
});
xhr.send(data);
})
}
function wait(duration){
return new Promise(function(resolve, reject) {
setTimeout(resolve,duration);
})
}
function waitFor(element,event,useCapture){
return new Promise(function(resolve, reject) {
element.addEventListener(event,function listener(event){
resolve(event)
this.removeEventListener(event, listener, useCapture);
},useCapture)
})
}
function loadImage(src) {
return new Promise(function(resolve, reject) {
var image = new Image;
image.addEventListener('load',function listener() {
resolve(image);
this.removeEventListener('load', listener, useCapture);
});
image.src = src;
image.addEventListener('error',reject);
})
}
function runScript(src) {
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = src;
script.addEventListener('load',resolve);
script.addEventListener('error',reject);
(document.getElementsByTagName('head')[0] || document.body || document.documentElement).appendChild(script);
})
}
function domReady() {
return new Promise(function(resolve, reject) {
if(document.readyState === 'complete') {
resolve();
} else {
document.addEventListener('DOMContentLoaded',resolve);
}
})
}
看到了吧,Promise风格API跟回调风格的API不同,它的参数跟同步的API是一致的,但是它的返回值是个Promise对象,要想得到真正的结果,需要在then的回调里面拿到。
在比较复杂的页面中,我们会使用到大量的异步操作。我们来看看使用Promise会带来怎样的便利吧~~
1、多个异步调用,同步/并行
例如我们页面调用了好几个异步函数,我们要等待所有的异步函数执行完成后,做一些操作,如弹出一个消息框提示用户操作成功。下面我们拿一个例子来说明一下:
Promise.all跟then的配合,可以视为调用部分参数为Promise提供的函数。譬如,我们现在有一个接受三个参数的函数:
function print(a, b, c) {
console.log(a + b + c);
}
现在我们调用print函数,其中a和b是需要异步获取的:
var c = 10; print(geta(), getb(), 10); //这是同步的写法 Promise.all([geta(), getb(), 10]).then(print); //这是 primise 的异步写法
如果用callback的话,我们就只能一个一个调用了,调用完了geta,然后在其回调函数里面调用getb,最后在getb的回调函数中调用print方法。串行和并行哪个更快,大家很清楚吧~~
2.竞争
如果说Primise.all是promise对象之间的“与”关系,那么Promise.race就是promise对象之间的“或”关系。比如,我要实现“点击按钮或者5秒钟之后执行”:
var btn = document.getElementsByTagName('button');
Promise.race(wait(5000), waitFor(btn, click)).then(function(){
console.log('run!')
})
3.异常处理
异常处理一直是回调的难题,而promise提供了非常方便的catch方法:在一次promise调用中,任何的环节发生reject,都可以在最终的catch中捕获到:
Promise.resolve().then(function(){
return loadImage(img1);
}).then(function(){
return loadImage(img2);
}).then(function(){
return loadImage(img3);
}).catch(function(err){
//错误处理
})
4.复杂流程
接下来,我们来看比较复杂的情况。
promise有一种非常重要的特性:then的参数,理论上应该是一个promise函数,而如果你传递的是普通函数,那么默认会把它当做已经resolve了的promise函数。
这样的特性让我们非常容易把promise风格的函数跟已有代码结合起来。
为了方便传参数,我们编写一个currying函数,这是函数式编程里面的基本特性,在这里跟promise非常搭,所以就实现一下:
function currying(){
var f = arguments[0];
var args = Array.prototype.slice.call(arguments,1);
return function(){
args.push.apply(args,arguments);
return f.apply(this,args);
}
}
currying会给某个函数"固化"几个参数,并且返回接受剩余参数的函数。比如之前的函数,可以这么玩:
var print2 = currying(print,11);
print2(2, 3); //得到 11 + 2 + 3 的结果,16
var wait1s = currying(wait,1000);
wait1s().then(function(){
console.log('after 1s!');
})
有了currying,我们就可以愉快地来玩链式调用了,比如以下代码:
Promise.race([
domReady().then(currying(wait,5000)),
waitFor(btn, click)])
.then(currying(runScript,'a.js'))
.then(function(){
console.log('loaded');
return Promise.resolve();
});
我们看到,不管Promise实现怎么复杂,但是它的用法却很简单,组织的代码很清晰,从此不用再受callback的折磨了。promise作为一个新的API,它的API本身没有什么特别的功能,但是它背后代表的编程思路是很有价值的。
最后,Promise是如此的优雅!但Promise也只是解决了回调的深层嵌套的问题,真正简化JavaScript异步编程的还是Generator,在Node.js端,建议考虑Generator。
JavaScript Promise迷你书(中文版) http://liubin.github.io/promises-book/
JavaScript Promise启示录 http://www.csdn.net/article/2014-05-28/2819979-JavaScript-Promise
用Promise组织程序 http://www.w3ctech.com/topic/721
作者:雲霏霏
QQ交流群:243633526
博客地址:http://www.cnblogs.com/yunfeifei/
声明:本博客原创文字只代表本人工作中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未授权,贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文连接。
如果大家感觉我的博文对大家有帮助,请推荐支持一把,给我写作的动力。
JavaScript进阶之路——认识和使用Promise,重构你的Js代码的更多相关文章
- JavaScript进阶之路(一)初学者的开始
一:写在前面的问题和话 一个javascript初学者的进阶之路! 背景:3年后端(ASP.NET)工作经验,javascript水平一般般,前端水平一般般.学习资料:犀牛书. 如有误导,或者错误的地 ...
- JavaScript进阶之路 初学者的开始
一:写在前面的问题和话 一个javascript初学者的进阶之路! 背景:3年后端(ASP.NET)工作经验,javascript水平一般般,前端水平一般般.学习资料:犀牛书. 如有误导,或者错误的地 ...
- Javascript进阶之路-论对象的重要性
要了解JavaScript对象,我们可以从对象创建.属性操作.对象方法这几个方面入手.概括起来,包括以下几模块: 1.创建对象 1.1 对象直接量 1.2 通过new创建对 ...
- Javascript 进阶 面向对象编程 继承的一个样例
Javascript的难点就是面向对象编程,上一篇介绍了Javascript的两种继承方式:Javascript 进阶 继承.这篇使用一个样例来展示js怎样面向对象编程.以及怎样基于类实现继承. 1. ...
- Javascript 进阶 面向对象编程 继承的一个例子
Javascript的难点就是面向对象编程,上一篇介绍了Javascript的两种继承方式:Javascript 进阶 继承,这篇使用一个例子来展示js如何面向对象编程,以及如何基于类实现继承. 1. ...
- 处女作《Web全栈开发进阶之路》出版了!
书中源码下载地址:https://github.com/qinggee/WebAdvanced 01. 当初决定写博客的原因非常的纯洁:只要每个月写上 4 篇以上博客,月底的绩效奖金就多 500 块. ...
- 2017PHP程序员的进阶之路
2017PHP程序员的进阶之路 又是一年毕业季,可能会有好多毕业生即将进入开发这个圈子,踏上码农这个不归路.根据这些年在开发圈子总结的LNMP程序猿发展轨迹,结合个人经验体会,总结出很多程序员对未来的 ...
- GO语言的进阶之路-初探GO语言
GO语言的进阶之路-初探GO语言 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.为什么我们需要一门新语言 Go语言官方自称,之所以开发Go 语言,是因为“近10年来开发程序之难 ...
- Scala进阶之路-Scala中的Ordered--Ordering
Scala进阶之路-Scala中的Ordered--Ordering 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 说道对象的比较,在Java中大家最熟悉不过的就是实现类本身实 ...
随机推荐
- CSS3 background-image背景图片相关介绍
这里将会介绍如何通过background-image设置背景图片,以及背景图片的平铺.拉伸.偏移.设置大小等操作. 1. 背景图片样式分类 CSS中设置元素背景图片及其背景图片样式的属性主要以下几个: ...
- 微信小程序开发心得
微信小程序也已出来有一段时间了,最近写了几款微信小程序项目,今天来说说感受. 首先开发一款微信小程序,最主要的就是针对于公司来运营的,因为,在申请appid(微信小程序ID号)时候,需要填写相关的公司 ...
- 谈谈如何使用Netty开发实现高性能的RPC服务器
RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络,从远程计算机程序上请求服务,而不必了解底层网络技术的协议.说的再直白一点,就是客户端在不必知道 ...
- Socket聊天程序——Common
写在前面: 上一篇记录了Socket聊天程序的客户端设计,为了记录的完整性,这里还是将Socket聊天的最后一个模块--Common模块记录一下.Common的设计如下: 功能说明: Common模块 ...
- 掌握javascript中的最基础数据结构-----数组
这是一篇<数据结构与算法javascript描述>的读书笔记.主要梳理了关于数组的知识.部分内容及源码来自原作. 书中第一章介绍了如何配置javascript运行环境:javascript ...
- 【Big Data】HADOOP集群的配置(一)
Hadoop集群的配置(一) 摘要: hadoop集群配置系列文档,是笔者在实验室真机环境实验后整理而得.以便随后工作所需,做以知识整理,另则与博客园朋友分享实验成果,因为笔者在学习初期,也遇到不少问 ...
- iOS架构一个中型普通App的一些经验总结
这一版比较完善的的App终于提交审核了.有时间写写自己的一些经验的总结了.自己主导的从0到比较成型的app到目前来说也只有两个,但是其中的很多东西都是大同小异.基本上是想到了什么就写什么,感觉写的不到 ...
- C#反序列化XML异常:在 XML文档(0, 0)中有一个错误“缺少根元素”
Q: 在反序列化 Xml 字符串为 Xml 对象时,抛出如下异常. 即在 XML文档(0, 0)中有一个错误:缺少根元素. A: 首先看下代码: StringBuilder sb = new Stri ...
- C#语法糖大汇总
首先需要声明的是"语法糖"这个词绝非贬义词,它可以给我带来方便,是一种便捷的写法,编译器会帮我们做转换:而且可以提高开发编码的效率,在性能上也不会带来损失.这让java开发人员羡慕 ...
- cesium核心类Viewer简介
1.简单描述Viewer Viewer类是cesium的核心类,是地图可视化展示的主窗口,cesium程序应用的切入口,扮演必不可少的核心角色. 官网的英文解析如下: A base widget fo ...