====== 第七章 异步编程基础 ======
===== 本章目标 =====
* 理解同步与异步编程的区别
* 掌握回调函数的使用方法
* 深入理解Promise的概念与用法
* 学会处理异步操作中的错误
===== 7.1 同步与异步 =====
==== 7.1.1 同步编程 ====
在JavaScript中,默认情况下代码是同步执行的,即按照代码的书写顺序一行一行地执行。只有当上一行代码执行完毕后,才会执行下一行代码。
**同步执行的特点:**
* 代码按照顺序执行,易于理解和调试
* 如果某行代码执行耗时较长,会阻塞后续代码的执行
* 整个程序的执行流程是确定且可预测的
console.log('第一步');
console.log('第二步');
console.log('第三步');
// 输出顺序:第一步 → 第二步 → 第三步
==== 7.1.2 异步编程 ====
异步编程允许程序在等待某些操作完成时继续执行其他代码,而不会阻塞主线程。这对于处理I/O操作(如网络请求、文件读写)尤为重要。
**异步执行的特点:**
* 不会阻塞主线程,提高程序的响应性
* 代码执行顺序不总是按照书写顺序
* 需要特殊的机制来处理异步操作的结果
console.log('第一步');
setTimeout(function() {
console.log('第二步(异步)');
}, 1000);
console.log('第三步');
// 输出顺序:第一步 → 第三步 → 第二步(异步)
===== 7.2 回调函数 =====
==== 7.2.1 什么是回调函数 ====
回调函数(Callback)是作为参数传递给另一个函数的函数,它将在某个特定事件发生时被调用。这是JavaScript中最基本的异步处理方式。
function fetchData(callback) {
setTimeout(function() {
const data = { id: 1, name: '张三' };
callback(data);
}, 1000);
}
fetchData(function(data) {
console.log('获取到数据:', data);
});
==== 7.2.2 回调地狱 ====
当需要依次执行多个异步操作时,回调函数会层层嵌套,形成所谓的"回调地狱"(Callback Hell),使代码难以阅读和维护。
// 回调地狱示例
getUserData(function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
getProductInfo(details.productId, function(product) {
console.log('产品信息:', product);
});
});
});
});
==== 7.2.3 解决回调地狱的方法 ====
**1. 函数命名拆分**
function handleProductInfo(product) {
console.log('产品信息:', product);
}
function handleOrderDetails(details) {
getProductInfo(details.productId, handleProductInfo);
}
function handleOrders(orders) {
getOrderDetails(orders[0].id, handleOrderDetails);
}
function handleUser(user) {
getOrders(user.id, handleOrders);
}
getUserData(handleUser);
**2. 使用Promise(推荐)**
将在7.3节详细介绍Promise的使用。
===== 7.3 Promise对象 =====
==== 7.3.1 Promise简介 ====
Promise是ES6引入的异步编程解决方案,它表示一个异步操作的最终完成(或失败)及其结果值。Promise有三种状态:
* **Pending(等待中)**:初始状态,既未完成也未拒绝
* **Fulfilled(已完成)**:操作成功完成
* **Rejected(已拒绝)**:操作失败
const promise = new Promise(function(resolve, reject) {
// 异步操作
setTimeout(function() {
const success = true;
if (success) {
resolve('操作成功!');
} else {
reject('操作失败!');
}
}, 1000);
});
==== 7.3.2 Promise的基本用法 ====
**then()方法**:用于处理Promise成功的情况
**catch()方法**:用于处理Promise失败的情况
function fetchUserData(userId) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (userId > 0) {
resolve({ id: userId, name: '用户' + userId });
} else {
reject('无效的用户ID');
}
}, 1000);
});
}
// 使用Promise
fetchUserData(1)
.then(function(user) {
console.log('用户信息:', user);
return user.id;
})
.then(function(userId) {
console.log('用户ID:', userId);
})
.catch(function(error) {
console.error('错误:', error);
});
==== 7.3.3 Promise链式调用 ====
Promise支持链式调用,每个then()方法都会返回一个新的Promise,这样可以避免回调地狱。
function step1() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('步骤1完成');
resolve(10);
}, 1000);
});
}
function step2(value) {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('步骤2完成,接收值:', value);
resolve(value * 2);
}, 1000);
});
}
function step3(value) {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('步骤3完成,接收值:', value);
resolve(value + 5);
}, 1000);
});
}
// 链式调用
step1()
.then(step2)
.then(step3)
.then(function(finalResult) {
console.log('最终结果:', finalResult); // 25
});
==== 7.3.4 Promise.all() ====
Promise.all()方法接收一个Promise数组,当所有Promise都成功完成时,返回一个包含所有结果的数组;如果任一Promise失败,则立即返回失败原因。
const promise1 = fetch('/api/users');
const promise2 = fetch('/api/products');
const promise3 = fetch('/api/orders');
Promise.all([promise1, promise2, promise3])
.then(function(responses) {
console.log('所有请求完成:', responses);
return Promise.all(responses.map(function(r) { return r.json(); }));
})
.then(function(data) {
console.log('解析后的数据:', data);
})
.catch(function(error) {
console.error('某个请求失败:', error);
});
==== 7.3.5 Promise.race() ====
Promise.race()方法接收一个Promise数组,返回最先完成(无论成功或失败)的那个Promise的结果。
const fastRequest = new Promise(function(resolve) {
setTimeout(function() { resolve('快速请求'); }, 100);
});
const slowRequest = new Promise(function(resolve) {
setTimeout(function() { resolve('慢速请求'); }, 1000);
});
Promise.race([fastRequest, slowRequest])
.then(function(winner) {
console.log('获胜者:', winner); // "快速请求"
});
// 实际应用:设置超时
function fetchWithTimeout(url, timeout) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise(function(_, reject) {
setTimeout(function() {
reject(new Error('请求超时'));
}, timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
==== 7.3.6 Promise.allSettled() ====
Promise.allSettled()方法等待所有Promise完成(无论成功或失败),返回每个Promise的状态和结果。
const promises = [
Promise.resolve('成功1'),
Promise.reject('失败1'),
Promise.resolve('成功2'),
Promise.reject('失败2')
];
Promise.allSettled(promises)
.then(function(results) {
results.forEach(function(result) {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失败:', result.reason);
}
});
});
===== 7.4 异步错误处理 =====
==== 7.4.1 Promise中的错误处理 ====
在Promise链中,任何一个环节发生的错误都会被catch()捕获。
function riskyOperation() {
return new Promise(function(resolve, reject) {
// 模拟随机失败
if (Math.random() > 0.5) {
resolve('操作成功');
} else {
reject(new Error('操作失败'));
}
});
}
riskyOperation()
.then(function(result) {
console.log(result);
// 这里也可能抛出错误
throw new Error('处理结果时出错');
})
.catch(function(error) {
console.error('捕获到错误:', error.message);
})
.finally(function() {
console.log('无论成功失败都会执行');
});
==== 7.4.2 全局错误处理 ====
对于未处理的Promise拒绝,可以使用unhandledrejection事件进行全局捕获。
window.addEventListener('unhandledrejection', function(event) {
console.error('未处理的Promise拒绝:', event.reason);
event.preventDefault(); // 防止控制台输出默认错误信息
});
// 创建一个没有catch的Promise
Promise.reject('未处理的错误');
===== 7.5 实战示例 =====
==== 7.5.1 封装AJAX请求 ====
function request(url, method, data) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open(method || 'GET', url);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error('HTTP ' + xhr.status + ': ' + xhr.statusText));
}
};
xhr.onerror = function() {
reject(new Error('网络请求失败'));
};
xhr.send(data ? JSON.stringify(data) : null);
});
}
// 使用封装的请求
request('/api/user/1')
.then(function(data) { return JSON.parse(data); })
.then(function(user) { console.log(user); })
.catch(function(error) { console.error(error); });
==== 7.5.2 图片预加载 ====
function preloadImage(src) {
return new Promise(function(resolve, reject) {
const img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = function() {
reject(new Error('图片加载失败:' + src));
};
img.src = src;
});
}
// 预加载多张图片
const imageUrls = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
'https://example.com/image3.jpg'
];
Promise.all(imageUrls.map(preloadImage))
.then(function(images) {
console.log('所有图片加载完成');
images.forEach(function(img) {
document.body.appendChild(img);
});
})
.catch(function(error) {
console.error('图片加载失败:', error);
});
===== 本章习题 =====
==== 基础练习 ====
**练习1:回调函数转换**
将以下使用回调函数的代码转换为使用Promise:
function getData(callback) {
setTimeout(function() {
callback(null, { data: 'some data' });
}, 1000);
}
**练习2:Promise链**
编写一个Promise链,依次完成以下操作:
- 获取用户ID
- 根据ID获取用户详情
- 根据用户详情获取订单列表
**练习3:并行请求**
使用Promise.all()同时请求多个API端点,并处理其中可能失败的情况。
==== 进阶练习 ====
**练习4:重试机制**
实现一个带重试机制的异步请求函数,当请求失败时自动重试指定次数。
function retryRequest(fn, maxRetries) {
// 实现重试逻辑
}
**练习5:队列执行**
实现一个异步任务队列,限制同时执行的异步任务数量(例如最多3个并发)。
==== 思考题 ====
1. 为什么JavaScript需要异步编程?哪些场景适合使用异步?
2. Promise相比回调函数有哪些优势?它解决了什么问题?
3. Promise.all()和Promise.race()分别在什么场景下使用?
4. 如何处理Promise链中某个步骤的错误,同时让后续步骤继续执行?
===== 参考答案 =====
**练习1答案:**
function getData() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ data: 'some data' });
}, 1000);
});
}
getData().then(function(data) {
console.log(data);
});
**练习4答案:**
function retryRequest(fn, maxRetries) {
return new Promise(function(resolve, reject) {
let attempts = 0;
function attempt() {
fn().then(resolve).catch(function(error) {
attempts++;
if (attempts < maxRetries) {
console.log('重试第' + attempts + '次...');
attempt();
} else {
reject(error);
}
});
}
attempt();
});
}
// 使用示例
retryRequest(function() {
return fetch('/api/data');
}, 3);
===== 小结 =====
本章我们学习了JavaScript异步编程的基础知识:
* **同步与异步**:理解了两者的区别,异步编程可以提高程序响应性
* **回调函数**:掌握了基本的异步处理方式,但也了解了回调地狱的问题
* **Promise**:学习了Promise的创建、链式调用、以及Promise.all()和Promise.race()的用法
* **错误处理**:掌握了Promise中的错误捕获和全局错误处理
下一章我们将深入探讨JavaScript的事件循环机制,理解异步代码的执行原理。