用户工具

站点工具


javascript:第七章异步基础

第七章 异步编程基础

本章目标

  • 理解同步与异步编程的区别
  • 掌握回调函数的使用方法
  • 深入理解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链,依次完成以下操作:

  1. 获取用户ID
  2. 根据ID获取用户详情
  3. 根据用户详情获取订单列表

练习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的事件循环机制,理解异步代码的执行原理。

javascript/第七章异步基础.txt · 最后更改: 127.0.0.1