0基础进大厂,第13天:Promise:你先等等我

深入理解 ES6 中的 Promise:异步编程的利器

在 JavaScript 的世界里,异步编程一直是一个重要且具有挑战性的领域。随着前端应用的日益复杂,处理异步操作变得愈发关键。ES6 引入的 Promise,为我们提供了一种更为优雅、强大的方式来管理异步操作,极大地改善了异步代码的可读性和可维护性。好的,现在就来聊聊 Promise

从回调地狱说起

在 Promise 出现之前,JavaScript 主要通过回调函数来处理异步操作。例如,使用setTimeout模拟异步任务:

javascript 复制代码
setTimeout(() => {
    console.log('第一个异步操作完成');
    setTimeout(() => {
        console.log('第二个异步操作完成');
        setTimeout(() => {
            console.log('第三个异步操作完成');
        }, 1000);
    }, 1000);
}, 1000);

这种层层嵌套的回调函数,在处理复杂异步逻辑时,会导致代码可读性极差,维护和调试都变得异常困难,这就是著名的 "回调地狱"。

再比如,在进行多个 AJAX 请求,且后一个请求依赖前一个请求的结果时,回调地狱的问题会更加突出:

javascript 复制代码
$.get('url1', function(data1) {
    $.get(data1.nextUrl, function(data2) {
        $.get(data2.nextUrl, function(data3) {
            console.log(data3);
        });
    });
});

随着异步操作的增多,代码会像金字塔一样层层嵌套,变得难以理解和维护。

Promise 的诞生

为了解决回调地狱的问题,Promise 应运而生。Promise 是 ES6 引入的一个对象,它代表了一个异步操作的最终完成(或失败)及其结果值。简单来说,Promise 就像是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上讲,Promise 是一个对象,通过它可以获取异步操作的消息,并且提供了统一的 API,使得各种异步操作都能用同样的方式进行处理。

Promise 有三种状态:

  • Pending(等待态) :这是 Promise 的初始状态,此时异步操作尚未完成,既没有被兑现(成功)也没有被拒绝(失败)。
  • Fulfilled(成功态) :当异步操作成功完成时,Promise 的状态会变为 Fulfilled,意味着操作已经成功,我们可以从 Promise 中获取到预期的结果。此时,Promise 会携带一个值,这个值就是异步操作成功的结果。
  • Rejected(失败态) :当异步操作失败时,Promise 的状态会变为 Rejected,此时,Promise 会携带一个原因,表示失败的原因。

一个 Promise 一旦从 Pending 状态变为 Fulfilled 或 Rejected,就不可以再进行其他状态转变,即 Promise 的状态只能改变一次。

Promise 的基本使用

创建 Promise 对象

使用new Promise()构造函数来创建一个 Promise 对象。构造函数接受一个带有两个参数的函数,分别是resolve和reject。resolve用于表示操作成功,reject用于表示操作失败。

javascript 复制代码
const myPromise = new Promise((resolve, reject) => {
    // 模拟异步操作,这里使用setTimeout
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('操作成功'); // 将Promise状态置为Fulfilled,并传递成功的值
        } else {
            reject('操作失败'); // 将Promise状态置为Rejected,并传递失败的原因
        }
    }, 1000);
});

在上述代码中,我们创建了一个 Promise 对象myPromise。在Promise的执行器函数中,通过setTimeout模拟了一个异步操作。1 秒后,根据success的值,调用resolve或reject方法来改变 Promise 的状态。

处理 Promise 的结果

通过then方法来处理 Promise 成功或失败的情况。then方法接收两个回调函数作为参数,第一个回调函数在 Promise 状态变为fulfilled时调用,第二个回调函数(可选)在 Promise 状态变为rejected时调用。

javascript 复制代码
myPromise.then((result) => {
    console.log(result); // 输出:操作成功
}, (error) => {
    console.error(error); // 若操作失败,输出错误信息
});

我们也可以使用catch方法专门处理 Promise 失败的情况,它实际上是then(null, onRejected)的语法糖。

javascript 复制代码
myPromise.then((result) => {
    console.log(result); // 输出:操作成功
}).catch((error) => {
    console.error(error); // 若操作失败,输出错误信息
});

Promise 的链式调用

then方法的一个重要特性是它返回一个新的 Promise,这使得我们可以进行链式调用。通过链式调用,我们可以将多个异步操作串联起来,每个then方法处理前一个 Promise 的结果,并返回一个新的 Promise 供下一个then方法处理。

假设我们有一个需求,需要先获取用户信息,然后根据用户信息获取用户的订单列表,最后统计订单的总金额。使用then方法的链式调用可以很方便地实现这个需求:

javascript 复制代码
// 模拟获取用户信息的异步操作
function getUserInfo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const user = { id: 1, name: '张三' };
            resolve(user);
        }, 1000);
    });
}
// 模拟根据用户信息获取订单列表的异步操作
function getOrderList(user) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const orders = [
                { id: 1, amount: 100 },
                { id: 2, amount: 200 }
            ];
            resolve(orders);
        }, 1000);
    });
}
// 模拟统计订单总金额的异步操作
function calculateTotalAmount(orders) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const total = orders.reduce((sum, order) => sum + order.amount, 0);
            resolve(total);
        }, 1000);
    });
}
getUserInfo()
   .then(getOrderList)
   .then(calculateTotalAmount)
   .then((total) => {
        console.log('订单总金额为:', total); // 输出:订单总金额为: 300
    })
   .catch((error) => {
        console.error('发生错误:', error);
    });

