Promise基础

Promise 是JavaScript异步编程的一种解决方案。

异步编程

同步与异步

在JavaScript中,许多任务的运行的模式是同步(synchronous)的,代码按照顺序一行一行执行,执行完一个任务再执行下一个。

但是,有些任务不适合这种运行模式:

  • 发起一个网络请求,得到响应后对返回数据进行一些处理

  • 加载一个script文件,文件加载完成之后调用script文件中的一些方法

由于这些任务的执行耗时可能较长,导致排在它后面的任务需要等待它完成之后才能执行,这就形成了阻塞(blocking)。

一些语言对此类需要耗时的任务的解决方式是多线程。而JavaScript是单线程执行的,它对此类任务的解决方式是异步(asynchronous)。

JS异步操作的几种方式

  • 回调函数

  • 事件监听

  • 发布/订阅模式

  • Promise

Promise的意义

我们使用一个通过ajax发送请求的例子:

$.ajax({
url: "http://localhost:8080/data.json",
success(data) {
// 处理data的逻辑
console.log('处理返回数据');
},
});

处理data的逻辑,需要放在success函数体内,否则这段代码是访问不到data的。这样会导致处理数据的代码与发送请求代码耦合 ,这又会带来另一个问题:如果再发起一个请求,需要用到data,这个请求代码需要写在success内,多层嵌套会出现回调地狱。

使用Promise的话,上面的代码可改写为:

const p = new Promise((resolve, reject) => {
$.ajax({
url: "http://localhost:8080/data.json",
success(data) {
resolve(data); // 这里不关心对数据的处理逻辑,只改变p的状态
},
});
});

p.then(data => {
// p成功之后,处理data
})

Promise不关心请求成功之后对数据的处理逻辑,它只负责用resolve或者reject来改变Promise状态。

promise直译为承诺,我们可以这样理解:

const p = new Promise((resolve, reject) => {
// 进行一些异步操作
// 承诺在某些情况下(例如网络请求成功)调用resolve
// 承诺在某些情况下(例如请求失败)调用reject
});

// 兑现承诺
p.then(res => {}, error => {})

使用Promise

创建Promise

let p = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */) {
resolve(value);
} else {
/* 异步操作失败 */
reject(new Error());
}
})

Promise构造方法接收一个函数参数,叫** 处理器函数** (executor function),处理器函数接收两个函数参数:resolvereject。当异步任务顺利完成且返回结果值时,会调用 resolve 函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject 函数。

注意,处理器函数中的代码是立即执行的:

new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
})
console.log('finifsh')

// 先输出'new Promise', 然后输出'finifsh'

处理器函数中可以放那些耗时长的任务,然后等任务完成再使用resolvereject改变状态。

Promise的状态

Promise对象通过自身的状态来控制异步操作。Promise实例具有三种状态:

  • 异步操作未完成(pending

  • 异步操作成功(fullfilled

  • 异步操作失败(rejected

这三种状态的变化途径有两种:

  1. 从未完成变成成功,Promise实例会传回一个值,状态变为fullfilled

  2. 从未完成变成失败,Promise实例会抛出一个错误,状态变为rejected

注意,Promise的状态由pending变为fullfilled / rejected二者之一后,就已敲定了,不能再改变。

JSX
new Promise((resolve, reject) => {
resolve('success')
reject('reject') // 无效
})

Promise实例的then方法,用来添加回调函数。then方法接收两个参数,第一个是异步操作成功时的回调函数,第二个是异步操作失败时的回调函数(可以省略)。

// 异步操作成功时(fullfilled),会执行f1
// 异步操作失败(rejected),会执行f2
p.then(f1, f2);

链式操作

then函数返回一个新的Promise实例,因此,可以链式操作Promise:

ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))

这解决了回调地狱的问题。

then返回值的处理技巧

let p1 = new Promise((resolve, reject) => {
resolve("p1成功了");
});
let p2 = p1
.then(
(msg) => {
return "p2成功了";
},
(error) => {
console.log(error);
}
)

p2p1.then()返回的新的Promise对象,那么p2的状态是什么呢?

  • 如果p1的回调返回一个字符串,那么p2的状态是成功;

  • 如果p1的回调返回一个Promise对象,那么p2的状态就是返回的这个Promise对象的状态。

Promise.all

Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例。

JSX
const p = Promise.all([p1, p2, p3])

p的状态由p1p2p3的状态共同决定,具体规则是:

  1. 只有p1p2p3的状态都为fullfilled,p的状态才为fullfilled,此时p1p2p3的返回值组成一个数组,传给p的回调函数;

  2. 只要p2p2p3之中有一个被rejected,p的状态就变为rejected,此时第一个被reject的实例的返回值,会传给p的回调函数。

Promise.race

Promise.race()all()类似,同样是将多个Promise实例包装成一个新的Promise实例。

JSX
const p = Promise.race([p1, p2, p3])

p的状态变化规则:

只要p1p2p3之中有一个实例率先改变状态,p的状态就会跟着改变。那个率先改变的Promise实例的返回值,会传给p的回调函数。

错误处理

使用promise.prototype.catch()捕获异常。如果Promise对象的then方法中没有传失败状态执行的回调函数,那么可以在.catch()中来捕获失败时抛出的异常。

promise.prototype.finally()则是无论成功或失败都会执行的回调函数。

async和await

asyncawait是Promise的语法糖。

给一个函数加上async关键字,这个函数返回的就是一个Promise对象。

async function test() {
return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"} // 默认是resolved状态

await 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值一样,并且 await 只能配套 async 使用。

async function test() {
let value = await sleep()
}

asyncawait能更一步优化回调地狱。Promise是把层层嵌套的回调“展开”成扁平的链式调用,async / await把链式调用都省了,让我们可以像写同步代码那样写异步代码。

async getLessons(username) {
let user = await ajax(url, username);
let lessons = await ajax(url, user.id); // 上一个promise resolve后,才会执行这个
}

参考

MDN - Promise

MDN - async函数

Author: kpt

Permalink: http://kpt.ink/2021/10/03/Promise-Basic/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。