目录
第七章 异步编程基础
本章目标
- 理解同步与异步编程的区别
- 掌握回调函数的使用方法
- 深入理解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的事件循环机制,理解异步代码的执行原理。
