1 简介
Promise 是异步编程的一种解决方案。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
在控制台打印Promise:
可以发现,Promise是一个构造函数,自身有all、race、reject、resolve等方法;
原型prototype上有then、catch等方法(因此只要作为Promise的实例,都可以共享并调用Promise.prototype上面的方法 then,catch)。
2 语法
new Promise(function(resolve,reject){}/*fn*/);
在实例化Promise时需要传入一个函数fn作为参数,并且在Promise构造函数执行时同步执行。
我们先new 一个Promise:
var p = new Promise(function (resolve, reject) {
console.log('Hello');
setTimeout(function () {
console.log(111);
}, 1000);
})
// 输出: Hello (1s later) 111
上述代码中,执行了一个异步操作,也就是setTimeout,1秒后,输出111。
Promise的构造函数接收一个参数,是一个function(fn入两个参数:resolve,reject,其实这两个参数也是函数,在function执行时被调用。
我们看到2s后在控制台输出相应的结果,这就说明在实例化过程中,作为参数的fn函数也会执行。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:
function fn() {
return new Promise((resolve, reject) => {
// code
});
}
fn()
在fn函数里,会return出Promise对象,也就是说,执行这个函数得到了一个Promise对象。
3 Promise的几种状态
- pending:初始状态,进行中
- fulfilled:操作成功
- rejected:操作失败
Promise对象有以下两个特点:
**(1)对象的状态不受外界影响。**Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
**(2)一旦状态改变,就不会再变。**Promise对象的状态改变,只有两种可能:
- 从pending变为fulfilled
- 从pending变为rejected
只要这两种情况发生,状态就凝固了,不会再变了,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1); // 再调用resolve方法,状态不会再改变到fulfilled了
}, 0);
reject(2); // pedding → rejected (状态凝固在rejected了)
}).then((data) => {
console.log(data);
}, (reason) => {
console.log(reason);
});
// 输出:2
调用reject方法后,Promise状态变为rejected并且不会再发生改变,此时执行then里面就会执行onrejected操作
4 resolve
当我们在function中调用resolve方法时,Promise的状态就变成fulfilled,即操作成功状态;
当Promise状态为fullfilled时执行then里面相应的操作(执行onfulfilled函数)。
注:then方法里面有两个参数,onfulfilled函数(fulfilled状态时执行) 和onrejected函数(rejected状态时执行)。
var p = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('执行操作1');
resolve('这是数据1');
}, 1000);
});
p.then(function (data) {
console.log(data);
console.log('这是成功操作');
});
// 输出:
(1s later)
执行操作1
这是数据1
这是成功操作
简单的理解就是调用resolve方法,Promise状态变为fulfilled,执行then方法里面第一个参数onfulfilled函数里的操作。其实then里面的函数就是我们平时所说的回调函数,只不过在这里只是把它分离出来而已。
5 reject
调用reject方法后,Promise状态变为rejected,即操作失败状态,此时执行then里面就会执行onrejected操作:
var p = new Promise(function (resolve, reject) {
var flag = false;
if (flag) {
resolve('这是数据2');
} else {
reject('这是数据2');
}
});
p.then((data)=> { // 状态为fulfilled时执行
console.log(data);
console.log('这是成功操作');
}, (reason)=> { // 状态为rejected时执行
console.log(reason);
console.log('这是失败的操作');
});
// 输出:
这是数据2
这是失败的操作
6 catch
Promise中的异常捕获
需求 :前面的Promise执行失败,但是不要影响后续Promise正常执行。
此时可以单独为每个promise通过.then()指定一下失败的回调:(这里以f2为例,只设置了f2)
function f1() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(1)
}, 1000)
})
}
function f2() {
return new Promise(function (resolve, reject) {
var flag = false
setTimeout(() => {
if (flag) {
resolve(2)
}
if (!flag) {
reject('error!')
}
}, 1000)
})
}
function f3() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(3)
}, 1000)
})
}
f1().then((data) => {
console.log(data);
return f2()
}).then((data) => {
console.log(data)
return f3()
}, (reason) => {
console.log(reason)
return f3()
}).then((data) => {
console.log(data)
})
// 输出:(1s later)1 (1s later)error! (1s later) 3
上述代码中,出现错误输出错误信息后,依旧执行了后面的Promise。
其实如果前面的Promise执行失败,后面Promise依赖于前面Promise执行结果的话,那么前面失败了后面也没有继续执行下去的意义了。此时可以使用 .catch()进行异常捕获,只要前面Promise有任何一个执行失败,立即终止所有的Promise的执行,并马上进入catch中去处理Promise中抛出的异常。
catch也是在Promise状态为rejected时执行,then方法捕捉到Promise的状态为rejected的话,就执行catch方法里面的操作。
f1().then((data) => {
console.log(data);
return f2()
}).then((data) => {
console.log(data)
return f3()
}).then((data) => {
console.log(data)
}).catch((err) => {
console.log(err)
})
// 输出:(1s later)1 (1s later)error!
// 此时不再执行f2后面的f3,因为f2依旧出现错误,没有再执行下去的必要了
下面用catch方法改写上面1.5 reject用法里面的例子:
p.then((data) => {
console.log(data);
console.log('这是成功操作');
}).catch((reason) => {
console.log(reason);
console.log('这是失败的操作');
});
还有,执行resolve的回调时,如果抛出异常(代码出错了),那么并不会报错卡死,而是会进到这个catch方法中:
getNumber()
.then(function (data) {
console.log('resolved');
console.log(data);
console.log(somedata); //此处的somedata未定义
})
.catch(function (reason) {
console.log('rejected');
console.log(reason);
});
上述代码如果进入resolved的回调中,那么输出:
resolved
3 // 随机
rejected
ReferenceError: somedata is not defined
在resolve的回调中,我们console.log(somedata);
而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,错误进到catch方法里面去了,而且把错误原因传到了reason参数中。这样一来,即便是有错误的代码也不会报错了。
7 all
Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。
Promise
.all([fn1(), fn2(), fn3()])
.then(function (results) {
console.log(results);
});
// output
异步任务1执行完成
异步任务2执行完成
异步任务3执行完成
[ '数据1', '数据2', '数据3' ]
用Promise.all来执行,all接收一个数组参数,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。三个异步操作返回的数据都在then里面,all会把所有异步操作的结果放进一个数组中传给then,就是上面的results。
8 race
// 把fn1、fn2、fn3中的时间分别改为1000,2000,2000
Promise
.race([fn1(), fn2(), fn3()])
.then(function (results) {
console.log(results);
});
// output
异步任务1执行完成
数据1
异步任务2执行完成
异步任务3执行完成
在then里面的回调开始执行时,fn2()和fn3()并没有停止,仍旧再执行。于是再过1秒后,输出了他们结束的标志。
比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作:
//请求某个图片资源
function requestImg() {
var p = new Promise(function (resolve, reject) {
var img = new Image();
img.onload = function () {
resolve(img);
}
img.src = 'xxxxxx';
});
return p;
}
//延时函数,用于给请求计时
function timeout() {
var p = new Promise(function (resolve, reject) {
setTimeout(function () {
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise
.race([requestImg(), timeout()])
.then(function (results) {
console.log(results);
})
.catch(function (reason) {
console.log(reason);
});
requestImg函数会异步请求一张图片,我把地址写为"xxxxxx",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。
9 Promise的作用
首先我们来看这样一个例子,取4个定时器,设置延迟时间都为1s,然后每隔1s依次在控制台输出1 2 3 4的字样:
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
setTimeout(() => {
console.log(4)
}, 1000)
}, 1000)
}, 1000)
}, 1000)
上面的问题就是,回调函数的嵌套有点多,使代码的可读性和可维护性都大大降低了。
这时如果我们使用Promise(链式操作)去实现这个效果,就能大大增强其可读性和可维护性:
function f1() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(1);
}, 1000)
})
}
// f2 f3 f4 同f1,省略
f1().then((data) => {
console.log(data)
return f2()
}).then((data) => {
console.log(data)
return f3()
}).then((data) => {
console.log(data)
return f4()
}).then((data) => {
console.log(data)
})
在这个例子中,将得到Promise实例的过程封装成一个函数(f1~f4)并返回一个Promise实例,再用实例去调用相应的then方法,在每个then方法中通过return得到下一级的Promise实例,然后再去调用then方法执行里面的操作,再返回下一个Promise对象。
也可以这么写,代码量更少:
function fn(n) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(n + 1)
}, 1000)
})
}
function f1(n) {
console.log(n);
return fn(n)
}
f1(1)
.then(v => f1(v))
.then(v => f1(v))
.then(v => f1(v))
// async/await 方法
async function doit() {
const t2 = await f1(1)
const t3 = await f1(t2)
const t4 = await f1(t3)
const res = await f1(t4)
}
doit()
实例:Promise封装ajax(这里用setTimeout模拟)
setTimeout(() => {
console.log(1);
}, 1000)
// 实际开发中一般不会把new Promise直接暴露在外面,而是封装成一个函数,比如这里的fn
function fn() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
});
return p
}
fn().then((data) => {
console.log(data)
})
多个异步操作:
function p(v) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(v)
}, v * 1000)
})
}
Promise.all([
p(1), p(2), p(3)
]).then(function (result) {
console.log(result);
})
// (3s later)[1,2,3]
有的时候有这样的需求:后面的ajax请求依赖前面的ajax请求必须按照顺序调动,实现方法就是上面的链式写法。
10 注意
Promise是异步的,是指他的then()和catch()方法,Promise本身还是同步的。
setTimeout(()=>{
console.log(1)
},1000)
new Promise(function (resolve, reject) {
console.log(2)
setTimeout(function () {
resolve();
}, 2000);
}).then(() => {
console.log(3);
});
// 输出:2 (1s later) 1 (1s later) 3