回调是JavaScript异步编程一种常用的手段,但这种手段有着局限性。
1)使用多个回调或者嵌套回调会使代码变得非常复杂,难以理解和维护。
2)如果你创建的回调函数在你使用的第三方插件或函数中调用,这会带来信任问题,比如有一天第三方API发生改变,(比如异步请求,5s内服务器没有响应会重新发送请求)你的回调可能被调用多次,而更加糟糕的是如果第三方函数是与支付相关的接口,用户可能会对同一件商品多次付款,而为了解决多次重复回调,你会在代码中作出对应的判断以保证回调最多触发一次。这又会使代码变得复杂。
在上面提到的情景中,你将自己所写代码的控制权交给了第三方,由服务器接受AJAX请求然后响应的结果来执行你的回调函数或者多个回调函数(但某个时间只能执行一个回调函数)。这是不安全的。
为了解决传统异步编程回调函数所带来的不便,我们不妨来尝试一种相对较新的异步编程思路或者方法Promise,说相对较新是因为很多开发者还是在使用传统的回调来进行异步编程,相对是因为Promise这个概念兴起已久而今也已正式写入ES6规范当中。
学习Promise之前你需要明确两点。
1)回调函数是异步编程的一种方式或者手段。
2)你利用Promise来进行异步编程,也会用到回调,不是说Promise中没有回调。
在说具体的概念之前,我们先看一个例子。
先简单描述场景:
在php文件中,有一个多维数组(这里没有用数据库)来保存员工信息。
输入员工编号,点击查询按钮看数据库(这里是前面创建的多维数组)中是否有响应编号的员工。并把查询的结果展示在页面上。
。
用传统的Ajax结合回调函数,你一定能很熟练解决上述需求,这里我们来看看用Promise
是如何实现的。
请看下面代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>staff system management</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="wrapper">
<div class="memberqueryWrapper">
<h3>员工查询</h3>
<label for="keyword">员工编号 </label>
<input type="text" name="staffNumber" id="keyword" placeholder="请输入三位数员工编号" />
<br/>
<button id="search">查询</button>
<p id="searchResult"></p>
</div>
<hr/>
<div class="memberRegisterWrapper">
<h3>员工创建</h3>
<label for="staffName">员工姓名</label>
<input type="text" name="" id="staffName" placeholder="请输入员工姓名">
<br/>
<label for="staffNumber">员工编号</label>
<input type="text" name="" id="staffNumber" placeholder="请输入员工编号">
<br/>
<label for="staffJob">员工职位</label>
<input type="text" name="" id="staffJob" placeholder="请输入员工职位">
<br/>
<label for="staffSex">员工性别</label>
<select id="staffSex">
<option >男</option>
<option >女</option>
</select>
<br/>
<button id="save">登记</button>
<p id="createResult"></p>
</div>
</div>
</body>
<script type="text/javascript">
let _$ = function(id) { //简化取得DOM
return document.getElementById(id);
}
const getJSON = function(url) { //getJSON封装AJAX操作,并返回一个Promise对象
const promise = new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onreadystatechange = handler;
function handler() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
return resolve(JSON.parse(this.response));
} else {
return reject(new Error(JSON.parse(this.statusText)));
}
}
});
return promise;
}
_$('search').onclick = function() {
let url = 'serverjson.php?number=' + _$('keyword').value;
getJSON(url)
.then(function(data) {
_$('searchResult').innerHTML = data.msg;
}, function(error) {
_$('searchResult').innerHTML = "出现错误:";
});
}
</script>
</html>
在<script>..</script>
标签中创建了getJSON
方法,getJSON
创建了一个Promise对象并返回这个promise
对象。promise对象中封装了AJAX请求。
按钮的点击事件调用getJSON方法,向服务器发送AJAX请求,请求访问php文件并期望得到返回结果。then方法对返回结果作出判断,请求成功了如何做?请求失败了如何做?你可能会疑惑上面then
中data
和error
是获取到的?
这里就涉及到Promise的核心思想,举例来说,后天就是元旦了,你想去吃羊肉火锅,但你晚上去的比较晚,你告诉火锅店店员你要吃羊肉火锅(把店员当作服务器?)这是发送一个请求,但是需要排队,这s时店员会给你一个号码小票,当喊道你的号码时你就可以坐在位子上享受羊肉火锅了,在等待期间你可以做其他事情,比如聊天,比如玩手机等。
这里的号码小票相当于羊肉火锅,你现在手里拿着小票表示我会在将来的某个时间(排到你的时候)能吃到羊肉火锅。所以你可以把小票当作店员对你的一种承诺,承诺你能吃到羊肉火锅(将来的羊肉火锅)。
而Promise也是这样,用一个承诺值代替将来值,这个将来值是异步操作返回的结果,你用这个承诺值来进行逻辑处理,当一旦异步请求完成后能取到将来值时,承诺值就会被期待值取代。所以上面的data
和error
你可以理解为承诺值,对应的是将来值AJAX异步请求返回的结果(data为请求成功返回的结果,error为请求失败返回的结果)。
使用承诺值来代替将来值我认为这是Promise最核心的一点。因为有了承诺值你就可以使用它而不用傻傻的等待(等待时间可能很长,可能很短,但终究是等待不是吗?)将获得将来值来进行处理。就像面的例子一样,点击事件发送一个AJAX请求,然后立即使用承诺值进行下一步的处理。
如果用回调函数来进行异步操作,你需要创建一个或多个回调函数针对不同的响应结果进行逻辑处理。
看了上面的例子你可能大概明白Promise是如何工作的,请记住承诺值代替将来值这句话。下面让我们学习更加细节的东西。
1.Promise对象的状态
Promise对象代表一个异步操作,对象的状态代表异步操作的状态
pending
: 进行中fulfilled
:已成功rejected
:已失败
2.构造Promise对象
const promise = new Promise( (resolve,reject) => {..});
Promise构造函数接受一个函数作为参数,这个函数有两个参数,resolve
,reject
,resolve
和reject
也是两个函数,由JS引擎提供,不用自己创建。我们称这两个函数为决议函数,决定了Promise对象的状态。
resolve在Promise状态变为resolved时调用,上面的例子中,status===200
表示服务器已经正常响应,这时调用resolve函数,获得response
。reject
在Promise
对象状态变为rejected
时调用,上面的例子中如果status !== 200
会调用reject函数。
从另一个角度来理解Promise
对象状态和resolve
函数、reject
函数。
resolve
和reject
函数控制着Promise
对象状态,调用resolve函数,Promise对象状态就由pendding
状态变为resolved
状态,并且不可改变;调用reject
函数,Promise
对象状态会由pendding
状态变为rejected
状态,什么时候调用resolve函数或者reject
函数,是由你的代码或者需求来决定的。举个例子。
waitTime = function(time) {
const promise = new Promise( (resolve,reject) => {
setTimeout(resolve,time);
});
return promise;
}
waitTime(1000);
这个例子中waitTime
函数创建了一个Promise
对象,至少1000ms
后,执行resolve
函数,这时Promise对象状态就变为resolved
。
3.then方法
promise对象生成后,可以用then方法指定resolved状态和rejected状态对应的回调函数,如果resolve
、reject
函数带有参数(将来值),那么这个参数会被传给对应状态的回调函数。这时回调函数就可以使用resolve和reject传出的餐宿,这时上文中说的承诺值就会被将来值取代。
注意:then方法会返回一个新的Promise
实例
4.当resolve或者reject函数的参数是一个Promise
决议函数的参数除了正常值意外,还可以是一个Promise对象,这时就会出现一个Promise对象的状态依赖另一个Promise对象的状态。
请看下面代码
const promise1 = new Promise((resolve, reject) => {
document.onclick = function() {
resolve('promise2 depends on promise1 ');
console.log('promise1 resolved');
}
});
const promise2 = new Promise((resolve, reject) => {
resolve(promise1 );
}).then((data) => {
console.log('promise2 resolved');
})
promise1
和promise2
两个promise
对象,promise2对象的resolve函数的参数是promise1,这时promise2对象的状态就取决于promise1。
在谷歌控制台测试上段代码,当点击document时,会触发resolve函数,这时promise1 对象状态变为resolved,随即promise2状态也变为resolved,在控制台会看到promise1 resolved ,promise2 resolved,如果不点击,两者的状态永远是pendding.
由promise1
的状态决定promise2
的状态。所以,后面的then
语句都变成针对后者promise1
.
请看
const promise1 = new Promise((resolve, reject) => {
document.onclick = function() {
resolve('promise2 depends on promise1 ');
console.log('promise1 resolved');
}
});
const promise2 = new Promise((resolve, reject) => {
resolve(promise1);
}).then(
(data) => console.log(data); //promise2 depends on promise1
);
5.Promise.protype.catch
Promise.protype.catch
是Promise.protype.then(null,rejection)
的别名,用于指定发生错误时的回调函数。
将第一个例子部分handler改为:
function handler() {
if (this.readyState === 4) {
if (this.status === 200) {
return resolve(JSON.parse(this.response));
} else {
return reject(this.status);
}
}
}
点击事件改为:
_$('search').onclick = function() {
let url = 'null_serverjson.php?number=' + _$('keyword').value
getJSON(url)
.then((data) => _$('searchResult').innerHTML = data.msg)
.catch((error) => _$('searchResult').innerHTML = error);
};
getJSON方法请求一个不存在的php文件null_serverjson.php文件,AJAX请求失败,ERROR:GET http://localhost:9000/miniManagementSystem/serverjson1.php?number=123 404 (Not Found)
异步操作status === 404
触发getJSON
方法中Promise
对象的决议函数reject()并返回状态吗status,点击事件中对异步操作的错误进行处理(将错误码显示在页面上)。
这里说明了如何catch
方法对异步异常进行处理。明确一点的是,如果异常处理发生,该promise
对象的状态是rejected
,并且这个promise
对象状态不会再改变。
Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch
语句捕获。如果没有报错,则会跳过catch
语句。
catch
方法之中还能在抛出错误。
catch
方法返回一个Promise
实例
6.Promise.all
Promise.all
方法用于将多个 Promise
实例,包装成一个新的 Promise
实例。
const p = Promise.all([p1,p2,p3]);
Promise.all接受一个数组,数组的每个元素是Promise实例,如果不是调用Promise.resolve将其转化成Promise实例。
p的状态由p1,p2,p3共同决定。
1)p1 & p2 & p3 resolved ===> p resoolved
2)
2)p1 || p2 || p3 rejected ===> p rejected
此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
3)如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => resolve('hello'))
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
const p = Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
p1
会resolved
,p2
首先会rejected
,但是p2
有自己的catch
方法,该方法返回的是一个新的 Promise
实例,p2
指向的实际上是这个实例。该实例执行完catch
方法后,也会变成resolved
,导致Promise.all()
方法参数里面的两个实例都会resolved
,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。
p2没有自己的catch方法,就会调用Promise.all()的catch方法。
7.Promise.race()
const p = Promsie.all([p1,p2,p3]);
如果p1,p2,p3不是Promise实例,则调用Promise.resolve将其转化为Promise对象。p的状态由p1 || p2 || p3的三者最先确定状态决定。
设想一种场景,移动端上传一张图片到服务器,因为没有限制图片的尺寸,如果规定的时间没有上传成功,建议用户上传体积更加小的图片或者检查网速。就可以考虑用Promise.race()
方法。
模拟图片上传
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('图片上传成功'), 6000);
});
const p2 = new Promise((resolve, reject) => {
let Errormsg = "上传较慢,请检查网速或重新选择图片!"
setTimeout(() => reject(new Error(Errormsg)), 5000);
});
function uploadImg() {
const p = Promise.race([p1, p2])
.then(msg => console.log(msg))
.catch(e => console.log(e));
}
document.onclick = uploadImg;
上图中p2状态先确定为rejected,p的状态变为rejected,执行catch方法回调函数。打印错误信息。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('图片上传成功'), 4000);
});
const p2 = new Promise((resolve, reject) => {
let Errormsg = "上传较慢,请检查网速或重新选择图片!"
setTimeout(() => reject(new Error(Errormsg)), 5000);
});
function uploadImg() {
const p = Promise.race([p1, p2])
.then(msg => console.log(msg))
.catch(e => console.log(e));
}
document.onclick = uploadImg;
p1状态先确定为resolve表示上传成功,p执行zhen方法的回调,控制台可以看到打印出上传成功。
本文写于 2017/12/31日,2017年写的博客就以Promise作为结束吧。