回调函数
在学习Promise之前,我们先学习一下前置知识,异步和同步,回调函数
同步和异步
举个生活中的例子了解下同步和异步:
早上起来,不论先刷牙还是先洗脸,都要等一个事情完毕后才能进行下一项,这就是同步
把水烧上,不用等水烧开,就去刷牙,烧水不会阻塞刷牙的执行,这就是异步
JavaScript是单线程语言,这意味着它一次只能执行一个任务。但是,它支持同步和异步编程。同步编程意味着每行代码都按顺序一个接一个地执行。异步编程允许程序在等待特定任务完成时执行其他任务。
看一段简单的代码,有一个求和函数和打印函数,求和函数内部有一个定时器,我们来看看输出的结果:
function sum(a, b) {
console.log("执行sum函数")
setTimeout(() => {
console.log("sum函数内的异步任务")
return a + b
}, 1000)
}
function log() {
console.log("执行log函数")
}
let result = sum(1, 2)
log()
console.log(result)
分析上面代码的执行步骤:
-
首先,当代码被执行时,会创建一个调用栈,称为主调用栈。主调用栈中的第一个函数是全局执行上下文。
-
sum函数进入调用栈,开始执行。在函数
sum内部,console.log是同步任务,立即执行输出"执行sum函数"。然后,
setTimeout函数被调用,并且一个箭头函数作为参数被传入。而setTimeout是异步任务,其内部的回调函数会被放到任务队列,等待主调用栈中的任务执行完毕后再进栈执行。 -
函数
log进栈被调用,并输出"执行log函数"。 -
console.log(result)进栈执行,但result接收的是setTimeout中回调函数的返回值,而回调函数还没有执行,因此输出"undefined"。这时,主调用栈中的任务已经全部执行完毕。 -
还记得在任务队列中等待执行的
setTimeout的回调函数吗,事件循环会将队列中的任务移动到主调用栈中执行。回调函数开始执行,输出"sum函数内的异步任务"并返回a+b。
现在定时器是不会阻塞后面代码的执行了,但是又有一个问题,上面代码中用result接收定时器返回的结果,但是输出result时,定时器还在任务队列里排队,没有执行,因此为result为undefined。
我们不知道异步函数结果何时返回,也就无法使用返回值,函数就失去了意义。那么我们如何获取到异步函数返回的结果呢?这就需要回调函数出场了
回调函数
回调函数首先是一个函数,但它会作为参数被传到另一个函数(父函数)中,当父函数执行完毕后,回调函数才会执行
这就解决了我们不知道异步代码何时执行完毕的问题,因为回调函数在父函数执行完毕后才会执行,可以在回调函数中将执行结果赋值给一个变量,并将该变量作为参数传递给父函数。
用回调函数改造之前的代码:
function sum(a, b, cb) {
setTimeout(() => {
const result = a + b
cb(result) // 将结果作为回调函数的参数返回
}, 0)
}
// 通过回调函数的参数获取异步函数的结果
sum(1, 2, (result) => {
console.log(result)
})
在前端开发中,回调函数被广泛应用于处理异步操作。以下是常见的使用场景:
- 定时器:
setTimeout和setInterval函数都接受一个回调函数作为参数。 - 事件监听器:事件监听器常常使用回调函数来响应事件。
- AJAX 请求:在 AJAX 请求中,回调函数被用于处理异步请求的响应。
- Promise 和 async/await:Promise 和 async/await 都是为了处理异步操作而出现的,它们的底层实现也依赖于回调函数。
现在我们可以通过回调函数获取异步函数的结果了,但又有一个问题,假设需要连续调用四个异步函数,且后一个要依赖于前一个的执行结果:
sum(1, 2, result => {
sum(result, 3, result => {
sum(result, 4, result => {
sum(result, 5, result => {
console.log(result)
})
})
})
})
当我们有n个不同的复杂异步函数,又层层嵌套,我们就会陷入”回调地狱“之中,增加了代码的复杂程度,可调式性差。于是,ES6 提出了 Promise,让我们可以更加简洁得处理异步代码。
Promise
我们先给出Promise的定义,它是异步编程的一种解决方案:
顾名思义,Promise是承诺,承诺它过一段时间会给你一个结果。从语法上讲,Promise是一个用来存储存储异步代码执行结果的对象
创建promise
Promise是构造函数,要想使用promise对象,要先new一个实例
const promise = new Promise((resolve, reject) => {})
构造函数接收一个回调函数作为参数,它会在创建Promise时被调用,调用时会传入两个参数,它们都是Promise内部定义好的回调函数:
- resolve:异步操作执行成功后的回调函数,用来传递执行成功后的数据
- reject:异步操作执行失败后的回调函数,用来传递执行出错时的信息
// 在回调中直接调用异步代码,通过resolve或reject函数将异步代码执行结果存储到Promise中
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success")
// reject(new Error("Error message"))
}, 1000)
})
打印看看promise实例,我们可以发现PromiseState都变成了fulfilled,而成功或失败的结果都存储到了PromiseResult中
既然结果都存到PromiseResult中了,那我们可以直接通过promise.PromiseResult获取数据吗?输出的结果还是undefined,因为我们不知道异步代码返回结果的时机,而输出promise.PromiseResult是同步执行的
获取Promise中的数据
then
then是Promise实例上的方法,通过 promise.then 获取到存储在Promsie中的数据
还记得我们是通过回调函数 resolve 和 reject 存储的数据吗?相应的,要获取其中的数据,then的参数也要是两个回调函数来指定resolve和reject,数据会作为回调函数的参数传递:
- resolve传递的数据(data),在第一个回调函数(cb1)的参数中接收,通常我们会在cb1中编写处理数据的代码
- reject传递的出现异常时的错误信息(error),在第二个回调函数(cb2)的参数中接收,通常我们会在cb2中编写处理异常的代码
promise.then(
(data) => {
console.log(data) // resolve传递的数据
},
(error) => {
throw error // reject传递的数据
}
)
我们给了then两个回调函数,then怎么知道要执行哪一个呢?
还记得我们打印过的promise对象吗, 其中有两个的属性:PromiseResult 和 PromiseState ,关键在于 PromiseState ,它代表着promise对象的状态,共有三种状态:
- pending(等待状态):当Promise创建时的初始值
- fulfilled(已完成):通过resolve存储数据时的状态,表示异步操作已成功完成
- rejected(已拒绝):通过reject报错时的状态,表示异步操作失败了
通过then读取数据时,实质上是为promise设置了两个回调函数,根据PromiseState状态的变化决定调用哪个函数:
- PromiseState === pending,什么都不做,等待状态改变,即等待数据存储到promise中,
- PromiseState === fulfilled,调用第一个回调函数
- PromiseState === rejected,调用第二个回调函数
链式调用
现在我们不再需要回调函数来返回异步函数的执行结果了,取而代之的是:
返回一个promise对象,将异步代码写在promise中,执行的结果通过resolve存储到promise中,再then方法获取promise中的数据
*function* sum(a, b){
return new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(a + b)
}, 1000)
})
}
sum(1, 2).then(result => {
console.log(result) // 3
})
但是怎么连续多次调用呢,刚逃出回调地狱,又要陷入promise地狱了吗?
// promise地狱
sum(1, 2).then(result => {
sum(result, 3).then(result =>{
sum(result, 4).then(result => {
console.log(result)
})
})
})
实际上想要实现promise的连续多次调用,只需要进行链式调用,即根据需要一直 .then 下去
sum(1, 2)
.then(result => sum(result, 3)) // return new Promise(sum(result, 3))
.then(result => sum(result, 4))
.then(result => console.log(result))
调用then或catch方法时,在我们看不到的地方,会返回一个新的promise对象,相当于有一行隐藏代码:return new Promise()
then或catch(后面讲)中回调函数的返回值会被存储到这个promise中,如果没有返回值则promise中也没有任何值
每一次.then,读取的都是上一次.then返回的新promise对象中的结果
catch
为了使代码逻辑更加清晰,一般会用promise对象的catch方法代替then中的第二个回调函数
和第二个回调函数的作用一样,catch也是用来指定reject的回调,当PromiseState === rejected,即异步操作失败时被调用
const promise = new Promise((resolve, reject) => {
reject("出错了")
}).then(data => {
console.log('resolved', data)
}).catch(err => {
console.log('rejected', err) // rejected 出错了
})
那在链式调用中怎么处理错误呢?我们先来看一下通过reject存储的错误信息的处理流程:
还记得promise的三种状态吗?在对promise进行链式调用时,then或catch会根据状态决定是否执行。当异步代码执行出错时,状态为rejected,遇到.then,如果then中没有第二个回调函数,则then不会执行,而是将异常信息封装到新的Promise中进行传递,直到异常被then的第二参数或是catch处理,如果一直没有处理,则异常会向外抛出,程序报错
同样的,如果异步代码执行成功,状态为fulfilled,中途遇到catch,也不会执行其中的代码,而是去执行后续.then的第一个回调
简而言之,对Promise进行链式调用时,如果上一步传来的promise的状态不是当前方法(then或catch)想要的状态:
resolve() → PromiseState: fulfilled → then(data ⇒ {})
reject()/ throw new Error → PromiseState: rejected → catch
则跳过当前的方法,这就是Promise的值穿透现象
这种设计方式使得我们可以在任意的位置对Promise的异常进行处理,例如如下代码:
function sum(a, b) {
return new Promise((resolve, reject) => {
if (Math.random() > 0.7) {
throw new Error // 直接抛错和reject()的结果是一样的,状态也会变为rejected
// reject("出错了")
}
resolve(a + b)
})
}
sum(1, 2)
.then((result) => sum(result, 3))
.then((result) => sum(result, 4))
.then((result) => console.log(result))
.catch((err) => console.log(err))
上例代码中,当随机数>0.7时会出现异常,我们无法确定出现异常的时机,但出现异常后所有的then在异常处理前都不会执行,错误信息会存入Promise中向下传递,所以我们可以将catch写在调用链的最后,这样无论哪一步出现异常,我们都可以在最后统一处理
Finally
finally也是Promise的实例方法之一,和then、catch不同,无论异步代码是否执行成功,finally中的回调函数总会执行,通常我们在finally中定义一些无论Promise正确执行与否都需要处理的工作
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success")
// reject(new Error("Error message"))
}, 1000)
})
promise
.then((data) => {
console.log(data)
})
.catch((error) => {
console.log(error)
})
.finally(() => {
console.log("Promise complete")
})
静态方法
Promise类提供了一些静态方法,让我们可以更简洁得操作promise
Promise.resolve
用来创建一个新的Promise实例,且直接通过resolve存入一个数据
Promise.resolve(10)
//等价于
new Promise((resolve, reject) => {
resolve(10)
})
//获取数据
Promise.resolve(10).then(r => console.log(r))
Promise.reject
用来创建一个新的Promise实例,且直接通过reject存入一个数据。
Promise.reject("错误")
//等价于
new Promise((resolve, reject) => {
reject("错误")
})
//获取数据,要通过catch获取
Promise.resolve(10).catch(r => console.log(r))
Promise.all
当我们要同时执行多个Promise,等他们都执行完毕后,再将其结果进行统一处理时,使用Promise.all
Promise.all 需要一个数组作为参数,数组中可以存放多个promise对象。调用后,all方法会返回一个新的promise,这个promise会等待数组中所有的promise都执行完后,将所有promise的结果封装到数组中返回
注意,数组中的prmise对象中只要有一个报错,Promise.all执行时就会报错
*function* sum(a, b) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(a + b)
}, 1000);
})
}
Promise.all([sum(1, 1), sum(2, 2), sum(3, 3)])
.then((result) => {
console.log(result)
})
// [2, 4, 6]
Promise.allSettled
Promise.allSettled也接收一个promise对象组成的数组,并返回一个Promise对象,该对象会在数组里的所有promise都完成后,不论成功或失败,将各自promise的结果封装到对象中,组成数组返回
const promises = [
Promise.resolve(1),
Promise.reject('error'),
Promise.resolve(3),
];
Promise.allSettled(promises)
.then(results => {
console.log(results);
})
/*
[ { status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: 'error' },
{ status: 'fulfilled', value: 3 } ]
*/
Promise.race
顾名思义,所有promise比赛执行速度,会返回首先执行完的Promise的执行结果,而忽略其他未执行完的Promise
Promise.race([
Promise.reject("出错了"),
sum(1, 2),
sum(3, 4)
]).then(res => {
console.log(r)
}).catch(err => {
console.log(err)
})
// 出错了
Promise.any
返回第一个成功的Promise的执行结果,如果所有的Promise都失败才会返回一个错误信息。
Promise.any([
Promise.reject("出错了"),
sum(1, 2),
sum(3, 4)
]).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
//3