Promise 实现 JS 异步编程

三葉Leaves Author

[!quote] promise 用作什么事情?
要创建一个进程,其中一个函数只有在另一个函数完成后才会触发,请参见关于 Promise 的文档。
——Window:setTimeout() 方法 - Web API | MDN

基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const promise = new Promise((resolve, reject) => {
if (true) {
setTimeout(() => {
resolve("我是传给 .then 的值,会被作为 .then 的参数");
}, 2000);
} else {
reject("我是传给 .catch 的值,会被作为 .catch 的参数");
}
});

promise
.then((params) => {
console.log(params); // 输出:我是传给 .then 的值,会被作为 .then 的参数
})
.catch((params) => {
console.log(params); // 输出:我是传给 .catch 的值,会被作为 .catch 的参数
});

对上面这段代码详细的事件循环分析,请见 用事件循环详细分析一个简单的 promise 案例

  • 更常见的一种结构:放在一个函数里作为返回值。
1
2
3
4
5
6
7
8
9
10
11
const axios = (params) => {
new Promise((resolve, reject) => {
// ...
});
};

// 之后

axios(data)
.then(( ... ) => { ... })
.catch(( ... ) => { ... });

很多库之类的都是基于这个结构封装。

Promise 内部

promise 用于处理异步函数,有几个关键点:

  1. Promise 的构造函数 new Promise(executor) 中的 executor 函数 (resolve, reject) => { ... }立即同步执行的。

  2. promise 内部一般会使用两个方法:

  • resolve()
    成功时候使用,对应 已兑现(fulfilled) 状态

  • reject()
    失败时候使用,对应 已拒绝(rejected) 状态

在这两个方法使用之前,promise 会处在 待定(pending) 状态。

在 Promise 内部一旦执行到这两个方法其中的一句,promise 的状态就会被敲定且不可再被更改。敲定以后,之前注册的 .then 或者 .catch 里的代码会立即被放入微任务队列里等待执行

因为这个原因,使用 Promise 实现了异步转同步:

  1. 把耗时的任务放进 Promise 内部,先不关心它到底能不能成功执行,而是继续执行其他的同步代码;

  2. 等到耗时的任务执行完毕,自然会触发后面的 .then() 或者 .catch() 。再结合多个 .then() 的链式调用(因为其中一个敲定后才会触发后一个),就可以把异步任务转成同步任务理解了,如此一来还解决回调地狱(callback hell)的问题。

.then 和 .catch

  • 成功或失败以后做什么?

刚刚说到,创建对象实例的时候,promise 里的代码会立刻执行。之后在成功或者失败后,我们会在 resolve() 或者 reject() 里放数据。那这个数据有什么用呢?

创建的实例会有 .then().catch() 两个方法,用于定义上文 promise 内部的函数执行成功或者失败之后,程序需要执行的后续操作。

  • .then() 方法里可以定义一个形参,这个形参等于上文中 resolve() 括号里的东西。resolve() 括号里一般会塞关键的结果数据。
  • .catch() 里的形参等于上文中 reject() 括号里的东西,这里面什么都能塞,如果塞的是字符串,那后面 .catch(e) 里的形参 e 也会是一个字符串。但是尽管如此,通常塞一个 Error 对象更合适。

在成功或者失败后,.then() 或者 .catch() 里的回调就会被放进微任务队列等待执行。
promise 巧妙的点就在于此,就像埋下了一个约定或者承诺,并且提前说好履约后会怎样,如此一来就不需要等待了,从而实现异步。

实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function fetchData(success = true) {
return new Promise((resolve, reject) => {
console.log("开始请求数据...");
// 模拟请求需要 2 秒钟
setTimeout(() => {
if (success) {
resolve({ code: 200, data: "这里是返回的数据" });
} else {
reject(new Error("请求失败:网络错误"));
}
}, 2000);
});
}

fetchData(true)
.then(result => {

// 请求成功: { code: 200, data: '这里是返回的数据' }

console.log("请求成功:", result);

// 继续处理下一个操作
return "处理完毕";
})
.then(nextStep => {

// 输出:下一步操作: 处理完毕

console.log("下一步操作:", nextStep);

})

.catch(err => {

// 输出:请求出错: 请求失败:网络错误

console.error("请求出错:", err.message);
})
.finally(() => {
console.log("请求结束,不管成功或失败都会执行这里");
});

链式调用

上面的代码中可以看到居然有多个 .then() 语句,他们之间的关系是什么?

我们用一个“接力赛”的比喻来理解。每个 .then() 都是赛道上的一站,它接收上一站传来的“接力棒”(数据),处理完后,再把一个新的“接力棒”传给下一站。

[!tip] 试想
每一段接力跑是耗时的任务,就像实际开发中的网络请求一样。把异步转同步的关键就在于设下一个个“接力站”,一段跑完成了才开始下一段跑,如此一来一整段赛跑看起来就变成了一个同步任务一样。

核心原则

promise.then() 本身会返回一个全新的 Promise。

这个新 Promise 的状态(是成功 fulfilled 还是失败 rejected)以及它的最终值,完全由你在 .then() 的回调函数中的 return 语句来决定。


