MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript Promise.all与Promise.race的应用

2021-09-146.8k 阅读

Promise.all 的应用

Promise.all 基础概念

Promise.all 是 JavaScript 中用于处理多个 Promise 对象的静态方法。它接受一个 Promise 对象的可迭代对象(如数组)作为参数,并返回一个新的 Promise 对象。当传入的所有 Promise 对象都成功解决(resolved)时,返回的 Promise 对象才会成功解决,并且其解决的值是一个包含所有传入 Promise 对象解决值的数组,顺序与传入的 Promise 对象顺序一致。如果其中任何一个 Promise 对象被拒绝(rejected),Promise.all 返回的 Promise 对象就会立即被拒绝,并且其拒绝原因就是第一个被拒绝的 Promise 对象的拒绝原因。

并发请求场景

在实际开发中,经常会遇到需要同时发起多个异步请求,并且要在所有请求都完成后执行一些操作的场景。例如,一个电商应用可能需要同时获取商品信息、用户信息以及购物车信息,在获取到所有这些信息后再进行页面渲染。

以下是一个简单的代码示例,模拟三个异步请求:

function fetchData1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data from request 1');
        }, 1000);
    });
}

function fetchData2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data from request 2');
        }, 1500);
    });
}

function fetchData3() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data from request 3');
        }, 2000);
    });
}

Promise.all([fetchData1(), fetchData2(), fetchData3()])
   .then((results) => {
        console.log(results); // 输出: ['Data from request 1', 'Data from request 2', 'Data from request 3']
    })
   .catch((error) => {
        console.error('An error occurred:', error);
    });

在这个示例中,Promise.all 接受三个异步函数返回的 Promise 对象组成的数组。即使 fetchData3 花费的时间最长,但 Promise.all 会等待所有的 Promise 都解决后才执行 then 回调,并且 results 数组中元素的顺序与传入 Promise.all 的 Promise 对象顺序一致。

处理文件读取

假设我们需要读取多个文件的内容,并在所有文件都读取完成后进行一些处理。在 Node.js 环境中,可以使用 fs.promises.readFile 方法,该方法返回一个 Promise 对象。

const fs = require('fs');
const path = require('path');
const { promisify } = require('util');

const readFile = promisify(fs.readFile);

const filePaths = [
    path.join(__dirname, 'file1.txt'),
    path.join(__dirname, 'file2.txt'),
    path.join(__dirname, 'file3.txt')
];

Promise.all(filePaths.map((filePath) => readFile(filePath, 'utf8')))
   .then((contents) => {
        contents.forEach((content, index) => {
            console.log(`Content of file ${index + 1}:`, content);
        });
    })
   .catch((error) => {
        console.error('Error reading files:', error);
    });

这里通过 map 方法将每个文件路径转换为对应的 readFile Promise 对象,然后使用 Promise.all 等待所有文件读取完成。contents 数组包含了所有文件的内容,顺序与 filePaths 数组中的文件路径顺序一致。

数据验证与批量处理

在数据处理过程中,可能需要对一组数据进行验证,并且只有当所有数据都通过验证后才能进行下一步处理。可以将每个数据的验证逻辑封装成一个 Promise 对象,然后使用 Promise.all 来确保所有验证都通过。

function validateData(data) {
    return new Promise((resolve, reject) => {
        // 简单的验证逻辑,这里假设数据大于 10 为有效
        if (data > 10) {
            resolve(data);
        } else {
            reject(new Error('Data is invalid'));
        }
    });
}

const dataArray = [15, 20, 25];

Promise.all(dataArray.map(validateData))
   .then((validatedData) => {
        console.log('All data is valid:', validatedData);
        // 进行下一步处理,例如保存到数据库等
    })
   .catch((error) => {
        console.error('Validation failed:', error);
    });

在这个示例中,map 方法将 dataArray 中的每个数据都转换为一个验证 Promise 对象。如果所有数据都通过验证,Promise.all 返回的 Promise 对象会成功解决,并且 validatedData 数组包含了所有通过验证的数据。如果有任何一个数据验证失败,Promise.all 就会立即被拒绝。

