一、同步 VS 异步编程
1.同步编程
(1)特点:代码按照自上而下的顺序执行,每一行代码都必须等待上一行代码执行完毕才能开始。
(2) 示例代码:
JavaScript
console.log('第一步:开始做饭');
console.log('第二步:做好了米饭'); // 这行代码会等待第一行执行完
console.log('第三步:做好了菜'); // 这行代码会等待第二行执行完
2.异步编程
(1)特点:代码的执行顺序无关。当遇到一个耗时的操作(比如网络请求、文件读写),程序会先发起这个任务,然后立即跳过它,去执行后面的代码,等到耗时任务完成后,再回头处理它的结果。
(2)示例代码:
JavaScript
console.log('第一步:开始请求数据...');
// 模拟一个异步操作,比如网络请求
setTimeout(() => {
console.log('第二步:数据请求成功!'); // 这行代码会在2秒后执行
}, 2000);
console.log('第三步:我已经不等了,继续执行后面的代码'); // 这行代码会立即执行
二、回调函数与回调地狱
1.回调函数(Callback)
(1)原理:将一个函数作为参数传递给另一个函数,当异步任务完成时,调用这个回调函数来处理结果。
(2)示例代码:
JavaScript
function greet(name) {
console.log(`Hello, ${name}!`);
}
function processUserInput(callback) {
const name = 'Alice';
callback(name); // 调用传入的函数
}
processUserInput(greet); // 将 greet 函数作为参数传入
2.回调地狱(Callback Hell)
(1)原理:当有多个相互依赖的异步任务时,回调函数会一层层嵌套,形成“金字塔”结构.
(2)示例代码:
JavaScript
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doAnotherThing(newResult, function(finalResult) {
console.log('最终结果是:' + finalResult);
});
});
});
(3)问题
难以阅读:代码向右横向拓展,嵌套层级非常深。
难以维护:如果你需要修改某个环节的逻辑,可能要逐层向上或向下追溯。
难以排错:错误处理逻辑散落在每一层回调中,不够集中。
三、Promise 的基础
1.概念:Promise 是一个代表异步操作最终完成(或失败)的对象。
(1)当你发起一个异步请求时,你会立即得到一个 Promise 对象,它代表着这个请求的结果(但结果还没出来)。
(2)这个 Promise 对象承诺你,当异步操作完成时,他会通知你成功的结果(fulfilled),或者失败的原因(rejected)。
(3)在这个结果出来之前,Promise 对象处于一个“待定”的状态。
2.Promise 的状态
(1)Pending(待定):初始状态,异步操作正在进行中。
(2)Fulfilled(已成功):异步操作成功完成,并返回了一个结果值。
(3)Rejected(已失败):异步操作失败,并返回了一个失败的原因。
(4)Promise 的状态一旦从 pending 变为 fulfilled 或 rejected,就不可逆转,也不会再改变。
3.如何创建和使用 Promise
(1)创建 Promise:通过 new Promise()
构造函数来创建一个 Promise 对象。它接受一个函数作为参数,这个函数有两个参数:
resolve
:一个函数,用于在异步操作成功时调用,并将结果作为参数传递。reject
:一个函数,用于在异步操作失败时调用,并将错误作为参数传递。
示例代码:
JavaScript
const myPromise = new Promise((resolve, reject) => {
// 模拟一个异步操作,比如从服务器获取数据
setTimeout(() => {
const success = true; // 假设操作成功
if (success) {
resolve('数据获取成功!'); // 异步操作成功,调用 resolve
} else {
reject('数据获取失败:网络错误'); // 异步操作失败,调用 reject
}
}, 1000);
});
(2)使用 Promise: .then()
和 .catch()
创建 Promise 后,你需要使用.then()
和 .catch()
方法来处理异步操作的结果。
.then()
:用于处理 Promise 成功时的结果。它接收一个回调函数,这个回调函数的参数就是resolve
传递过来的值。.catch()
:用于处理 Promise 失败时的原因。它接收一个回调函数,这个回调函数的参数就是reject
传递过来的错误。
示例代码:
JavaScript
// 使用 myPromise 对象
myPromise
.then((result) => {
// 异步操作成功时执行这里的代码
console.log('成功啦!', result); // 输出: "成功啦! 数据获取成功!"
})
.catch((error) => {
// 异步操作失败时执行这里的代码
console.error('出错了:', error);
});
4.Promise 链式调用:解决回调地狱
(1)Promise 最强大的功能就是链式调用。每个.then()
方法都会返回一个新的 Promise,这允许我们将多个异步操作按顺序串联起来,从而摆脱回调地狱。
(2)当你在一个 .then()
回调函数中返回一个值,这个值会作为下一个 .then()
的参数。如果返回的是一个新的 Promise 对象,那么下一个 .then()
会等待这个新的 Promise 完成。
(3)示例代码:
JavaScript
function step1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('第一步完成');
resolve(10); // 返回一个数字
}, 1000);
});
}
function step2(prevResult) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('第二步完成,拿到上一步结果:', prevResult);
resolve(prevResult * 2); // 返回新结果
}, 1000);
});
}
function step3(prevResult) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('第三步完成,拿到上一步结果:', prevResult);
resolve('所有步骤都完成了!');
}, 1000);
});
}
// 链式调用
step1()
.then((result) => step2(result)) // result 是 step1 的返回值
.then((result) => step3(result)) // result 是 step2 的返回值
.then((finalResult) => {
console.log('最终结果:', finalResult); // finalResult 是 step3 的返回值
})
.catch((error) => {
console.error('链式调用中出现错误:', error);
});
四、async/await
1.概念:async/await是 JavaScript 中处理异步操作的一种现代化、更优雅的方式。它是建立在 Promise 的基础之上的,但通过一种更直观的语法,让异步代码看起来就像同步代码一样。这极大地提高了代码的可读性和可维护性。
2. async
关键字:让函数变为异步
(1)当你把async关键字放在一个函数面前时,这个函数就成为了一个异步函数。
(2)核心特性:async函数会始终返回一个 Promise 对象。
如果你在async函数中返回一个普通值,JavaScript 会自动将其包装在一个成功的 Promise 中
如果你在async函数中抛出一个错误,JavaScript 会自动将其包装在一个失败的 Promise 中
(3)示例代码:
JavaScript
// 这是一个普通的异步函数
async function getGreeting() {
return 'Hello, async!';
}
// 调用它,得到一个 Promise
getGreeting().then(result => {
console.log(result); // 输出: 'Hello, async!'
});
3.await
关键字:等待异步操作完成
(1)作用: await
关键字只能在 async函数内部使用。他会“暂停”async函数的执行,直到紧跟在它后面的 Promise 得到解决(成功或失败)。
(2)核心特性:
- 当
await等待的 Promise 成功时,它会返回 Promise 的结果值。
当await等待的 Promise 失败时,他会抛出一个错误,就像同步代码中
throw
一样。await关键字使得我们可以用顺序的、同步的思维来编写异步代码。
(3)示例代码:
JavaScript
function fetchUserData() {
return new Promise(resolve => {
setTimeout(() => {
resolve({ name: 'Alice', id: 1 });
}, 1000); // 模拟1秒后返回数据
});
}
// 使用 async/await
async function showUserData() {
console.log('开始获取用户数据...');
// await 会暂停函数执行,直到 fetchUserData() 的 Promise 成功
const user = await fetchUserData();
console.log('数据获取成功!');
console.log('用户名:', user.name);
}
showUserData();
4.如何处理错误
(1)async/await
结合了 Promise 的错误处理机制,但使用了更熟悉的 try...catch
语法,这让错误处理变得非常直观。
使用
try...catch
: 你可以用一个try...catch
块将await
调用包裹起来。如果await
等待的 Promise 失败了,它会抛出一个错误,catch
块就能捕获到这个错误。
(2)示例代码:
JavaScript
function fetchFailedData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('数据获取失败:网络超时'); // 模拟失败
}, 1000);
});
}
async function getFailedData() {
try {
console.log('开始尝试获取数据...');
const data = await fetchFailedData();
console.log('数据获取成功:', data); // 这行代码不会执行
} catch (error) {
console.error('出错了:', error); // 捕获到 Promise 失败的错误
}
}
getFailedData();
5.async/await
的优点总结
(1)可读性极高:异步代码看起来像同步代码,逻辑更清晰,更容易理解和维护。
(2)解决了回调地狱:它让复杂的异步流程变得扁平化,避免了层层嵌套。
(3)更简洁的错误处理:使用 try...catch
捕获错误,这是一种被开发者广泛接受的、直观的 错误处理方式。
(4) 更好的调试体验:在 async函数中使用断点,调速器可以像对待同步代码一样,逐行暂停 和执行,这比调试 Promise 链要方便得多。