为什么要用promise?
promise是ES6中用来处理 异步操作 的。如果不使用promise,就需要使用 回调函数来处理异步操作后的结果,此时如果有 多个有顺序 的异步操作,会造成回调的嵌套,引发 回调地狱。
举个例子,如果有两个异步操作(执行需要时间),分别是 砍柴 和 烧水,我们不妨将他们定义为下面的两个函数:
// 砍柴需要时间2s, 是个异步操作
function kanChai(fn) {
setTimeout(() => {
fn();
}, 2000)
}
// 烧水需要时间1s, 是个异步操作
function shaoShui(fn) {
setTimeout(() => {
fn();
}, 1000)
}
其中fn
是回调函数,定义了砍柴或烧水这两个异步操作 执行完成后 要做的事情。
如果现在要求这两个异步操作必须按顺序执行,先砍柴再烧水,可不可以这么写呢?
(function func() {
// 先砍柴
kanChai(() => {
console.log("砍柴完成")
});
// 后烧水
shaoShui(() => {
console.log("烧水完成");
})
})();
这么写的话,其实砍柴和烧水两个函数的执行在func
函数中是同步的,两个动作会同时进行,并且由于烧水用时短,会先执行完毕输出"烧水完成"。显然并不是我们要的结果。
正确的写法应该把烧水嵌套在砍柴内:
(function func() {
kanChai(() => {
console.log("砍柴完成");
shaoShui(() => {
console.log("烧水完成");
})
})
})();
这样执行的结果是2s后输出"砍柴完成",接着1s后输出"烧水完成"。
相信大家很容易看出这种方式的弊端:如果有 多个有顺序的 异步操作,则会不停的嵌套,造成 回调地狱:即 砍柴完成后要做的事情(砍柴的回调) 和 烧水完成后要做的事情(烧水的回调) 被堆到了一块,造成代码可读性和可维护性极差。
而promise的出现,将 砍柴完成后要做的事情 和 烧水完成后要做的事情 从代码层面上分离开来了。
怎么用promise?
我们把上面的那个例子用promise
来重新写一下,不过要对两个函数修改一下,将回调函数去掉,用promise
来代替。
function kanChai() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("砍柴完成");
}, 2000)
})
}
function shaoShui() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("烧水完成");
}, 1000)
})
}
在砍柴和烧水两个函数中返回了 包裹着异步代码的Promise对象
,以便于我们可以调用then方法
。then方法
会接收resolve
传递的数据,对该数据进行进一步操作。
kanChai().then((data) => {
console.log(data);
})
在只有一个异步操作时,还看不出优势。但是如果要实现先砍柴,后烧水,就可以这么写。
kanChai().then((data) => {
console.log(data);
return shaoShui();
}).then((data) => {
console.log(data);
})
通过这种链式调用,即可实现2s后输出"砍柴完成",再过1s输出"烧水完成"。如果还有一些其他的异步操作(如煮饭,盛饭等)在他们之后执行,则是这是的:
kanChai().then((data) => {
console.log(data);
return shaoShui();
}).then((data) => {
console.log(data);
return zhufan();
}).then((data)=>{
console.log(data);
return chengfan();
}).then(...)
从这里就可以看出来promise和普通回调函数的区别:
- 普通回调函数将"取数据"和"操作数据"两个动作封装到了一块。
- Promise将"取数据"这样的异步动作放在了Promise内,而"操作数据"动作被放到了then方法中,实现了"取数据"和"操作数据"的解耦。
上面的代码暂时看不懂没事,只要知道使用Promise能带来的好处:将本应该在回调函数中处理的代码提取到了外面,避免了回调函数的嵌套,使得代码更可读可维护。接下来讲一下Promise到底内部做了什么。
promise原理?
通过new Promise构造器创建的promise对象是 有状态的,状态可能是
- pending(进行中),
- resolved(已完成,也称为fullfilled),
- rejected(已失败)
let p = new Promise((resolve, reject) => {
setTimeout(function () {
resolve('11');
}, 1000)
})
当实例化promise对象后,定时结束前,Promise对象状态为pedding。
一旦定时结束,Promise对象状态由pending切换为fulfilled。
细心的同学可能还注意到了,Promise内部除了状态,还多了个结果字符串"11"。这个结果就存了Promise中resolve要传出的值。
其实promise即承诺,意思是在 未来某一个时间点 承诺返回 数据 给你。因此Promise除了需要记录当前状态,还要记录传出去的数据。
resolve()方法
resolve方法有两个作用:
- 将这个promise对象的状态从 pending 变为 resolved。
- 将异步操作的结果data,作为参数传递出去。
一旦promise对象的状态从pending变成resolved,就调用then方法中的第一个回调函数。所以 调用resolve方法其实就是在调用then方法。
then()方法
then方法接收的参数同样是一个函数,这个函数的作用是对异步操作成功后resolve传出的数据进行处理。
这里值得一提的是then()方法的返回值:then方法的返回值也是一个Promise对象。
function kanChai() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("砍柴完成");
}, 2000)
})
}
let p = kanChai().then((data) => {
console.log(data);
})
此时的p是个没有Result的Promise对象。
如果我们在then方法中返回一个普通字符串。
let p = kanChai().then((data) => {
console.log(data);
return "你好"
})
2s过后,此时p对象的Result不再为undefined。
事实上,即使then()方法中返回的是"你好"字符串,内部也同样会将该字符串包装成一个Promise对象返回。
正是由于then()方法也返回Promise对象这个特性,使得我们可以链式调用.then方法。
kanChai().then((data) => {
console.log(data);
return shaoshui();
}).then((data) => {
console.log(data);
})
如果直接返回一个Promise对象,不就是最开始举的那个例子吗,是不是看懂了。
其他
刚刚其实只讲了Promise中的异步代码执行成功以后的处理,如果异步代码执行失败,则会用到一个与resolve函数对应的 reject函数。
let p = new Promise((resolve, reject)=>{
resolve("执行成功的数据");
reject("执行失败的提示")
})
then方法的第二个参数也是一个函数,这个函数用来对错误信息的处理。
p.then((data)=>{
对data的处理...
}, (err)=>{
对错误的处理...
})
当有多个Promise中的异步请求之间没有执行顺序时,我们可以使用Promise.all方法来包裹这些Promise。
Promise.all([
new Promise(...),
new Promise(...),
...
]).then(result=>{
console.log(result[0]);
console.log(result[1]);
})