错误处理与中断

Promise.all 的错误处理机制非常重要。一旦其中一个 Promise 对象被拒绝,Promise.all 返回的 Promise 对象就会被拒绝,并且后续的 Promise 对象不会被取消(在 JavaScript 中没有内置的机制自动取消 Promise,除非使用第三方库,如 abortcontroller - polyfill 等)。

function successPromise() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Success');
        }, 1000);
    });
}

function errorPromise() {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error('Error occurred'));
        }, 1500);
    });
}

function anotherSuccessPromise() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Another success');
        }, 2000);
    });
}

Promise.all([successPromise(), errorPromise(), anotherSuccessPromise()])
   .then((results) => {
        console.log(results); // 不会执行到这里
    })
   .catch((error) => {
        console.error('Caught error:', error.message); // 输出: Caught error: Error occurred
    });

在这个例子中,errorPromise 会在 1500 毫秒后被拒绝,Promise.all 返回的 Promise 对象也会立即被拒绝,anotherSuccessPromise 虽然还在执行,但不会影响 Promise.all 的状态。catch 块会捕获到 errorPromise 的拒绝原因。

Promise.race 的应用

Promise.race 基础概念

Promise.race 也是 JavaScript 中处理多个 Promise 对象的静态方法。它同样接受一个 Promise 对象的可迭代对象(如数组)作为参数,并返回一个新的 Promise 对象。与 Promise.all 不同的是,Promise.race 返回的 Promise 对象会在传入的可迭代对象中的任何一个 Promise 对象首先被解决(resolved)或被拒绝(rejected)时,就立即被解决或被拒绝,其解决值或拒绝原因就是第一个被解决或被拒绝的 Promise 对象的值或原因。

限时操作场景

在很多情况下,我们希望某个操作在一定时间内完成,如果超时则执行其他逻辑。可以通过 Promise.race 结合一个定时器 Promise 来实现。

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data fetched');
        }, 2500);
    });
}

function timeoutPromise(duration) {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error('Operation timed out'));
        }, duration);
    });
}

Promise.race([fetchData(), timeoutPromise(2000)])
   .then((result) => {
        console.log(result); // 如果 fetchData 在 2000 毫秒内完成,输出 'Data fetched'
    })
   .catch((error) => {
        console.error('Error:', error.message); // 如果超时,输出 'Operation timed out'
    });

在这个示例中,Promise.race 接受 fetchDatatimeoutPromise 两个 Promise 对象。如果 fetchData 在 2000 毫秒内完成,Promise.race 返回的 Promise 对象就会成功解决,否则 timeoutPromise 会在 2000 毫秒后被拒绝,导致 Promise.race 返回的 Promise 对象也被拒绝。

多个数据源优先获取

假设有多个数据源可以获取数据,我们希望尽快得到数据,而不关心是从哪个数据源获取到的。例如,一个应用可能从本地缓存和服务器同时请求数据,只要其中一个请求先返回数据,就使用该数据。

function fetchFromCache() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data from cache');
        }, 1500);
    });
}

function fetchFromServer() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data from server');
        }, 2000);
    });
}

Promise.race([fetchFromCache(), fetchFromServer()])
   .then((data) => {
        console.log('First data received:', data);
    })
   .catch((error) => {
        console.error('Error:', error);
    });

这里 Promise.race 会等待 fetchFromCachefetchFromServer 中先完成的那个 Promise 对象。如果 fetchFromCache 先完成,就会输出 Data from cache,如果 fetchFromServer 先完成,就会输出 Data from server

实时监控多个事件

在一些实时应用中,可能需要监控多个事件,只要其中一个事件发生,就执行相应的操作。例如,一个在线游戏可能同时监控玩家的输入事件、服务器推送的新消息事件等,只要有任何一个事件发生,就更新游戏状态。

function monitorPlayerInput() {
    return new Promise((resolve) => {
        // 模拟玩家输入事件
        setTimeout(() => {
            resolve('Player input detected');
        }, 3000);
    });
}

