前言
在现代 Web 开发中,异步编程是一个相当重要的概念。异步编程是一种编程模式,旨在处理耗时操作而不阻塞主线程。与同步编程不同,异步编程允许程序在等待某些操作完成时继续执行其他任务。JavaScript 中的 Promise 是一种用于处理异步操作的强大工具。本文将深入探讨 Promise 的概念、用法及其在实际开发中的应用。
什么是 Promise?
Promise 是 JavaScript 中用于表示异步操作最终完成(或失败)及其结果值的对象。它可以看作是一个容器,封装了一个未来可能会被填充的值。Promise 有三种状态:
- Pending(进行中):初始状态,既不是成功也不是失败。
- Fulfilled(已成功):操作成功完成,Promise 有了一个结果值。
- Rejected(已失败):操作失败,Promise 有了一个失败原因。
创建 Promise
要创建一个 Promise,可以使用Promise构造函数。该构造函数接受一个执行器函数作为参数,该函数包含两个参数:resolve和reject。当异步操作成功时,调用resolve,当失败时,调用reject。
javascript
const myPromise = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const success = true; // 模拟操作结果
if (success) {
resolve("操作成功!");
} else {
reject("操作失败!");
}
}, 2000);
});
使用 Promise
一旦创建了 Promise,可以使用then()方法来处理成功的结果,使用catch()方法来处理失败的情况。
javascript
myPromise
.then((result) => {
console.log(result); // 输出: 操作成功!这里的result是resolve传递的值
})
.catch((error) => {
console.error(error); // 输出: 操作失败!这里的error是reject传递的值
});
Promise 原理
有限状态机
Promise 本质上是一个有限状态机。它有且仅有三种状态:Pending、Fulfilled 和 Rejected。状态的转换只能从 Pending 到 Fulfilled 或 Rejected,且状态改变是不可逆的,一旦状态改变就不会再变。
执行器与同步执行
当 new Promise(executor) 时,传入的函数executor是立即同步执行的。这意味着 Promise 的状态转换(调用resolve或reject)也是同步发生的。
javascript
const promise = new Promise((resolve, reject) => {
console.log("1. Executor是同步的,立即执行");
// 模拟异步操作
resolve("2. Promise状态变为Fulfilled"); //状态改变
});
promise.then((value) => {
console.log(value);
});
console.log("3. 这行代码在Executor之后执行");
/*
上面的代码输出顺序为:
1. Executor是同步的,立即执行
3. 这行代码在Executor之后执行
2. Promise状态变为Fulfilled
*/
发布-订阅模式
什么是发布-订阅模式?
发布-订阅模式(Publish-Subscribe Pattern)通常简称为 Pub-Sub,是一种消息范式。发布-订阅模式 (Publish-Subscribe Pattern),通常简称为 Pub-Sub,是一种消息范式。用最通俗的话来概括:它是一种"一对多"的依赖关系,让多个观察者(订阅者)同时监听某一个主题对象(发布者),当这个主题对象的状态发生变化时,会通知所有订阅者。这是 Promise 实现异步处理的核心机制。
Promise 中的发布-订阅模式
当你调用promise.then(callback)时,如果 Promise 还处于 Pendding 状态,callback 函数不能被立即执行。所以,Promise 会将 callback 函数存储在一个数组中,这实际上就是订阅。当异步操作结束后,调用resolve(value)或reject(value)时,Promise 会遍历这个数组,依次将 value 传递给每个 callback 函数并执行它们,这就是发布。和一般的发布-订阅模式不同,promise 的发布-订阅稍微特殊一点,它的状态一旦改变(发布了一次),就凝固了。后续再有新的"订阅者"进来(再调用 .then),会直接拿到结果,而不需要再次触发发布动作。
下面是一个简单的示例,展示了 Promise 中发布-订阅模式的实现原理:
javascript
class EventEmitter {
constructor() {
//1.缓存列表,用来存放所有的订阅回调函数
//结构类似于{'click': [fn1, fn2], 'upload':[fn3]}
this.events = {};
}
//2.订阅(Subscribe)
//相当于点击了"关注"按钮
on(eventName, callback) {
//如果还没有这个事件的队列,就初始化一个空数组
if (!this.events[eventName]) {
this.events[eventName] = [];
}
//把回调函数推入队列
this.events[eventName].push(callback);
}
//3.发布(Publish)
//相当于发布了一条动态,通知所有关注我的人
emit(eventName, ...args) {
//取出该事件所对应的所有回调函数
const callbacks = this.events[eventName];
if (callbacks) {
//依次执行回调函数,并传入参数
callbacks.forEach((callback) => {
callback(...args);
});
}
}
//4.取消订阅(Unsubscribe)
//相当于取关
off(eventName, callback) {
if (!this.events[eventName]) return;
//过滤掉要取消的那个函数
this.events[eventName] = this.events[eventName].filter(
(cb) => cb !== callback
);
}
}
//使用示例
const bus = new EventEmitter();
bus.on("eat", (food) => {
console.log(`我正在吃${food}`);
});
bus.on("eat", (food) => {
console.log(`他也想吃${food}`);
});
console.log("还没开饭");
bus.emit("eat", "咖喱饭"); // 发布事件
/*
输出结果:
还没开饭
我正在吃咖喱饭
他也想吃咖喱饭
*/
在上面的代码中,EventEmitter类实现了一个简单的发布-订阅系统。on方法用于订阅事件,emit方法用于发布事件,off方法用于取消订阅。通过这种方式,Promise 能够有效地管理异步操作的回调函数,实现异步处理。
微任务队列
微任务队列导入
上面的例子中提到了队列的概念。在 JavaScript 中,微任务队列(Microtask Queue)是一个特殊的任务队列,用于存放需要在当前执行栈清空后立即执行的任务。即使resolve是同步调用的,.then中的代码也不会立即执行。Promise 的回调函数(通过then和catch注册的函数)会被放入微任务队列中。要解释清楚这个机制,下面我来详细介绍一下 JavaScript 的事件循环(Event Loop)。
JavaScript 的事件循环
JavaScript 是单线程语言,这意味着它一次只能执行一个任务。为了处理异步操作,JavaScript 引入了事件循环机制。可以将三者的关系理解为:银行柜台办业务。
宏任务
宏任务(MacroTask)是"主流"的异步任务,可以理解为银行柜台等待办理业务的一个个独立的客户。每次执行栈(主线程)清空后,事件循环会从宏任务队列中取出下一个宏任务来执行,且每次只能执行一个宏任务,执行完一个后,浏览器会去检查微任务队列,或者进行页面渲染。常见的宏任务包括:
setTimeoutsetIntervalsetImmediate(Node.js 环境)- I/O 操作
- UI 渲染
微任务
微任务(MicroTask)是依附于当前宏任务的紧急任务,它的优先级要高于宏任务,当前宏任务结束后,下一个宏任务开始前,会先清空微任务队列。可以理解为当前正在办业务的客户存完钱后又想办点别的业务,这时候这个客户具有"插队"的权利来办理业务。一旦开始执行微任务,就会把微任务队列清空为止,包括在执行微任务期间新产生的微任务。常见的微任务包括:
Promise的回调函数(通过then和catch注册的函数)MutationObserverprocess.nextTick(Node.js 环境)
事件循环
事件循环(Event Loop)是 JavaScript 处理异步操作的核心机制。它不断地检查执行栈和任务队列,当执行栈为空时,它会从宏任务队列中取出下一个宏任务来执行。在所有同步代码或每个宏任务执行完后,事件循环会检查微任务队列,并执行所有的微任务,如果执行微任务产生了新的微任务,那新的微任务就会被加到队尾并在本轮一起执行完,直到微任务队列为空,然后才会继续处理下一个宏任务。可以将事件循环理解为银行的叫号系统和柜员的工作流程。事件循环的标准流程大致如下:
- 执行栈 (Call Stack): 首先执行主线程中的同步代码。
- 清空微任务队列: 当同步代码执行完毕(或一个宏任务执行完毕),立即检查微任务队列。
- 如果有微任务,全部执行完(清空队列)。
- 如果在执行微任务时产生了新的微任务,继续加到队尾并在本轮一起执行完。
- 渲染 UI (可选): 浏览器判断是否需要更新页面视图。
- 执行一个宏任务: 从宏任务队列中取出一个(最老的)任务执行。
- 回到步骤 2(循环往复)。
事件循环示例
javascript
console.log("1. Script Start (同步)");
//宏任务1
setTimeout(() => {
console.log("4. setTimeout (宏任务)");
Promise.resolve().then(() => {
console.log("5. Promise in setTimeout (微任务)");
});
}, 0);
// 微任务1
new Promise((resolve) => {
console.log("2. Promise Executor (同步)");
resolve();
}).then(() => {
console.log("3. Promise then (微任务)");
});
console.log("6. Script End (同步)");
/*输出结果:
1. Script Start (同步)
2. Promise Executor (同步)
6. Script End (同步)
3. Promise then (微任务)
4. setTimeout (宏任务)
5. Promise in setTimeout (微任务)
*/
| 时间节点 | 正在执行 (Call Stack) | 微任务队列 (Micro Queue) | 宏任务队列 (Macro Queue) |
|---|---|---|---|
| 1. 启动 | Script (Macro #1) | [] |
[] |
| 2. 运行中 | console.log(1)... |
[Promise回调] |
[setTimeout回调] |
| 3. Script 结束 | (空) | [Promise回调] |
[setTimeout回调] |
| 4. 清算微任务 | 执行 Promise 回调 | [] |
[setTimeout回调] |
| 5. 第一轮结束 | (空) | [] |
[setTimeout回调] |
| 6. 启动第二轮 | 执行 setTimeout | [] |
[] |
| 7. 运行中 | console.log(4)... |
[内部新回调] |
[] |
| 8. 宏任务结束 | (空) | [内部新回调] |
[] |
| 9. 清算微任务 | 执行 内部新回调 | [] |
[] |
Promise 链式调用
Promise 支持.then()链式调用,这是因为then()和catch()方法内部会返回一个新的 Promise 实例(不是原来那个),使得可以在前一个 Promise 处理完成后继续处理下一个 Promise。如果前一个then或catch中返回一个值,这个值会被传递给下一个then的回调函数;如果返回一个 Promise,则下一个then会等待这个 Promise 状态改变后再执行。
javascript
const promise = new Promise((resolve, reject) => {
resolve(1);
});
promise
.then((value) => {
console.log(value); // 输出: 1
return value + 1;
})
.then((value) => {
console.log(value); // 输出: 2
return new Promise((resolve) => {
setTimeout(() => resolve(value + 1), 1000);
});
})
.then((value) => {
console.log(value); // 输出: 3 (延迟1秒后)
});
Promise 的简易版原理实现
下面是一个简易版的 Promise 实现,展示了其基本原理:
javascript
class MyPromise {
constructor(executor) {
this.state = 'pending';// 初始状态
this.value = undefined;// 成功的值
this.callbacks = [];//订阅者队列
const resolve = (value) => {
if(this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
//发布:执行所有订阅的回调(需放入微任务队列,此处简化直接调用)
this.callbacks.forEach(cb = > cb.onFulfilled(value));
}
}
const reject = (reason) => {
if(this.state === 'pending') {
this.state = 'rejected';
this.value = reason;
//发布:执行所有订阅的回调(需放入微任务队列,此处简化直接调用)
this.callbacks.forEach(cb => cb.onRejected(reason));
}
}
//立即执行 executor
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
//如果是pending状态,存入订阅队列
if(this.state === 'pending') {
this.callbacks.push({
onFulfilled: val => {
try {
const result = onFulfilled(val);
resolve(result);//处理链式调用
} catch (err) { reject(err); }
}
onRejected: reason => {
try {
const result = onRejected(reason);
resolve(result);
} catch (err) { reject(err); }
}
})
} else if(this.state === 'fulfilled') {
//如果已经是fulfilled状态,利用微任务执行
queueMicrotask(() => {
const result = onFulfilled(this.value);
resolve(result);//处理链式调用
})
}
})
}
}
Async/Await 简介
async 和 await 是基于 Promise 的语法糖,能够让我们用同步代码的风格编写异步代码,这样可以提高代码的可读性。
async 和 await 解决了什么问题?
Promise 虽然解决了"回调地狱"的问题,但引入了链式复杂性,使得代码在处理多个异步操作时任然显得冗长和难以阅读。async/await 通过让异步代码看起来像同步代码,极大地提升了代码的可读性和维护性。
如果链式调用太长
javascript
fetchData()
.then((data) => processData(data))
.then((processedData) => saveData(processedData))
.then(() => console.log("All done!"))
.catch((error) => console.error(error));
这样的代码虽然避免了回调地狱,但仍然显得冗长,且可读性差。
使用 async/await 重写
javascript
async function handleData() {
try {
const data = await fetchData();
const processedData = await processData(data);
await saveData(processedData);
console.log("All done!");
} catch (error) {
console.error(error);
}
}
handleData();
通过使用 async/await,代码变得更加简洁和易读,逻辑流程也更加清晰。
async 和 await 的使用
使用示例
要使用 async/await,只需在函数前加上async关键字,并在需要等待 Promise 结果的地方使用await关键字。
javascript
async function fetchData() {
// 模拟异步数据获取
return new Promise((resolve) => {
setTimeout(() => {
resolve("数据已获取");
}, 1000);
});
}
async function main() {
console.log("开始获取数据...");
const data = await fetchData();
console.log(data); // 输出: 数据已获取
console.log("数据处理完成");
}
main();
async 和 await 原理
async 函数的原理
async关键字的作用是为函数提供一个 Promise 包装器和状态机环境。当一个函数被定义为async函数时,JS 引擎会保证该函数的返回值永远是一个 Promise 对象。如果函数内部返回一个非 Promise 值,JS 引擎会自动将其包装成一个已解决的 Promise。
javascript
async function f() {
return 1;
}
async function g() {
throw new Error("出错了");
}
上面的代码等同于下面的实现:
javascript
function f() {
return Promise.resolve(1);
}
function g() {
return Promise.reject(new Error("出错了"));
}
await 的原理
await是实现暂停和恢复的关键。当 JS 引擎遇到await时,它会执行以下三个核心步骤:
等待 Promise 结果
引擎首先评估await右侧的表达式,如果右侧的是一个非 Promise 值(如 await 10),引擎会立即将其视为Promise.resolve(10),并以微任务的形式继续执行(当在大多数现代 JS 引擎中,这种情况下会优化成同步执行)。如果右侧是一个 Promise 对象,JS 引擎会"暂停"当前 async 函数的执行,指导该 Promise 状态确定。
让出控制权(Yield)
这是await关键的动作。首先引擎暂停函数,将当前 async 函数的执行状态保存在内部的"状态机"中,标记为暂停。然后让出线程,主线程立即恢复执行async函数后面的同步代码。这保证了await不会阻塞整个 JS 进程。
作为微任务恢复执行
当await等待的 Promise 状态确定(无论是 fulfilled 还是 rejected)后,引擎会调度,将async函数中await后面的所有代码作为一个回调,放入微任务队列。之后在 Event Loop 的下一个微任务清算周期,该回调被执行,async函数从上次暂停的地方恢复执行,并拿到 Promise 的结果。
核心机制
要真正理解 async 和 await 的原理,需要知道它在底层是如何被转换的。在概念上,async/await 代码被编译成了一个使用Generator函数和Promise包装的函数。
转换示例
javascript
async function fetchUser() {
const id = await getId(); //暂停点1
const info = await getInfo(id); //暂停点2
return info;
}
// 转换后等同于:
function fetchUser() {
//1.返回一个Promise包装整个逻辑
return new Promise((resolve, reject) => {
//2.启动一个Generator状态机
const generator = actualFunctionGenerator();
//3.核心驱动函数
function step(nextPromise) {
const next = generator.next(nextPromise); //启动/恢复函数
if (next.done) {
//如果运行到函数末尾,Promise成功
return resolve(next.value);
}
//如果遇到yield(相当于await),继续驱动
Promise.resolve(next.value).then(step, reject);
}
//首次运行
step();
});
}
这里await对应着yield,标记暂停点,让出控制权。Promise.resolve.then()对应着恢复执行,确保await后面的代码以微任务的形式被调度。因此我们可以得出,async/await 的原理就是引擎将同步的写法,在编译阶段高效地转化成了基于 Promise 和微任务的异步流程控制。
结语
到此为止,我们已将 JavaScript 中的 Promise 及其相关概念进行了全面的介绍。从 Promise 的基本概念、状态管理,到其背后的发布-订阅模式和微任务队列机制,再到 async/await 的语法糖及其原理,我们深入探讨了异步编程在 JavaScript 中的实现方式。现在我们可以把这三个概念穿通一下:Event Loop 是底层机制->Promise 是核心对象->Async/Await 是最佳写法。希望本文对你有所帮助。