场景 1: 返回一个基本数据类型或对象 (非 Promise 值) 或者 不返回

这是最简单、最常见的情况。

关系
当你从一个 .then() 回调中返回一个普通的值(如数字、字符串、布尔值、nullundefined 或一个普通对象),这个值会被“包装”成一个立即成功的 Promise。然后,这个“包装”好的值会作为参数,传递给下一个 .then() 的回调函数。

[!note] 总结
返回值是普通值或者没有 return 语句的时候,下一个 .then 会立即接收到上一个 .then 的返回值作为参数(即使是 undefined)。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Promise.resolve(10) // 初始Promise,成功状态,值为 10
.then(num => {
console.log("第一站接到:", num); // 输出: 第一站接到: 10
// 处理后,返回一个基本数据类型
return num * 2;
})
.then(num => {
console.log("第二站接到:", num); // 输出: 第二站接到: 20
// 处理后,返回一个对象
return { value: num, message: "完成" };
})
.then(data => {
console.log("第三站接到:", data); // 输出: 第三站接到: { value: 20, message: "完成" }
// 如果没有 return 语句,相当于 return undefined;
})
.then(lastData => {
console.log("第四站接到:", lastData); // 输出: 第四站接到: undefined
});

场景 2: 返回一个新的 Promise

这是实现异步操作串联转为同步的核心。

关系
如果一个 .then() 回调返回了一个新的 Promise,那么整个 Promise 链会“暂停”,等待这个新的 Promise 状态落定(即 fulfilledrejected)。

[!warning] 这里的”暂停“是关键

  • 如果新的 Promise 成功 (fulfilled):它的成功值(resolve() 里的玩意)会作为参数,传递给链中的下一个 .then()
  • 如果新的 Promise 失败 (rejected):它的失败原因会传递给链中的下一个 .catch() (或 .then 的第二个参数)。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.resolve("开始处理")
.then(message => {
console.log("第一站:", message); // 输出: 第一站: 开始处理

// 返回一个新的 Promise,模拟一个异步操作
return new Promise(resolve => {
console.log("-> 启动了一个2秒的异步任务...");
setTimeout(() => {
resolve("异步任务的结果");
}, 2000);
});
})
.then(asyncResult => {
// 🌟 这个 .then 会等待上面那个新的 Promise resolve 之后才执行
console.log("第二站接到:", asyncResult); // 2秒后输出: 第二站接到: 异步任务的结果
return "链式调用结束";
})
.then(finalMessage => {
console.log("第三站接到:", finalMessage); // 立即输出: 第三站接到: 链式调用结束
});

总结:下一个 .then执行时机和接收到的值,完全由上一个 .then 返回的那个新 Promise 决定。这就是 Promise 链能够“等待”异步操作完成的原理。


场景 3: 返回一个函数

关系
在 JavaScript 中,函数也是一种对象。因此,返回一个函数和返回一个普通对象遵循同样的规则(场景 1)。函数本身会被当作一个值,传递给下一个 .then,而不会被执行

[!note] 总结
返回一个函数时,下一个 .then 接收到的参数是这个函数本身,而不是它的执行结果。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Promise.resolve()
.then(() => {
console.log("第一站:准备返回一个函数");
const myFunction = () => "Hello from function!";
return myFunction; // 返回的是函数本身,而不是它的执行结果
})
.then(receivedValue => {
console.log("第二站接到的值类型:", typeof receivedValue); // 输出: 第二站接到的值类型: function
console.log("第二站接到的值:", receivedValue); // 输出: 第二站接到的值: [Function: myFunction]

// 如果想执行它,需要自己调用
const result = receivedValue();
console.log("手动调用接收到的函数:", result); // 输出: 手动调用接收到的函数: Hello from function!
return result;
})
.then(finalResult => {
console.log("第三站接到:", finalResult); // 输出: 第三站接到: Hello from function!
});

总结表格

.then 中返回… 行为描述 下一个 .then 的回调函数…
基本数据类型或对象 (e.g., 123, 'abc', {}) 值被“包装”在一个立即成功的 Promise 中。 立即执行,并接收这个值作为参数。
undefined (无 return 语句) 相当于 return undefined;,遵循上一条规则。 立即执行,并接收 undefined 作为参数。
一个新的 Promise 整个链条会等待这个新的 Promise 状态落定。 在新 Promise 成功后执行,并接收其成功值作为参数。
一个函数 函数本身被视为一个普通值。 立即执行,并接收这个函数本身作为参数(而不是函数的执行结果)。
throw new Error() 或一个被拒绝的 Promise 链条中断,寻找错误处理。 不会执行,执行会跳转到最近的 .catch().then(null, onRejected)

理解这个机制是使用 Promise 和 async/await(它其实是 Promise 的语法糖)构建健壮异步代码的基础。

  • 标题: Promise 实现 JS 异步编程
  • 作者: 三葉Leaves
  • 创建于 : 2025-06-17 00:00:00
  • 更新于 : 2025-08-08 09:29:22
  • 链接: https://blog.oksanye.com/9c7fd22a9f27/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论