背景
我们在实际的开发当中,会遇到很多这样的场景,就是我们不能立即知道接下来该如何去执行一段代码,就比如我们的ajax请求,因为根据很多外界的因素,我们获得服务器返回的数据的时间是不确定的,所以我们必须要等到结果出来之后,才知道接下来该做什么。
回调地狱
我们可以用原生的Ajax请求来模拟一个场景:
<script>
var url = 'https://www.abc/do';
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.response);
}
}
</script>
这是原生的ajax实现,我们需要用到onreadystatechange
事件来确定我们的代码何时才能拿到我们的数据,拿到数据之后,才可以处理这些数据。光这样看起来,其实毫无毛病,但是如果这个时候我们又有一个ajax请求,但是其中的某个参数需要等到第一次的ajax结束后才能拿到,我们不得不这样去做:
<script>
var url = 'https://www.abc/do';
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
var url2 = "https://www.abc/happy?user=" + xhr.response.user;
var xhr2 = new XMLHttpRequest();
xhr2.open('GET', url);
xhr2.send();
xhr2.onreadystatechange = function() {
if(xhr2.readyState == 4 && xhr2.status == 200) {
console.log(xhr2.responseText);
}
}
}
}
</script>
当出现第3次ajax请求依赖于上一次请求的话,我们的代码就嵌套非常深了,变得非常恶心,这样的情况就被称之为回调地狱
。于是我们需要一种手段来解决这种情况。
Promise
Promise就是用于解决回调地狱问题的,当然了除了这个,promise将数据的请求
和数据的处理
明确的分开来了。然而我们原生的ajax对于这一块是很混乱的,当数据比较复杂的时候,就很难去维护了。Promise准确来说是一个构造函数,我们可以通过它构造出Promise对象。
Promise对象的3种状态
状态 | 描述 |
---|---|
pending | 等待/进行中,还未得到结果 |
resolved | 完成,得到了想要的结果,可以继续往下执行 |
rejected | 完成,得到了错误结果,拒绝执行 |
这三种状态只能是由pending
改变成resolved
或者rejected
,是不可逆的。
如何使用Promise
我们可以用Promise构造函数构造一个Promise对象,在构造函数中,第一个参数传入一个函数,这个函数是用于处理Promise状态的变化的,看具体代码:
<script>
new Promise(function(resolve, reject){
if(true) { resolve() }
if(false) { reject() }
})
</script>
resolve
和reject
都是函数,作用就是将pending状态修改为 resolved或者rejected。
promise对象有一个then方法,可以对不同状态分别执行,then方法接受两个参数,这俩参数都是函数,第一个函数是接受到resolved状态对应执行的函数,第二个则是接受到rejected状态对应执行的函数。
<script>
function isNumber(num){
return new Promise((resolve, reject) => {
if(typeof num == 'number') { resolve() }
else { reject() }
}).then(() => {
console.log("是数字类型的");
}, () => {
console.log("不是数字类型的");
});
}
isNumber(1);
isNumber("Lan");
</script>
Output:
是数字类型的
不是数字类型的
then方法执行结束后,依然会返回一个Promise对象,因此我们可以链式调用then方法:
<script>
function isNumber(num){
return new Promise((resolve, reject) => {
if(typeof num == 'number') { resolve() }
else { reject() }
}).then(() => {
console.log("是数字类型的");
}).then(null, () => {
console.log("不是数字类型的");
});
}
isNumber(1);
isNumber("Lan");
</script>
链式的去调用then方法也是解决回调地狱的主要方法。.then(null, function(){})
也可以写成catch(function(){})
Promise数据传递
只需要把想要传递的数据座位参数放入到resolve()和reject()函数中即可,体会一波:
<script>
var succTip = "is Number";
var errTip = "not Number";
function isNumber(num){
return new Promise((resolve, reject) => {
if(typeof num == 'number') { resolve(succTip) }
else { reject(errTip) }
});
}
isNumber(1).then(tip => {
console.log(tip);
});
isNumber("Lan").then(tip => {
console.log(tip);
});
</script>
Output:
is Number
Uncaught (in promise) not Number
Promise封装ajax
我们可以先封装一个get方法:
<script>
var url = "http://www.abc.cn/test";
function promsieAjax(url){
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
if(xhr.status == 200) {
var response = JSON.parse(xhr.responseText);
resolve(response);
}else {
reject(new Error(xhr.statusText));
}
}
}
})
}
promsieAjax(url).then(data => {
console.log(data);
}).catch(error => {
console.log(error);
});
</script>
总之就是在正确的地方调用resolve,在错误的地方调用reject,并且把数据传递出来即可。
Promise.all
Promise.all接受一个由Promise对象组成的数组,然后返回一个新的Promise对象,必须当这个数组中所有的Promise对象的状态都是resolved的时候,新的Promise对象的状态才是resolved。例子:
<script>
var p1 = new Promise((resolve, reject) => {
resolve("success 1");
});
var p2 = new Promise((resolve, reject) => {
resolve("success 2");
});
var p = Promise.all([p1, p2]);
p.then((data) => {
console.log(data);
}).catch(error => {
console.log("error");
})
</script>
Output: ["success 1", "success 2"]
Promise.race
Promise.race 的用法和all的用法大致相同,同样的,Promise.race也是接受一个Promise的对象数组,不过新的Promise对象状态取决于数组中第一个Promise对象所变化的状态。直接看例子吧:
<script>
var p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success 1");
},1000);
});
var p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success 2");
},500);
});
var p = Promise.race([p1, p2]);
p.then((data) => {
console.log(data);
}).catch(error => {
console.log("error");
})
</script>
Output:success 2
Async/await
async是属于ES7规范里面的,现在已经广泛使用了,他其实是Generator
函数的语法糖,是目前解决异步操作的最佳方案。
Async function
我们可以把async关键字放在一个函数的申明前面,像这样:
<script>
async function fn() {
return 1;
}
</script>
async的作用就在于,指定的函数总是会返回一个Promise对象,就算本身我们返回的不是Promise对象,比如我们上面手动返回的1,它也会帮我们把返回的这个value值包装成promise的resolved值,我们可以测试一下:
<script>
async function fn() {
return 1;
}
fn().then((data) => {
console.log(data);
})
</script>
Output:1
也就是说上面的代码等同于下面的代码:
<script>
async function fn() {
return Promise.resolve(1);
}
</script>
也就是说,async可以确保一个函数返回的是一个Promise对象,除了async关键字,还有一个await
关键字,它只能在async函数中使用。
Await
关键词await
可以让JavaScript进行等待,直到一个promise执行并返回它的结果,JavaScript才会继续往下执行。
<script>
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('done!'), 1000)
})
let result = await promise // 直到promise返回一个resolve值
console.log(result);
}
f()
</script>
Output:done!
在await的那一行,会“暂停”,直到promise处理完之后会重新恢复,resolve的值就会变成result,最终输出了done。await语句可以让promise更加的易于阅读。
async/await的应用
假如我们有一个操作,有很多步骤,并且每一步都是一个异步操作,后面的异步操作可能会依赖异步操作的结果,那么这种情况,我们用async/await可以很简单的解决:
<script>
function asyncTest(time){
return new Promise(resolve => {
setTimeout(resolve(time + 100), time);
})
};
function stepOne(n){
console.log("stepOne", n);
return asyncTest(n);
}
function stepTwo(n1, n2){
console.log("stepTwo", n1, n2);
return asyncTest(n1 + n2);
}
function stepThree(n1, n2 , n3){
console.log("stepThree", n1, n2, n3);
return asyncTest(n1 + n2 + n3);
}
async function doAll(){
let time1 = 500;
let time2 = await stepOne(time1);
let time3 = await stepTwo(time1, time2);
let res = await stepThree(time1, time2, time3);
console.log("res:", res);
}
doAll();
</script>
Output:
stepOne 500
stepTwo 500 600
stepThree 500 600 1200
res: 2400
错误处理
如果一个Promise正常的resolved的话,它会返回结果,但是如果rejected的了,它会抛出一个错误,就好像那一行有一个throw
语句一样。
<script>
async function fn(){
await Promise.reject(new Error("ERR"));
}
</script>
和下面的是一样的:
<script>
async function fn(){
throw new Error("ERR");
}
</script>
在真实的使用场景中,promise在reject抛出错误之前可能需要一段时间,所以await将会等待,然后才抛出一个错误。
我们可以使用try-catch语句捕获错误,就像在正常抛出中处理异常一样:
<script>
async function fn() {
try {
let response = await fetch('http://www.abc/some')
} catch (err) {
console.log(err)
}
}
fn()
</script>
如果我们不使用try-catch,然后async函数f()的调用产生的promise变成reject状态的话,我们可以添加.catch去处理它:
<script>
async function fn() {
let response = await fetch('http://www.bac/some')
}
fn().catch((err) => {
console.log(err);
})
</script>
总结
我们使用async/await 搭配 Promise可以很好的解决异步操作问题,写出更易于阅读的异步操作代码。