function monitorServerMessage() {
    return new Promise((resolve) => {
        // 模拟服务器消息事件
        setTimeout(() => {
            resolve('Server message received');
        }, 2000);
    });
}

Promise.race([monitorPlayerInput(), monitorServerMessage()])
   .then((event) => {
        console.log('Event occurred:', event);
        // 根据不同的事件执行相应的游戏状态更新逻辑
    })
   .catch((error) => {
        console.error('Error:', error);
    });

在这个示例中,Promise.race 会等待 monitorPlayerInputmonitorServerMessage 中先触发的事件。如果 monitorServerMessage 先触发,就会输出 Server message received,并执行相应的游戏状态更新逻辑。

错误处理与结果获取

Promise.race 的错误处理与 Promise.all 有所不同。因为 Promise.race 只关心第一个完成的 Promise 对象,所以错误处理也基于第一个完成的 Promise 对象的状态。

function successPromise() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Success');
        }, 1000);
    });
}

function errorPromise() {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error('Error occurred'));
        }, 1500);
    });
}

function anotherSuccessPromise() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Another success');
        }, 2000);
    });
}

Promise.race([successPromise(), errorPromise(), anotherSuccessPromise()])
   .then((result) => {
        console.log('First result:', result); // 输出: First result: Success
    })
   .catch((error) => {
        console.error('Error:', error.message);
    });

在这个例子中,successPromise 最先完成并成功解决,所以 Promise.race 返回的 Promise 对象也成功解决,then 块会执行并输出 First result: Success。如果 errorPromise 最先完成,Promise.race 返回的 Promise 对象就会被拒绝,catch 块会捕获到错误。

Promise.all 与 Promise.race 的组合应用

在一些复杂的场景中,可能需要同时使用 Promise.allPromise.race。例如,我们有多个分组的异步任务,每个分组内的任务需要全部完成,而不同分组之间只需要有一个分组先完成即可。

function group1Task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Group 1 Task 1 completed');
        }, 1000);
    });
}

function group1Task2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Group 1 Task 2 completed');
        }, 1500);
    });
}

function group2Task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Group 2 Task 1 completed');
        }, 2000);
    });
}

function group2Task2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Group 2 Task 2 completed');
        }, 2500);
    });
}

const group1Promises = [group1Task1(), group1Task2()];
const group2Promises = [group2Task1(), group2Task2()];

Promise.race([
    Promise.all(group1Promises),
    Promise.all(group2Promises)
])
   .then((results) => {
        console.log('First group completed:', results);
    })
   .catch((error) => {
        console.error('Error:', error);
    });

在这个示例中,Promise.all 首先确保每个分组内的任务全部完成,然后 Promise.race 等待两个分组中先完成的那个分组。如果 group1Promises 先全部完成,then 块会输出 First group completed: ['Group 1 Task 1 completed', 'Group 1 Task 2 completed']

性能考量与应用场景选择

在选择使用 Promise.all 还是 Promise.race 时,性能是一个重要的考量因素。Promise.all 适合需要等待所有异步操作都完成的场景,例如数据的批量处理、多个资源的加载等。但如果所有异步操作中有一个操作非常耗时,可能会导致整体等待时间较长。而 Promise.race 适合在多个异步操作中只需要第一个完成的结果的场景,例如限时操作、多个数据源优先获取等。它可以更快地得到结果,提高应用的响应速度。

例如,在一个需要加载多个图片的页面中,如果每个图片的加载都封装成一个 Promise 对象,使用 Promise.all 可以确保所有图片都加载完成后再进行页面渲染,保证页面的完整性。但如果是一个实时监控系统,需要尽快获取到第一个发生的事件,那么 Promise.race 就是更好的选择。

在实际应用中,还需要考虑错误处理和业务逻辑的复杂性。Promise.all 的错误处理相对简单,只要有一个 Promise 被拒绝,整个 Promise.all 就被拒绝。而 Promise.race 的错误处理取决于第一个完成的 Promise 的状态,需要更细致地考虑不同的情况。

同时,随着应用规模的扩大,可能需要结合其他工具和技术,如 async/await 语法糖来简化异步代码的编写,以及使用第三方库来处理更复杂的异步场景,如 Promise 队列、Promise 取消等功能。

