什么是Promise?
Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一,最早由社区提出并实现,典型的一些库有Q,when, bluebird等;它的出现是为了更好地解决JavaScript中异步编程的问题,传统的异步编程最大的特点就是地狱般的回调嵌套,一旦嵌套次数过多,就很容易使我们的代码难以理解和维护。而Promise则可以让我们通过链式调用的方法去解决回调嵌套的问题,使我们的代码更容易理解和维护,而且Promise还增加了许多有用的特性,让我们处理异步编程得心应手。
Promise,就是一个对象,用来传递异步操作的消息。代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。
常见异步编程方案
- 回调函数
- 事件监听
- 发布/订阅
- Promise对象
Promise出现的原因
在Promise出现以前,我们处理一个异步网络请求通常使用回调函数解决方案,对于回调函数,我们用ajax获取数据时 都是以回调函数方式获取的数据,我们来看个例子:
// request 表示 一个异步网络调用,result1 表示网络请求的响应。
request1(function(result1){
处理result1
})
上述代码看起来好像没有什么问题,但是如果我们需要根据第一请求的结果去处理第二个网络请求,那么代码会是下面这个样子:
request1(function(result1){
request2(function(result2){
处理result2
})
})
上面代码看起好像还好,但是有可能会出现第二次请求的结果,需要在第三次异步请求时使用,就是多个异步请求之间是依赖关系,那么我们的代码就会变成下面这个样子,即形成回调地狱:
request1(function(result1){
request2(function(result2){
request3(function(result3){
request4(function(result4){
request5(function(result5){
......等等
})
})
})
})
})
上述代码不好的是我们基本上还要对每次请求的结果进行处理,代码会更加臃肿,如果我们在一个团队中,那么代码review以及后续维护将会是一个很困难的过程。
回调地狱带来的问题
- 代码臃肿。
- 可读性差。
- 耦合度过高,可维护性差。
- 代码复用性差。
- 容易产生bug。
- 只能在回调里处理异常。
面对上述问题,就有人思考了,能不能用一种更加友好的代码去解决上述异步嵌套的问题。于是Promise规范诞生了,并且在业界有了很多实现来解决回调地狱的痛点。
Promise处理多个相互关联的异步请求
Promise可以更直观的方式来解决"回调地狱"
我们来看个例子:
const promise = url => {
return new Promise((resolve, reject) => {
$.get(url, res => {
resolve(res)
});
})
};
// 请求data1
promise(url).then(res1 => {
return promise(res1.url);
}).then(res2 => {
return promise(res2.url);
}).then(res3 => {
console.log(res3);
}).catch(err => throw new Error(err));
可以总结为如下代码形式:
new Promise(request1)
.then(request2(result1))
.then(request3(result2))
.then(request4(result3))
.then(request5(result4))
.catch(处理异常(异常信息))
我们可以比较下这种写法和上面的回调式的写法。会发现,Promise 的写法更为直观,简洁明了,并且能够在外层捕获异步函数的异常信息。
如何使用promise
ES6给我们提供了一个原生的构造函数Promise,Promise是一个构造函数,我们可以看下这个构造函数:
console.logtypeof Promise();
// "function" 可以看出这是一个构造函数
console.log(Promise);
// function Promise() { [native code] } // ES6的原生支持
new Promise返回一个promise对象接收一个excutor执行函数作为参数, excutor有两个函数类型形参resolve reject。
let promise = new Promise((resolve, reject) => {
// 异步请求处理
// 异步请求结束后、调用resolve或reject
});
我们对上述代码解说一下:
- Promise是一个构造函数,所以使用new操作符来创建promise。
- 构造函数Promise的参数是一个函数(func),这个函数有两个参数resolve和reject,分别是两个函数,作用就是将promise的状态从pending转换为resolved或者从pending转换为rejected。
- 创建后的promise有一些方法,then和catch。当然我们也可人为的在Promise函数上添加一些满足自己需求的方法,方便promise对象使用。
promise相当于一个状态机, 有三种状态:
- pending
- fulfilled
- rejected
promise 对象初始化时状态为pending,当调用resolve(成功),会由pending => fulfilled, 当调用reject(失败),会由pending => rejected。 状态一旦改变,就不会再变。创造promise实例后,它会立即执行。
通俗一点来讲就是:
Promise函数体的内部包裹着一个异步的请求或者操作或者函数;然后我们可以在这个异步的操作完成的时候使用resolve函数将我们获得的结果传递出去,或者使用reject函数将错误的消息传递出去。
注意:promsie状态只能由pending => fulfilled/rejected, 一旦修改就不能再变。
promise对象方法
- then方法注册resolve(成功)/reject(失败)的回调函数
// onFulfilled接收promise成功的值
// onRejected接收promise失败的原因
promise.then(onFulfilled, onRejected);
注意:then方法是异步执行的
- resolve(成功) onFulfilled会被调用
let promise = new Promise((resolve, reject) => {
resolve('fulfilled'); // 由pending => fulfilled
});
promise.then(result => { // onFulfilled
console.log(result); // 'fulfilled'
}, reason => { // onRejected不会被调用
})
- reject(失败) onRejected会被调用
let promise = new Promise((resolve, reject) => {
reject('rejected'); // 由pending => rejected
});
promise.then(result => { // onFulfilled 不会被调用
}, (err) => { // onRejected
console.log('rejected',err); // 'rejected'
})
- promise.catch
在链式写法中可以捕获上述代码中发送的异常
promise.catch(onRejected)
相当于
promise.then(null, onRrejected);
// 注意onRejected不能捕获当前onFulfilled中的异常
promise.then(onFulfilled, onRrejected);
// 可以写成:
promise.then(onFulfilled)
.catch(onRrejected);
- promise chain
promise.then方法每次调用都返回一个新的promise对象所以可以写成链式
调用形式
function fn1() {console.log("Task 1");}
function fn2() {console.log("Task 2");}
function onRejected(error) {console.log("Catch Error: 1 or 2", error);}
var promise = Promise.resolve();
promise
.then(fn1())
.then(fn2())
.catch(onRejected()) // 捕获异常
总结:
- 我们可以看到,then接受两个函数作为参数,第一个函数是用来处理resolve的结果,第二个是可选的,用来处理reject的结果。即我们在创建Promise对象的时候,通过函数resolve传递出去的结果可以被promise的第一个then方法中的第一个函数捕获然后作为它的参数。通过函数reject传递出去的结果可以被promise的第一个then方法中的第二个函数捕获然后作为它的参数。总结一句话就是(then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调)
- 每一个then方法中都可以再次新创建一个Promise对象,然后返还给下一个then方法处理。
- resolve找then里的成功回调,reject找then里失败的回调。
Promise的静态方法
- Promise.resolve返回一个fulfilled状态的promise对象
Promise.resolve('node').then(function(res){
console.log(res); // node
});
Promise.resolve('node'); // node
// 相当于
let promise = new Promise(resolve => {
resolve('node'); // node
});
- Promise.reject返回一个rejected状态的promise对象
Promise.reject(‘失败’);
new Promise((resolve, reject) => {
reject(‘失败’);
});
- Promise.all接收一个promise对象数组为参数
只有全部为resolve才会调用,通常会用来处理多个并行异步操作。
谁跑的慢,以谁为准执行回调。all接收一个数组参数,里面的值最终都返回Promise对象。
有了all,就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很爽?有一个场景是很适合用这个,比如一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。
let p1 = new Promise((resolve, reject) => {
resolve(1);
});
let p2 = new Promise((resolve, reject) => {
resolve(2);
});
let p3 = new Promise((resolve, reject) => {
resolve(3);
});
Promise.all([p1, p2, p3]).then(data => {
console.log(data); // [1, 2, 3] 结果顺序和promise实例数组顺序是一致的
}, err => {
console.log(err);
});
- Promise.race 接收一个promise对象数组为参数
Promise.race 只要有一个promise对象进入FulFilled或者Rejected 状态的话,就会继续进行后面的处理。
谁跑的快,以谁为准执行回调
function timerPromisefy(delay) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
var startDate = Date.now();
Promise.race([
timerPromisefy(10),
timerPromisefy(20),
timerPromisefy(30)
]).then(function (values) {
console.log(values); // 10
});
Promise特点
- Promise的立即执行性
const promise = new Promise(function(resolve, reject){
console.log("create a promise");
resolve("success");
});
console.log("after new Promise");
promise.then(function(res){
console.log(res);
});
输出: // "create a promise"
// "after new Promise"
// "success"
Promise对象表示未来某个将要发生的事件,但在创建(new)Promise时,作为Promise参数传入的函数是会被立即执行的,只是其中回调执行的代码是异步代码。
- Promise 状态的不可逆性
const promise1 = new Promise(function(resolve, reject){
resolve("success1");
resolve("success2");
});
const promise2 = new Promise(function(resolve, reject){
resolve("success");
reject("reject");
});
promise1.then(function(res){
console.log(res);
});
promise2.then(function(res){
console.log(res);
});
// 输出结果: "success1" "success"
- 链式调用
var p = new Promise(function(resolve, reject){
resolve(1);
});
p.then(function(value){ //1
console.log(value);
return value*2;
}).then(function(value){ //2
console.log(value);
}).then(function(value){ //3
console.log(value);
return Promise.resolve('resolve');
}).then(function(value){ //4
console.log(value);
return Promise.reject('reject');
}).then(function(value){ //5
console.log('resolve: '+ value);
}, function(err){
console.log('reject: ' + err);
})
// 输出:
1
2
undefined
"resolve"
"reject: reject"
Promsie 与事件循环
Promise在初始化时,传入的函数是同步执行的,然后注册then回调,注册完之后,继续往下执行同步代码,在这之前,then 中回调不会执行。同步代码块执行完毕后,才会在事件循环中检测是否有可用的 promise 回调,如果有,那么执行,如果没有,继续下一个事件循环。
promise应用场景
- 回调地狱,代码难以维护, 通常第一个的异步请求的输出是第二个请求的输入这种现象(多个请求互相依赖关系)。
- promise可以支持多个并发的请求,获取并发请求中的数据
- promise可以解决异步的问题,本身不能说promise是异步的
promise使用总结
- 首先初始化一个 Promise 对象,可以通过两种方式创建, 这两种方式都会返回一个 Promise 对象。
1、new Promise(fn)
2、Promise.resolve(fn) - 然后调用上一步返回的 promise 对象的 then 方法,注册回调函数。
1、then 中的回调函数可以有一个参数,也可以不带参数。如果 then 中的回调函数依赖上一步的返回结果,那么要带上参数。比如
new Promise(fn)
.then(fn1(value){
//处理value
})
- 最后注册catch异常处理函数,处理前面回调中可能抛出的异常。
通常通过这三个步骤,就能够应对绝大部分的异步处理场景。
最后我们看一些常见的promise例子:
demo1:
const p1 = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
p1.then(() => {
console.log(3)
})
console.log(4)
最终输出结果是:1 2 4 3,Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的。
demo2:
const p1 = new Promise((resolve, reject) => {
resolve('success1')
reject('error')
resolve('success2')
})
p1.then((res) => {
console.log('then: ', res)
})
.catch((err) => {
console.log('catch: ', err)
})
最终输出结果:success1,构造函数中的resolve或reject只有第一次执行有效,多次调用没有任何作用,即promise 状态一旦改变则不能再变。
后续会有一篇文章讲解下如何手写一个promise。