Promise 是干嘛的?
Promise是ES6针对js异步编程一种解决方案,也解决了ES5之前异步编程大量回调函数的写法的痛点,我们来亲切感受一下。
假设有这么一个需求:一个页面需要进行4次ajax请求才能渲染完所有内容,但是每一次请求依赖上一次请求返回的动态url。我们会联想到这将是一次链式请求。
ES5写法:
// request 假设是事先封装好的ajax方法
request(url, function(res1) {
/* 大量业务逻辑 */
...
request(res1.url, function(res2) {
/* 大量业务逻辑 */
...
request(res2.url, function(res3) {
/* 大量业务逻辑 */
...
request(res3.url, function(res4) {
/* 大量业务逻辑 */
...
});
});
});
});
看到这样的异步回调,是不是有一种在地狱间来回穿梭的感觉,这就是传说中的“回调地狱”,代码层级嵌套深,结构很不直观,增加大量的维护成本(当然谁会去想维护这样的代码?,何以解忧,唯有离职)-0—。
ES6写法:
// request 假设是事先封装好的ajax Promise 方法
request(url, (res) => {
/* 大量业务逻辑 */
...
return request(res.url);
}).then((res) => {
/* 大量业务逻辑 */
...
return request(res.url);
}).then((res) => {
/* 大量业务逻辑 */
...
return request(res.url);
}).then((res) => {
/* 大量业务逻辑 */
...
});
领教了ES5回调地狱,回头看看这ES6的Promise写法,是不是觉得优雅不要太多,链式调用清晰明了,结构分明(当然这是我个人感觉了,你觉得呢?)-0-。
Promise 基本用法
Promise 其实就是一个构造函数,它接收一个回调函数作为一个参数,这个回调函数默认有两个参数,即resolve和reject。进行new后,返回一个promise对象,这个对象即有then,catch,finally的Promise原型方法。
1.创建一个Promise
我们先来一个最基本的promise:
// 创建一个promise对象
var promiseObj = new Promise((resolve, reject) => {
/* 业务逻辑 */
...
if (true) {
// success
resolve(success);
} else {
reject(error);
}
});
// 调用promise对象方法
promiseObj.then((success) => {
// 此处为成功状态的回调,success数据即为以上resolve函数里的success
}).catch((e) => {
// 此处为失败状态的回调, error即为以上reject函数里的error;
}).finally(() => {
// 此处不管成功还是失败,都会执行,使用情况比较少。
});
在创建一个promise对象的时候,Promise构造函数里的参数即回调函数,它会执行一遍,所以我们通常会将new Promise的过程放在一个方法里作为return返回,在需要这个promise实例的时候,调用这个方法即可获取到这个实例并执行代码业务逻辑,具体如下。
2.封装我们想要的Promise
我们来写一个文章开头提到的Promise的request方法,深刻感受一下:
function request(url) {
return new Promise((resolve, reject) => {
var XHR = new XMLHttpRequest();
XHR.open('GET', url);
XHR.onreadystatechange = function () {
if (XHR.readyState === 4) {
if (XHR.status === 200) {
// 请求成功,将服务器返回的数据reslove出去
resolve(XHR.responseText);
} else {
// 请求失败,将触发的错误reject出去
reject(new Error(XHR.responseText));
}
}
};
XHR.send(null);
});
}
// 调用request
request(url).then((res) => {
// res 即为 以上resolve函数里面的XHR.responseText;
console.log(res);
}).catch((e) => {
// catch 通常即为捕获错误的地方,即reject返回的new Error(XHR.responseText)
new Error(e);
});
Promise 实例方法
在Promise实例化一个promise对象后,它只有三个方法,即 then, catch, finally。
1.then
then 方法里执行的是resolve状态
request(url).then((res) => {
// res 为resolve状态返回的数据,一般也指成功状态
})
then 方法可以链式调用,即文章开头的request方法链式then调用
request(url).then((res) => {
...
return res.data;
}).then((data) => {
// 此处的data 即为上一步then的return值:res.data
});
所以then的return值也可以是一个promise对象:
request(url, (res) => {
...
// 返回request方法调用结果:即一个promise对象
return request(res.url);
}).then((res) => {
// 此处的res 即为上一步返回方法request里的的resolve值,相当于上一步返回request调用then
...
// 继续返回一个promise对象
return request(res.url);
}).then((res) => {
})
2.catch
catch 方法里执行的是reject状态和错误处理
request(url).then((res) => {
}).catch((e) => {
// reject状态或者request方法内部错误了,都会在这里被捕获到。
});
说白了,catch方法不仅处理reject状态数据,而且还会捕获因为代码运行而产生的错误,我们就叫它错误垃圾处理箱吧。
3.finally
finally 方法即不管resolve还是reject状态都会执行的方法。
request(url).then((res) => {
}).catch((e) => {
}).finally(() => {
console.log('请求完成了');
});
Promise 静态方法
1.Promise.all()
假设一个页面有多个并行的请求,但是你想等他们都请求完后再统一获取他们返回的数据并处理,就可以用到这个方法。
此方法是将多个Promise实例包装成一个新的Promise实例。
Promise.all 方法接收一个数组作为参数,数组的值即为各个promise实例:
var promises = Promise.all([p1, p2, p3]);
promises.then((resArr) => {
// resolve状态 此时返回的resArr为一个数组,数组值为p1,p2,p3的resolve数据
}).catch((e) => {
// reject 状态 p1,p2,p3的reject
});
如上,Promise.all将promise实例p1,p2,p3包装成一个新的promise实例promises,此时promises的回调方法返回参数会发生一些改变:
- promises的resolve状态由p1,p2,p3共同决定,只有p1,p2,p3状态都为resolve了,promises的状态才会变为resolve
- promises的reject状态由p1,p2,p3任何一个决定,只要p1,p2,p3的状态某一个为reject了,promises状态就会为reject
我们可以通过多个request方法来深刻了解一下:
// requests为三个不同的请求被Promise.all包装成功一个新的promise实例
var requests = Promise.all([request(url1), request(url2), request(url3)]);
// 调用requests,会同时发出三个请求
requests.then((res) => {
// resovle状态:当三个请求都成功以后,会进入此状态
// 此时res 为一个数组,即三个请求返回的数据组成的数组
console.log(res.length) // 3
}).catch((e) => {
// reject状态: 只要三个请求其中一个发生了错误,就会进入此状态
});
2.Pormise.race()
Promise.race 同Promise.all一样,也是将多个promise实例包装成一个新的promise实例:
var promises = Pormise.race([p1, p2, p3]);
但是,它跟Promise.all不同的是,只要p1,p2,p3中谁最先变为状态(不管是resolve状态还是reject状态),promises回调函数即为它的状态。
我们先看看resolve状态:
promises.then((res) => {
// 假设p1,p2,p3中p2最先改变为resolve状态,res参数即为p2的resolve数据
});
再看看reject状态的运用:
// 请求超时promise 设置5秒超时
function timeout () {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('timeout'));
}, 5000);
});
}
var requests = Promise.race([request(url), timeout()]);
requests.then((res) => {
// 如果5秒之内成功请求,此时即为request的resolve状态
}).catch((e) => {
// 如果5秒超时,此时即为timeout的 reject状态
});
3.Promise.resolve() 与 Promise.reject()
这两种方法都是将现有对象转为Promise对象,区别是该对象是否有then方法,有then方法则直接调用该对象的then方法,如果没有,则直接返回这个对象。
这两种方法我其实用的比较少,不过我们可以改造一下request方法来熟悉这两个方法:
// request2方法做了一些改变, 直接返回一个具有then方法和请求结果的对象。
function request2 (url) {
var data = null;
var XHR = new XMLHttpRequest();
XHR.open('GET', url);
XHR.onreadystatechange = function () {
if (XHR.readyState === 4) {
if (XHR.status === 200) {
data = XHR.responseText;
}
}
};
XHR.send(null);
return {
data,
then (resolve, reject) {
resolve(this.data);
}
};
}
// 理由Promise.resolve 转化request2为promise对象
var promise = Promise.resolve(request2(url));
promise.then((res) => {
// 此时 res为 request2返回对象then方法的返回值 this.data
});
Promise.reject 与之是同样的用法,但是与Promise.resolve的区别是reject状态返回的不是reject数据,而是这个request2返回的对象本身:
function request2 (url) {
...
return {
data,
then (resolve, reject) {
reject('出错了');
}
}
}
var thenObj = request2(url);
var promise = Promise.reject(thenObj);
promise.then((res) => {
}).catch((e) => {
// e即为thenObj对象本身,并不是 reject 数据'出错了'
console.log(e === thenObj) //true
})
总结
通过Promise构造函数封装我们的异步业务逻辑,再进行优雅的链式then调用,跳出“回调地狱”,再在垃圾箱catch中捕获你的错误,Promise,就是这么简单。