一、了解进程与线程
在不同场景中,进程都可以用来描述该场景中的一个效果,线程是进程里面的一个更小的单位,通常多个线程配合工作构成一个进程。
- 进程:操作系统资源分配的基本单位,每个进程拥有独立的内存空间和资源。一个进程可以有多个线程。比如在Windows系统中,一个运行的
xx.exe就是一个进程。它是一个运行中的程序实例,负责完成特定任务。进程之间相互独立,切换时需要较大的系统开销。 - 线程:进程中的执行单元,CPU调度的基本单位。线程共享进程的内存和资源,但每个线程有自己的栈和程序计数器。线程切换的开销较小,因此被称为轻量级进程。
二、v8 引擎的异步执行机制
js 默认是单线程的语言,因为 js 设定是为了做浏览器的脚本语言,尽量少的开销用户设备的性能。因为 js 的这种执行规则,导致我们在开发过程中时而会出现代码异步的情况。
- 同步代码:按顺序执行,阻塞后续代码直至完成。
- 异步代码:被挂起并放入任务队列,待同步代码执行完毕后进入微任务/宏任务队列。
v8运行一份js代码,会创建一个进程,从上往下执行代码,遇到同步代码就直接执行,遇到异步代码就跳过,先去执行后面的同步代码,等到后面的同步代码全部执行完毕后,再回过头执行异步代码。
以相亲函数案例说明传统模式的缺陷:
function date() {
setTimeout(() => {
console.log("相亲成功");
return true;
}, 1000);
}
function marry() {
console.log("结婚");
}
date();
marry();
上述代码执行结果为结婚 相亲成功,其根本原因是v8引擎跳过异步代码继续执行后续同步代码,导致逻辑顺序错乱。
三、回调函数:异步编程的基础
1. 什么是回调函数?
回调函数是指将一个函数作为参数传递给另一个函数,并在外部函数内部调用这个传入的函数。在异步编程中,回调函数通常用于处理异步操作完成后的结果。
function fetchData(callback) {
console.log("开始获取数据...");
setTimeout(() => {
const data = "这里是从服务器获取的数据";
callback(data); // 当数据获取完成后,调用回调函数
}, 2000); // 模拟延迟2秒
}
function handleData(data) {
console.log("获取的数据:", data);
}
// 调用fetchData并传入handleData作为回调
fetchData(handleData);
在这个示例中,fetchData函数模拟了从服务器获取数据的过程,使用setTimeout模拟网络延迟。callback参数是我们传入的处理函数,当数据准备好后,通过callback(data)调用这个处理逻辑。
2. 回调函数的问题
早期浏览器广泛支持回调函数,适合简单的异步操作。但当嵌套过深时,代码的可读性差,维护困难,排查问题困难,形成回调地狱。
回调地狱:多层嵌套导致代码可读性灾难。
API1(() => {
API2(() => {
API3(() => {
// 业务逻辑
});
});
});
代码结构随异步层级增加呈"括号金字塔"形态,严重降低维护性。
四、Promise:异步编程的救星
1. Promise的基本概念
promise 是 es6 新增的一个语法,用来解决回调地狱的问题。promise 是一个构造函数,用来封装一个异步操作,并且可以获取到异步操作的结果。
promise 有三种状态: 待定态(pending)、兑现态(fulfilled)、拒绝态(rejected)。
-
pending:初始状态,既不是成功,也不是失败;
-
fulfilled:操作成功完成,通过
resolve方法传递结果。 -
rejected:操作失败,通过
reject方法传递错误信息。
promise 状态的改变只有两种: 从待定态到兑现态或者从待定态到拒绝态。一旦状态变为 fulfilled 或 rejected,就不会再变(不可逆性)。
function fetchData() {
return new Promise((resolve, reject) => {
console.log("开始获取数据...");
setTimeout(() => {
const data = "这里是从服务器获取的数据";
resolve(data); // 成功,返回结果
// 如果出错可以调用 reject(error)
}, 2000);
});
}
// 调用 fetchData 并处理返回值
// 1. 执行 fetchData 函数,立即返回一个promise实例对象,但是此时该对象的状态是 pending(等待状态)
// 2. .then 立即触发,但是 then 里面的回调函数没有触发
// 3.等待 fetchData 函数里面的 resolve() 执行完毕,此时实例对象的状态会变更为 fulfilled (成功状态),此时 .then 里面的回调函数会触发执行
fetchData()
.then((data) => {
console.log("获取的数据:", data);
})
.catch((error) => {
console.error("发生错误:", error);
});
在这个实例中,fetchData返回一个Promise对象。在Promise内部,我们仍然使用setTimeout模拟异步操作。数据成功时调用resolve(data),将结果传递给.then()中的方法;如果发生错误,则调用reject(error)并通过.catch()捕获。
Promise.then() 方法返回一个新的 Promise 实例,从而支持链式调用。如果回调函数返回一个值,新Promise将以该值为结果resolve。
2. Promise的核心优势
| 特性 | 传统回调 | Promise机制 |
|---|---|---|
| 代码结构 | 嵌套层级加深 | 链式调用(.then().then()) |
| 错误处理 | 分散在各回调中 | 统一错误捕获(.catch()) |
| 并行操作 | 手动协调 | Promise.all()统一管理 |
| 状态追踪 | 黑箱操作 | 明确的pending/resolved状态 |
五、展望:从Promise到async/await
async、await本质上是生成器(Generator)与Promise的组合封装。
async 函数:用 async 声明的函数会自动返回一个 Promise 对象。无论函数内部返回的是否是Promise,都会被包装成 Promise。
async function example() {
return 42; // 自动包装为Promise.resolve(42)
}
await 表达式:await 会暂停当前 async 函数的执行,等待后面的Promise完成。如果是非Promise值,会使用Promise.resolve()包装。
async function foo() {
const result = await somePromise; // 暂停直到 somePromise 解决
return result;
}
相比Promise链式调用,async/await使异步代码呈现同步代码的线性结构,消除了回调地狱问题。
// Promise链式调用
fetchData()
.then(data => processData(data))
.then(result => displayResult(result))
.catch(error => handleError(error));
// async/await版本
async function handleData() {
try {
const data = await fetchData();
const result = await processData(data);
displayResult(result);
} catch (error) {
handleError(error);
}
}