在这个示例中,getUserInfo返回一个 Promise,当这个 Promise 成功时,then方法会调用getOrderList并传入用户信息,getOrderList又返回一个 Promise,当这个 Promise 成功时,下一个then方法会调用calculateTotalAmount并传入订单列表,最后计算出订单总金额并打印。如果在任何一个步骤中出现错误,catch方法会捕获并处理错误。

Promise 的静态方法

Promise.all

Promise.all方法用于将多个 Promise 对象包装成一个新的 Promise 对象。只有当所有传入的 Promise 对象都成功时,新的 Promise 对象才会成功,其结果是一个包含所有成功结果的数组;只要有一个 Promise 对象失败,新的 Promise 对象就会失败,其结果是第一个失败的 Promise 对象的错误信息。

假设我们需要同时发起多个 AJAX 请求,获取多个数据,然后在所有请求都完成后进行统一处理,就可以使用Promise.all:

javascript 复制代码
const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('结果1');
    }, 1000);
});
const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('结果2');
    }, 1500);
});
const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('操作失败');
    }, 2000);
});
Promise.all([promise1, promise2, promise3])
   .then((results) => {
        console.log(results); // 不会执行,因为promise3失败
    })
   .catch((error) => {
        console.error(error); // 输出:操作失败
    });

在这个例子中,Promise.all接收一个包含三个 Promise 对象的数组。由于promise3失败,所以新的 Promise 对象立即失败,并捕获到promise3的错误信息。

Promise.race

Promise.race方法同样用于将多个 Promise 对象包装成一个新的 Promise 对象。与Promise.all不同的是,只要传入的 Promise 对象中有一个率先改变状态(成功或失败),新的 Promise 对象就会立即改变状态,其结果就是第一个改变状态的 Promise 对象的结果。

javascript 复制代码
const promise4 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('结果4');
    }, 2000);
});
const promise5 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('操作失败');
    }, 1000);
});
Promise.race([promise4, promise5])
   .then((result) => {
        console.log(result); // 不会执行,因为promise5率先失败
    })
   .catch((error) => {
        console.error(error); // 输出:操作失败
    });

在这个例子中,promise5在 1 秒后率先失败,所以Promise.race返回的新 Promise 对象立即失败,并捕获到promise5的错误信息。

实际案例应用

图片加载

在网页开发中,经常需要加载多张图片。我们可以使用 Promise 来管理图片的加载过程,确保所有图片都加载完成后再进行下一步操作。

javascript 复制代码
function loadImage(url) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.src = url;
        img.onload = () => {
            resolve(img);
        };
        img.onerror = () => {
            reject(new Error('图片加载失败'));
        };
    });
}
const urls = [
    'image1.jpg',
    'image2.jpg',
    'image3.jpg'
];
Promise.all(urls.map(loadImage))
   .then((images) => {
        // 所有图片都加载完成,进行下一步操作,比如将图片添加到页面
        const container = document.createElement('div');
        images.forEach((image) => {
            container.appendChild(image);
        });
        document.body.appendChild(container);
    })
   .catch((error) => {
        console.error(error);
    });

在这个案例中,我们定义了一个loadImage函数,它返回一个 Promise,用于加载一张图片。通过Promise.all和map方法,我们同时加载多张图片,并在所有图片都加载完成后,将它们添加到页面中。

AJAX 请求

在实际项目中,AJAX 请求是非常常见的异步操作。使用 Promise 可以更好地管理 AJAX 请求的流程和错误处理。

javascript 复制代码
function ajax(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject(new Error('请求失败'));
                }
            }
        };
        xhr.send();
    });
}
ajax('data.json')
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,ajax函数返回一个 Promise,用于发送 AJAX 请求。通过then和catch方法,我们可以方便地处理请求成功和失败的情况。

总结

Promise 为 JavaScript 的异步编程带来了极大的便利,它通过链式调用和统一的错误处理机制,有效地解决了回调地狱的问题,使异步代码更加清晰、易读、易维护。掌握 Promise 的使用,不仅可以提升我们编写异步代码的能力,还能更好地理解和运用其他与异步相关的技术,如async/await等。在现代前端开发中,Promise 已经成为了处理异步操作的核心机制之一,无论是使用原生的 JavaScript 进行开发,还是使用各种前端框架(如 Vue、React 等),都离不开 Promise。

相关推荐
RaidenLiu4 分钟前
告别陷阱:精通Flutter Signals的生命周期、高级API与调试之道
前端·flutter·前端框架
非凡ghost4 分钟前
HWiNFO(专业系统信息检测工具)
前端·javascript·后端
非凡ghost6 分钟前
FireAlpaca(免费数字绘图软件)
前端·javascript·后端
非凡ghost13 分钟前
Sucrose Wallpaper Engine(动态壁纸管理工具)
前端·javascript·后端
拉不动的猪14 分钟前
为什么不建议项目里用延时器作为规定时间内的业务操作
前端·javascript·vue.js
该用户已不存在21 分钟前
Gemini CLI 扩展,把Nano Banana 搬到终端
前端·后端·ai编程
地方地方23 分钟前
前端踩坑记:解决图片与 Div 换行间隙的隐藏元凶
前端·javascript
炒米233325 分钟前
【Array】数组的方法
javascript
小猫由里香28 分钟前
小程序打开文件(文件流、地址链接)封装
前端
Tzarevich31 分钟前
使用n8n工作流自动化生成每日科技新闻速览:告别信息过载,拥抱智能阅读
前端