Appearance
JavaScript 异步编程
什么是异步编程
异步编程是一种编程模式,允许程序在执行一个可能需要长时间运行的操作时,不阻塞其他操作的执行。在 JavaScript 中,异步编程尤为重要,因为 JavaScript 是单线程的,如果所有操作都是同步执行的,那么长时间运行的操作会导致页面卡顿,用户体验不佳。
同步 vs 异步
同步执行
同步执行是指代码按照顺序依次执行,每一行代码必须等待上一行代码执行完成后才能执行。
javascript
// 同步执行示例
console.log("Start");
function synchronousTask() {
// 模拟耗时操作
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
return sum;
}
const result = synchronousTask();
console.log("Result:", result);
console.log("End");
// 输出顺序:
// Start
// Result: 499999999500000000
// End
// 注意:在计算过程中,页面会卡顿异步执行
异步执行是指代码不按照顺序执行,而是在某个操作完成后通过回调、Promise 或 async/await 等方式通知程序继续执行后续操作。
javascript
// 异步执行示例
console.log("Start");
function asynchronousTask(callback) {
// 模拟异步耗时操作
setTimeout(() => {
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
callback(sum);
}, 0);
}
asynchronousTask((result) => {
console.log("Result:", result);
});
console.log("End");
// 输出顺序:
// Start
// End
// Result: 499999999500000000
// 注意:在计算过程中,页面不会卡顿异步编程的实现方式
1. 回调函数
回调函数是最基本的异步编程方式,它是一个函数,作为参数传递给另一个函数,当异步操作完成时被调用。
javascript
// 回调函数示例
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = function () {
if (xhr.status === 200) {
callback(null, JSON.parse(xhr.responseText));
} else {
callback(new Error("请求失败"), null);
}
};
xhr.onerror = function () {
callback(new Error("网络错误"), null);
};
xhr.send();
}
// 使用回调函数
fetchData("https://api.example.com/data", (error, data) => {
if (error) {
console.error("错误:", error);
} else {
console.log("数据:", data);
}
});回调地狱
当多个异步操作需要按顺序执行时,回调函数会嵌套在一起,形成所谓的"回调地狱",代码可读性和可维护性变差。
javascript
// 回调地狱示例
fetchData("https://api.example.com/user/1", (error, user) => {
if (error) {
console.error("错误:", error);
} else {
fetchData(
`https://api.example.com/posts?userId=${user.id}`,
(error, posts) => {
if (error) {
console.error("错误:", error);
} else {
fetchData(
`https://api.example.com/comments?postId=${posts[0].id}`,
(error, comments) => {
if (error) {
console.error("错误:", error);
} else {
console.log("用户:", user);
console.log("帖子:", posts);
console.log("评论:", comments);
}
}
);
}
}
);
}
});2. Promise
Promise 是 ES6 引入的异步编程解决方案,它代表一个异步操作的最终完成(或失败)及其结果值。
Promise 的基本用法
javascript
// Promise 示例
function fetchData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = function () {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error("请求失败"));
}
};
xhr.onerror = function () {
reject(new Error("网络错误"));
};
xhr.send();
});
}
// 使用 Promise
fetchData("https://api.example.com/data")
.then((data) => {
console.log("数据:", data);
return fetchData("https://api.example.com/other");
})
.then((otherData) => {
console.log("其他数据:", otherData);
})
.catch((error) => {
console.error("错误:", error);
})
.finally(() => {
console.log("操作完成");
});Promise 的状态
Promise 有三种状态:
- pending:初始状态,既不是成功,也不是失败
- fulfilled:操作成功完成
- rejected:操作失败
一旦 Promise 的状态从 pending 变为 fulfilled 或 rejected,就不能再改变。
Promise 的方法
Promise.resolve()
创建一个立即解决的 Promise。
javascript
const promise1 = Promise.resolve("成功");
promise1.then((value) => {
console.log(value); // 成功
});
const promise2 = Promise.resolve(42);
promise2.then((value) => {
console.log(value); // 42
});Promise.reject()
创建一个立即拒绝的 Promise。
javascript
const promise = Promise.reject(new Error("失败"));
promise.catch((error) => {
console.error(error); // Error: 失败
});Promise.all()
接收一个 Promise 数组,返回一个新的 Promise,当所有 Promise 都解决时才解决,当任何一个 Promise 拒绝时就拒绝。
javascript
const promise1 = fetchData("https://api.example.com/data1");
const promise2 = fetchData("https://api.example.com/data2");
const promise3 = fetchData("https://api.example.com/data3");
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log("所有数据:", values);
})
.catch((error) => {
console.error("错误:", error);
});Promise.race()
接收一个 Promise 数组,返回一个新的 Promise,当第一个 Promise 解决或拒绝时,就采用该 Promise 的结果。
javascript
const promise1 = new Promise((resolve) => setTimeout(resolve, 100, "第一个"));
const promise2 = new Promise((resolve) => setTimeout(resolve, 50, "第二个"));
Promise.race([promise1, promise2]).then((value) => {
console.log(value); // 第二个
});Promise.allSettled()
接收一个 Promise 数组,返回一个新的 Promise,当所有 Promise 都完成(无论成功或失败)时,才解决并返回所有 Promise 的结果。
javascript
const promise1 = fetchData("https://api.example.com/data1");
const promise2 = fetchData("https://api.example.com/data2"); // 假设这个会失败
const promise3 = fetchData("https://api.example.com/data3");
Promise.allSettled([promise1, promise2, promise3]).then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("成功:", result.value);
} else {
console.error("失败:", result.reason);
}
});
});Promise.any()
接收一个 Promise 数组,返回一个新的 Promise,当第一个 Promise 解决时,就采用该 Promise 的结果;当所有 Promise 都拒绝时,才拒绝。
javascript
const promise1 = Promise.reject(new Error("第一个失败"));
const promise2 = Promise.resolve("第二个成功");
const promise3 = Promise.reject(new Error("第三个失败"));
Promise.any([promise1, promise2, promise3])
.then((value) => {
console.log(value); // 第二个成功
})
.catch((error) => {
console.error("所有都失败:", error);
});3. async/await
async/await 是 ES2017 引入的异步编程语法糖,它基于 Promise,使异步代码看起来更像同步代码,提高了代码的可读性。
基本用法
javascript
// async/await 示例
async function fetchData(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error("请求失败");
}
const data = await response.json();
return data;
}
// 使用 async/await
async function getData() {
try {
const data = await fetchData("https://api.example.com/data");
console.log("数据:", data);
const otherData = await fetchData("https://api.example.com/other");
console.log("其他数据:", otherData);
} catch (error) {
console.error("错误:", error);
} finally {
console.log("操作完成");
}
}
getData();注意事项
async函数返回一个 Promiseawait只能在async函数内部使用await后面跟着一个 Promise,等待该 Promise 解决- 可以使用
try/catch捕获异步操作的错误
并行执行
使用 Promise.all() 可以在 async/await 中实现并行执行多个异步操作。
javascript
async function getMultipleData() {
try {
const [data1, data2, data3] = await Promise.all([
fetchData("https://api.example.com/data1"),
fetchData("https://api.example.com/data2"),
fetchData("https://api.example.com/data3"),
]);
console.log("数据1:", data1);
console.log("数据2:", data2);
console.log("数据3:", data3);
} catch (error) {
console.error("错误:", error);
}
}
getMultipleData();4. 事件监听
事件监听是另一种异步编程方式,通过监听事件的触发来执行相应的回调函数。
javascript
// 事件监听示例
const button = document.getElementById("myButton");
button.addEventListener("click", function (event) {
console.log("按钮被点击了");
// 执行异步操作
fetchData("https://api.example.com/data")
.then((data) => {
console.log("数据:", data);
})
.catch((error) => {
console.error("错误:", error);
});
});5. Generator 函数
Generator 函数是 ES6 引入的一种特殊函数,可以暂停执行和恢复执行,也可以用于异步编程。
javascript
// Generator 函数示例
function* generator() {
console.log("开始");
const value1 = yield "第一个值";
console.log("接收到:", value1);
const value2 = yield "第二个值";
console.log("接收到:", value2);
return "结束";
}
const gen = generator();
console.log(gen.next()); // { value: '第一个值', done: false }
console.log(gen.next("传入第一个值")); // { value: '第二个值', done: false }
console.log(gen.next("传入第二个值")); // { value: '结束', done: true }使用 Generator 函数处理异步操作
javascript
// 使用 Generator 函数处理异步操作
function* fetchDataGenerator() {
try {
const user = yield fetchData("https://api.example.com/user/1");
console.log("用户:", user);
const posts = yield fetchData(
`https://api.example.com/posts?userId=${user.id}`
);
console.log("帖子:", posts);
const comments = yield fetchData(
`https://api.example.com/comments?postId=${posts[0].id}`
);
console.log("评论:", comments);
} catch (error) {
console.error("错误:", error);
}
}
// 手动执行 Generator 函数
function runGenerator(generator) {
const gen = generator();
function iterate(iteration) {
if (iteration.done) return iteration.value;
const promise = iteration.value;
return promise
.then((value) => iterate(gen.next(value)))
.catch((error) => iterate(gen.throw(error)));
}
return iterate(gen.next());
}
// 运行 Generator 函数
runGenerator(fetchDataGenerator);异步编程的应用场景
1. 网络请求
javascript
// 使用 fetch API 进行网络请求
async function fetchUsers() {
try {
const response = await fetch("https://api.example.com/users");
const users = await response.json();
console.log("用户列表:", users);
return users;
} catch (error) {
console.error("获取用户失败:", error);
throw error;
}
}
fetchUsers();2. 文件操作
javascript
// 使用 FileReader 读取文件
function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function () {
resolve(reader.result);
};
reader.onerror = function () {
reject(new Error("文件读取失败"));
};
reader.readAsText(file);
});
}
// 使用示例
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", async function (e) {
const file = e.target.files[0];
if (file) {
try {
const content = await readFile(file);
console.log("文件内容:", content);
} catch (error) {
console.error("错误:", error);
}
}
});3. 定时器
javascript
// 封装定时器为 Promise
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// 使用示例
async function countdown() {
for (let i = 5; i > 0; i--) {
console.log(i);
await delay(1000);
}
console.log("开始!");
}
countdown();4. 数据库操作
javascript
// 模拟数据库操作
function queryDatabase(sql) {
return new Promise((resolve, reject) => {
// 模拟异步查询
setTimeout(() => {
if (sql.includes("SELECT")) {
resolve([
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
]);
} else {
reject(new Error("查询失败"));
}
}, 500);
});
}
// 使用示例
async function getUsers() {
try {
const users = await queryDatabase("SELECT * FROM users");
console.log("用户:", users);
} catch (error) {
console.error("错误:", error);
}
}
getUsers();5. 动画效果
javascript
// 封装动画为 Promise
function animate(element, properties, duration) {
return new Promise((resolve) => {
const start = performance.now();
const originalProperties = {};
// 保存原始属性
for (const prop in properties) {
originalProperties[prop] = parseFloat(getComputedStyle(element)[prop]);
}
function updateAnimation(currentTime) {
const elapsed = currentTime - start;
const progress = Math.min(elapsed / duration, 1);
// 应用动画
for (const prop in properties) {
const startValue = originalProperties[prop];
const endValue = properties[prop];
const currentValue = startValue + (endValue - startValue) * progress;
element.style[prop] = currentValue + (prop === "opacity" ? "" : "px");
}
if (progress < 1) {
requestAnimationFrame(updateAnimation);
} else {
resolve();
}
}
requestAnimationFrame(updateAnimation);
});
}
// 使用示例
async function runAnimation() {
const box = document.getElementById("box");
await animate(box, { left: 200, opacity: 0.5 }, 1000);
console.log("第一个动画完成");
await animate(box, { top: 100, opacity: 1 }, 1000);
console.log("第二个动画完成");
await animate(box, { left: 0, top: 0 }, 1000);
console.log("所有动画完成");
}
runAnimation();异步编程的最佳实践
1. 错误处理
- 回调函数:使用错误优先回调模式
- Promise:使用
.catch()捕获错误 - async/await:使用
try/catch捕获错误
javascript
// 错误处理示例
// 回调函数
function fetchDataCallback(url, callback) {
setTimeout(() => {
if (url.includes("error")) {
callback(new Error("请求失败"));
} else {
callback(null, { data: "成功" });
}
}, 1000);
}
fetchDataCallback("https://api.example.com/data", (error, data) => {
if (error) {
console.error("错误:", error);
} else {
console.log("数据:", data);
}
});
// Promise
function fetchDataPromise(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.includes("error")) {
reject(new Error("请求失败"));
} else {
resolve({ data: "成功" });
}
}, 1000);
});
}
fetchDataPromise("https://api.example.com/data")
.then((data) => {
console.log("数据:", data);
})
.catch((error) => {
console.error("错误:", error);
});
// async/await
async function fetchDataAsync(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.includes("error")) {
reject(new Error("请求失败"));
} else {
resolve({ data: "成功" });
}
}, 1000);
});
}
async function getData() {
try {
const data = await fetchDataAsync("https://api.example.com/data");
console.log("数据:", data);
} catch (error) {
console.error("错误:", error);
}
}
getData();2. 避免阻塞
- 避免在主线程上执行长时间运行的同步操作
- 使用 Web Workers 处理密集计算
- 使用异步 API 进行 I/O 操作
3. 并行执行
- 使用
Promise.all()并行执行多个独立的异步操作 - 避免不必要的串行执行,提高性能
javascript
// 并行执行示例
async function getMultipleData() {
// 串行执行(较慢)
const start1 = performance.now();
const data1 = await fetchData("https://api.example.com/data1");
const data2 = await fetchData("https://api.example.com/data2");
const data3 = await fetchData("https://api.example.com/data3");
const end1 = performance.now();
console.log("串行执行时间:", end1 - start1);
// 并行执行(较快)
const start2 = performance.now();
const [data4, data5, data6] = await Promise.all([
fetchData("https://api.example.com/data1"),
fetchData("https://api.example.com/data2"),
fetchData("https://api.example.com/data3"),
]);
const end2 = performance.now();
console.log("并行执行时间:", end2 - start2);
}
getMultipleData();4. 取消异步操作
- 使用 AbortController 取消 fetch 请求
- 使用标志变量取消长时间运行的操作
javascript
// 取消 fetch 请求
const controller = new AbortController();
const signal = controller.signal;
fetch("https://api.example.com/data", { signal })
.then((response) => response.json())
.then((data) => console.log("数据:", data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("请求被取消");
} else {
console.error("错误:", error);
}
});
// 取消请求
setTimeout(() => {
controller.abort();
}, 1000);
// 使用标志变量取消操作
let isCancelled = false;
async function longRunningTask() {
for (let i = 0; i < 1000000000; i++) {
if (isCancelled) {
console.log("操作被取消");
return;
}
// 执行计算
}
console.log("操作完成");
}
longRunningTask();
// 取消操作
setTimeout(() => {
isCancelled = true;
}, 1000);5. 内存管理
- 避免创建不必要的闭包
- 及时清理定时器和事件监听器
- 避免循环引用
6. 代码组织
- 将异步操作封装为独立的函数
- 使用模块化组织代码
- 保持函数的单一职责
异步编程的常见问题
1. 回调地狱
问题:多个嵌套的回调函数导致代码可读性差。
解决方案:使用 Promise 或 async/await 替代嵌套回调。
2. 未处理的 Promise 拒绝
问题:Promise 被拒绝但没有被捕获,导致控制台错误。
解决方案:总是使用 .catch() 捕获 Promise 错误,或在 async 函数中使用 try/catch。
3. 竞态条件
问题:多个异步操作的执行顺序不确定,导致结果不一致。
解决方案:使用适当的同步机制,如 Promise.all() 或 async/await 的顺序执行。
4. 内存泄漏
问题:异步操作导致的内存泄漏,如未清理的定时器或事件监听器。
解决方案:及时清理定时器、事件监听器和其他资源。
5. 超时处理
问题:异步操作没有设置超时,可能导致无限等待。
解决方案:为异步操作设置超时,使用 Promise.race() 实现。
javascript
// 设置超时
function withTimeout(promise, ms) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("操作超时")), ms);
});
return Promise.race([promise, timeoutPromise]);
}
// 使用示例
async function fetchWithTimeout(url, timeout = 5000) {
try {
const response = await withTimeout(fetch(url), timeout);
const data = await response.json();
return data;
} catch (error) {
console.error("错误:", error);
throw error;
}
}
fetchWithTimeout("https://api.example.com/data", 3000);6. 错误传播
问题:异步操作的错误没有正确传播,导致难以调试。
解决方案:确保错误在 Promise 链中正确传播,或在 async 函数中使用 try/catch。
总结
异步编程是 JavaScript 中非常重要的一部分,它允许我们处理耗时操作而不阻塞主线程。JavaScript 提供了多种异步编程方式:
- 回调函数:最基本的异步编程方式,但容易导致回调地狱
- Promise:ES6 引入的异步编程解决方案,解决了回调地狱问题
- async/await:ES2017 引入的语法糖,基于 Promise,使异步代码更像同步代码
- 事件监听:通过监听事件触发来执行回调
- Generator 函数:ES6 引入的特殊函数,可以暂停和恢复执行
在实际开发中,我们应该根据具体场景选择合适的异步编程方式:
- 简单的异步操作可以使用回调函数
- 复杂的异步操作链应该使用 Promise 或 async/await
- 用户交互相关的异步操作可以使用事件监听
同时,我们应该遵循异步编程的最佳实践:
- 正确处理错误
- 避免阻塞主线程
- 合理使用并行执行
- 注意内存管理
- 保持代码组织清晰
通过掌握异步编程,我们可以创建更响应、更高效的 JavaScript 应用。