兼容性与 polyfill

虽然现代浏览器和 Node.js 版本都已经原生支持 Promise.allPromise.race,但在一些旧版本的环境中可能不支持。为了确保代码的兼容性,可以使用 polyfill。例如,在不支持 Promise 的旧浏览器中,可以引入一个 Promise 的 polyfill 库,如 es6 - promise

对于 Promise.allPromise.race 本身,如果在一些不支持的环境中,也可以手动实现简单的 polyfill。以下是一个简单的 Promise.all 的 polyfill 示例:

if (!Promise.all) {
    Promise.all = function (promises) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(promises)) {
                return reject(new TypeError('Promise.all accepts an array'));
            }
            const results = [];
            let completed = 0;
            promises.forEach((promise, index) => {
                Promise.resolve(promise)
                   .then((value) => {
                        results[index] = value;
                        completed++;
                        if (completed === promises.length) {
                            resolve(results);
                        }
                    })
                   .catch((error) => {
                        reject(error);
                    });
            });
            if (promises.length === 0) {
                resolve(results);
            }
        });
    };
}

同样,也可以实现 Promise.race 的 polyfill:

if (!Promise.race) {
    Promise.race = function (promises) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(promises)) {
                return reject(new TypeError('Promise.race accepts an array'));
            }
            promises.forEach((promise) => {
                Promise.resolve(promise)
                   .then((value) => {
                        resolve(value);
                    })
                   .catch((error) => {
                        reject(error);
                    });
            });
            if (promises.length === 0) {
                return reject(new Error('Promise.race requires at least one promise'));
            }
        });
    };
}

通过这些 polyfill,可以在不支持 Promise.allPromise.race 的环境中使用它们的功能。

与其他异步处理方式的对比

在 JavaScript 中,除了 Promise.allPromise.race 外,还有其他异步处理方式,如回调函数、async/await 等。

回调函数是早期处理异步操作的主要方式,但随着异步操作的复杂性增加,回调地狱(Callback Hell)的问题就会凸显出来,代码的可读性和维护性会变得很差。例如:

function asyncOperation1(callback) {
    setTimeout(() => {
        callback('Result of async operation 1');
    }, 1000);
}

function asyncOperation2(result1, callback) {
    setTimeout(() => {
        callback(`Result of async operation 2 with ${result1}`);
    }, 1000);
}

function asyncOperation3(result2, callback) {
    setTimeout(() => {
        callback(`Result of async operation 3 with ${result2}`);
    }, 1000);
}

asyncOperation1((result1) => {
    asyncOperation2(result1, (result2) => {
        asyncOperation3(result2, (finalResult) => {
            console.log(finalResult);
        });
    });
});

Promise 的出现改善了这个问题,通过链式调用使得异步代码更加清晰。Promise.allPromise.race 更是提供了处理多个 Promise 对象的便捷方式。例如,使用 Promise 重写上面的代码:

function asyncOperation1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Result of async operation 1');
        }, 1000);
    });
}

function asyncOperation2(result1) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`Result of async operation 2 with ${result1}`);
        }, 1000);
    });
}

function asyncOperation3(result2) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`Result of async operation 3 with ${result2}`);
        }, 1000);
    });
}

asyncOperation1()
   .then((result1) => asyncOperation2(result1))
   .then((result2) => asyncOperation3(result2))
   .then((finalResult) => {
        console.log(finalResult);
    });

async/await 是基于 Promise 的更高级的语法糖,它使得异步代码看起来更像同步代码,进一步提高了代码的可读性。例如:

async function main() {
    const result1 = await asyncOperation1();
    const result2 = await asyncOperation2(result1);
    const finalResult = await asyncOperation3(result2);
    console.log(finalResult);
}

main();

在处理多个异步操作时,Promise.allPromise.raceasync/await 结合使用可以发挥更大的优势。例如,使用 async/awaitPromise.all 来处理多个并发请求:

async function fetchAllData() {
    const [data1, data2, data3] = await Promise.all([fetchData1(), fetchData2(), fetchData3()]);
    console.log(data1, data2, data3);
}

fetchAllData();

通过对比可以看出,Promise.allPromise.race 在结合 async/await 等语法时,为 JavaScript 中的异步编程提供了强大而灵活的工具。

在不同框架和库中的应用

在现代前端框架如 React、Vue.js 以及后端框架如 Node.js 的 Express 等中,Promise.allPromise.race 都有广泛的应用。

在 React 中,当需要在组件挂载时同时获取多个数据,并在所有数据都获取完成后渲染组件,可以使用 Promise.all。例如:

import React, { useEffect, useState } from'react';

function MyComponent() {
    const [data1, setData1] = useState(null);
    const [data2, setData2] = useState(null);

    useEffect(() => {
        const fetchData1 = () => new Promise((resolve) => {
            setTimeout(() => {
                resolve('Data 1');
            }, 1000);
        });

        const fetchData2 = () => new Promise((resolve) => {
            setTimeout(() => {
                resolve('Data 2');
            }, 1500);
        });

        Promise.all([fetchData1(), fetchData2()])
           .then(([result1, result2]) => {
                setData1(result1);
                setData2(result2);
            });
    }, []);

    return (
        <div>
            {data1 && <p>Data 1: {data1}</p>}
            {data2 && <p>Data 2: {data2}</p>}
        </div>
    );
}

export default MyComponent;

在 Vue.js 中,也可以类似地在 created 钩子函数中使用 Promise.all 来获取多个数据:

<template>
    <div>
        <p v - if="data1">Data 1: {{ data1 }}</p>
        <p v - if="data2">Data 2: {{ data2 }}</p>
    </div>
</template>

<script>
export default {
    data() {
        return {
            data1: null,
            data2: null
        };
    },
    created() {
        const fetchData1 = () => new Promise((resolve) => {
            setTimeout(() => {
                resolve('Data 1');
            }, 1000);
        });

        const fetchData2 = () => new Promise((resolve) => {
            setTimeout(() => {
                resolve('Data 2');
            }, 1500);
        });

        Promise.all([fetchData1(), fetchData2()])
           .then(([result1, result2]) => {
                this.data1 = result1;
                this.data2 = result2;
            });
    }
};
</script>

在 Node.js 的 Express 框架中,Promise.all 可以用于在处理请求前获取多个数据资源。例如:

const express = require('express');
const app = express();

function fetchData1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data 1');
        }, 1000);
    });
}

function fetchData2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data 2');
        }, 1500);
    });
}

app.get('/', async (req, res) => {
    try {
        const [result1, result2] = await Promise.all([fetchData1(), fetchData2()]);
        res.send(`Data 1: ${result1}, Data 2: ${result2}`);
    } catch (error) {
        res.status(500).send('Error:'+ error.message);
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

这些示例展示了 Promise.all 在不同框架和库中的常见应用方式,Promise.race 同样可以根据具体的业务需求在这些场景中发挥作用,例如在限时请求、优先获取数据等场景下。

总结与展望

Promise.allPromise.race 是 JavaScript 异步编程中非常重要的工具,它们为处理多个 Promise 对象提供了简洁而强大的方式。Promise.all 适用于需要等待所有异步操作完成的场景,而 Promise.race 则适用于只需要第一个完成的异步操作结果的场景。

随着 JavaScript 的不断发展,异步编程的重要性日益凸显。未来,我们可能会看到更多基于 Promise 的高级特性和工具出现,进一步简化和优化异步代码的编写。同时,在处理复杂的异步场景时,合理地选择和组合 Promise.allPromise.race 以及其他异步处理方式,将是开发者需要不断掌握和提升的技能。无论是在前端开发、后端开发还是全栈开发中,熟练运用这些工具都能极大地提高代码的质量和性能,为用户带来更好的体验。

在实际项目中,开发者需要根据具体的业务需求、性能要求以及代码的可维护性来选择合适的异步处理策略。通过不断地实践和总结经验,能够更好地发挥 Promise.allPromise.race 的优势,构建出更加健壮和高效的 JavaScript 应用程序。