普通视图

发现新文章,点击刷新页面。
昨天以前首页

React Hook 核心指南:从实战到源码,彻底掌握状态与副作用

2025年8月16日 14:07

Hook 的出现让函数组件拥有了管理状态和副作用的能力,极大地提升了代码的可读性和复用性。今天,我想和大家分享 React 中最核心、最常用的几个 Hook:useStateuseEffectuseContextuseReduceruseCallbackuseMemouseRef。我会从基本用法讲起,结合实际例子,深入探讨它们的原理和最佳实践,并手写简化版实现,最后附上高频面试题。希望这篇博客能让你对 React Hook 有更深刻的理解。

1. useState:管理组件状态

1.1 基本概念

useState 是最基础的 Hook,用于在函数组件中添加状态。它接收一个初始状态值,返回一个包含当前状态和更新函数的数组。

const [state, setState] = useState(initialState);
  • state:当前状态值。
  • setState:更新状态的函数。
  • initialState:初始状态,可以是任意值(原始值、对象、函数等)。

1.2 使用场景

计数器是最经典的例子:

import React, { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>当前计数: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                增加
            </button>
            <button onClick={() => setCount(prevCount => prevCount - 1)}>
                减少
            </button>
        </div>
    );
}

关键点:

  • setState 是异步的,多次调用会进行批处理。
  • 更新函数可以接收一个函数,该函数的参数是前一个状态值(prevState),这在需要基于前一个状态更新时非常有用。

1.3 手写实现(简化版)

// 模拟 React 内部状态存储
let hooks = [];
let currentHookIndex = 0;

function useState(initialValue) {
    const hookIndex = currentHookIndex;

    // 如果是第一次调用,初始化状态
    if (!hooks[hookIndex]) {
        hooks[hookIndex] = initialValue;
    }

    // 返回当前状态和更新函数
    const setState = (newValue) => {
        // 如果传入的是函数,执行它
        const value = typeof newValue === 'function' 
            ? newValue(hooks[hookIndex]) 
            : newValue;
        
        hooks[hookIndex] = value;
        // 模拟触发重新渲染
        render();
    };

    return [hooks[hookIndex], setState];
}

// 模拟组件渲染
function render() {
    currentHookIndex = 0; // 重置索引
    // 重新执行组件函数
    App();
}

function App() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('张三');

    console.log('Count:', count, 'Name:', name);
}

// 初始渲染
App(); // Count: 0 Name: 张三
setCount(1); // Count: 1 Name: 张三
setName('李四'); // Count: 1 Name: 李四

注意: 这只是一个极度简化的模型,真实的 React 使用 Fiber 节点和 Hook 链表来管理状态。

1.4 注意事项

  • 不要在条件或循环中使用 Hook:React 依赖 Hook 的调用顺序来正确匹配状态。违反规则会导致 Invalid hook call 错误。
  • 状态更新是异步的setState 后立即读取状态可能得不到最新值。
  • 函数式更新:当新状态依赖于前一个状态时,使用函数形式 setState(prev => prev + 1)

2. useEffect:处理副作用

2.1 基本概念

useEffect 用于在函数组件中执行副作用操作,如数据获取、订阅、手动 DOM 操作等。它替代了类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount

useEffect(() => {
    // 执行副作用
    return () => {
        // 清理副作用(可选)
    };
}, [dependencies]); // 依赖数组

2.2 使用场景

场景一:数据获取

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

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        // 模拟 API 请求
        const fetchUser = async () => {
            setLoading(true);
            try {
                // 假设这里调用 API
                const response = await fetch(`/api/users/${userId}`);
                const userData = await response.json();
                setUser(userData);
            } catch (error) {
                console.error('获取用户信息失败:', error);
            } finally {
                setLoading(false);
            }
        };

        fetchUser();
    }, [userId]); // 仅当 userId 变化时重新执行

    if (loading) return <div>加载中...</div>;
    if (!user) return <div>用户不存在</div>;

    return (
        <div>
            <h1>{user.name}</h1>
            <p>{user.email}</p>
        </div>
    );
}

场景二:订阅和清理

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

function MouseTracker() {
    const [position, setPosition] = useState({ x: 0, y: 0 });

    useEffect(() => {
        const handleMouseMove = (event) => {
            setPosition({ x: event.clientX, y: event.clientY });
        };

        // 添加事件监听器
        window.addEventListener('mousemove', handleMouseMove);

        // 返回清理函数
        return () => {
            window.removeEventListener('mousemove', handleMouseMove);
        };
    }, []); // 空依赖数组,只在挂载时执行一次

    return (
        <div>
            鼠标位置: {position.x}, {position.y}
        </div>
    );
}

2.3 依赖数组详解

  • []:只在组件挂载时执行一次(类似 componentDidMount)。
  • [dep1, dep2]:当 dep1dep2 变化时执行。
  • 不传:每次组件重新渲染时都执行(不推荐,容易造成性能问题或无限循环)。

2.4 手写实现(简化版)

let dependencies = [];
let cleanup = null;

function useEffect(callback, deps) {
    const hasChanged = !deps || 
        deps.some((dep, index) => !dependencies[index] || dep !== dependencies[index]);

    if (hasChanged) {
        // 执行清理函数(如果存在)
        if (cleanup) {
            cleanup();
        }
        // 执行副作用
        cleanup = callback();
        // 更新依赖
        dependencies = deps || [];
    }
}

// 模拟组件
function Component({ count }) {
    useEffect(() => {
        console.log('Effect 执行,count:', count);
        return () => {
            console.log('清理 Effect,count:', count);
        };
    }, [count]);

    return <div>Count: {count}</div>;
}

// 初始渲染
Component({ count: 0 }); // Effect 执行,count: 0

// 更新
Component({ count: 1 }); // 清理 Effect,count: 0 -> Effect 执行,count: 1

2.5 注意事项

  • 必须返回函数或 undefined:清理函数必须是同步的。
  • 避免无限循环:确保依赖数组正确,避免在 useEffect 内部更新依赖项。
  • 异步操作:不能直接在 useEffect 回调中使用 async,需要在内部创建异步函数。
// ❌ 错误
useEffect(async () => {
    const data = await fetchData();
}, []);

// ✅ 正确
useEffect(() => {
    const fetchData = async () => {
        const data = await fetchData();
    };
    fetchData();
}, []);

3. useContext:跨组件传递数据

3.1 基本概念

useContext 用于订阅 React 的 Context。它接收一个 context 对象(React.createContext 的返回值),并返回当前 context 的值。

const value = useContext(MyContext);

3.2 使用场景

主题切换是经典例子:

import React, { createContext, useContext, useState } from 'react';

// 创建 Context
const ThemeContext = createContext();

// 主题提供者组件
function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');

    const toggleTheme = () => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
    };

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}

// 使用 Context 的组件
function Header() {
    const { theme, toggleTheme } = useContext(ThemeContext);

    return (
        <header style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
            <h1>我的网站</h1>
            <button onClick={toggleTheme}>
                切换到 {theme === 'light' ? '暗色' : '亮色'} 主题
            </button>
        </header>
    );
}

// 根组件
function App() {
    return (
        <ThemeProvider>
            <Header />
            <MainContent />
        </ThemeProvider>
    );
}

3.3 手写实现(简化版)

// 模拟 Context
const contextStack = [];

function createContext(defaultValue) {
    return { defaultValue };
}

function Provider({ context, value, children }) {
    contextStack.push(value);
    const result = children;
    contextStack.pop();
    return result;
}

function useContext(context) {
    return contextStack[contextStack.length - 1] || context.defaultValue;
}

// 使用
const ThemeContext = createContext('light');

function ThemeProvider({ children }) {
    const [theme] = useState('dark');
    return Provider({ context: ThemeContext, value: theme, children });
}

function ThemeDisplay() {
    const theme = useContext(ThemeContext);
    return <div>当前主题: {theme}</div>;
}

3.4 注意事项

  • useContext 会订阅 Context 的变化,当 Providervalue 变化时,所有使用该 Context 的组件都会重新渲染。
  • 避免将 Context 用于频繁变化的状态,可能导致性能问题。

4. useReducer:管理复杂状态逻辑

4.1 基本概念

useReduceruseState 的替代方案,适用于状态逻辑较复杂的情况(如包含多个子值或下一个状态依赖于前一个状态)。它接收一个 reducer 函数和初始状态,返回当前状态和 dispatch 函数。

const [state, dispatch] = useReducer(reducer, initialState);
  • reducer(state, action) => newState,纯函数。
  • initialState:初始状态。

4.2 使用场景

管理表单或购物车状态:

import React, { useReducer } from 'react';

// 定义 action 类型
const ADD_ITEM = 'ADD_ITEM';
const REMOVE_ITEM = 'REMOVE_ITEM';
const UPDATE_QUANTITY = 'UPDATE_QUANTITY';

// Reducer 函数
function cartReducer(state, action) {
    switch (action.type) {
        case ADD_ITEM:
            return {
                ...state,
                items: [...state.items, action.payload]
            };
        case REMOVE_ITEM:
            return {
                ...state,
                items: state.items.filter(item => item.id !== action.payload.id)
            };
        case UPDATE_QUANTITY:
            return {
                ...state,
                items: state.items.map(item =>
                    item.id === action.payload.id
                        ? { ...item, quantity: action.payload.quantity }
                        : item
                )
            };
        default:
            return state;
    }
}

function ShoppingCart() {
    const [state, dispatch] = useReducer(cartReducer, { items: [] });

    const addItem = (item) => {
        dispatch({ type: ADD_ITEM, payload: item });
    };

    const removeItem = (id) => {
        dispatch({ type: REMOVE_ITEM, payload: { id } });
    };

    return (
        <div>
            <h2>购物车</h2>
            <ul>
                {state.items.map(item => (
                    <li key={item.id}>
                        {item.name} - 数量: {item.quantity}
                        <button onClick={() => removeItem(item.id)}>删除</button>
                    </li>
                ))}
            </ul>
            <button onClick={() => addItem({ id: Date.now(), name: '商品', quantity: 1 })}>
                添加商品
            </button>
        </div>
    );
}

4.3 手写实现(简化版)

function useReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);

    const dispatch = (action) => {
        const newState = reducer(state, action);
        setState(newState);
    };

    return [state, dispatch];
}

// 使用
const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        default:
            return state;
    }
}, { count: 0 });

4.4 注意事项

  • useReducer 适合状态更新逻辑复杂或需要处理多个 action 的场景。
  • reducer 必须是纯函数。

5. useCallback:缓存函数

5.1 基本概念

useCallback 返回一个记忆化的回调函数。它接收一个内联回调函数和依赖数组,只有当依赖项变化时,才会返回新的函数。

const memoizedCallback = useCallback(
    () => {
        doSomething(a, b);
    },
    [a, b],
);

5.2 使用场景

避免子组件不必要的重新渲染:

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

// 子组件(使用 React.memo 优化)
const ExpensiveComponent = React.memo(({ onClick, value }) => {
    console.log('ExpensiveComponent 渲染');
    return (
        <button onClick={onClick}>
            点击我 ({value})
        </button>
    );
});

function Parent() {
    const [count, setCount] = useState(0);
    const [text, setText] = useState('');

    // 使用 useCallback 缓存函数
    const handleClick = useCallback(() => {
        setCount(prev => prev + 1);
    }, []); // 依赖数组为空,函数不会变化

    return (
        <div>
            <p>计数: {count}</p>
            <input value={text} onChange={e => setText(e.target.value)} />
            {/* 传递缓存的函数 */}
            <ExpensiveComponent onClick={handleClick} value={count} />
        </div>
    );
}

如果没有 useCallback,每次 Parent 重新渲染时,handleClick 都会是一个新的函数,导致 ExpensiveComponent 重新渲染。

5.3 手写实现(简化版)

let memoizedCallback = null;
let deps = null;

function useCallback(callback, dependencies) {
    const hasChanged = !deps || 
        dependencies.some((dep, index) => !deps[index] || dep !== deps[index]);

    if (hasChanged) {
        memoizedCallback = callback;
        deps = dependencies;
    }

    return memoizedCallback;
}

5.4 注意事项

  • 只在需要传递给子组件且子组件使用 React.memo 时才使用 useCallback
  • 过度使用 useCallback 可能导致内存占用增加。

6. useMemo:缓存计算结果

6.1 基本概念

useMemo 返回一个记忆化的值。它接收一个计算函数和依赖数组,只有当依赖项变化时,才会重新计算。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

6.2 使用场景

优化昂贵的计算:

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

function Fibonacci({ n }) {
    // 模拟昂贵的计算
    const fibonacci = (num) => {
        if (num <= 1) return num;
        return fibonacci(num - 1) + fibonacci(num - 2);
    };

    // 使用 useMemo 缓存计算结果
    const result = useMemo(() => fibonacci(n), [n]);

    return <div>斐波那契数列第 {n} 项: {result}</div>;
}

function App() {
    const [n, setN] = useState(10);
    const [input, setInput] = useState('');

    return (
        <div>
            <input value={input} onChange={e => setInput(e.target.value)} placeholder="输入文本" />
            <Fibonacci n={n} />
            <button onClick={() => setN(prev => prev + 1)}>增加 n</button>
        </div>
    );
}

如果没有 useMemo,每次 input 变化导致 App 重新渲染时,都会重新计算 fibonacci(n)

6.3 手写实现(简化版)

let memoizedValue = null;
let deps = null;

function useMemo(factory, dependencies) {
    const hasChanged = !deps || 
        dependencies.some((dep, index) => !deps[index] || dep !== deps[index]);

    if (hasChanged) {
        memoizedValue = factory();
        deps = dependencies;
    }

    return memoizedValue;
}

6.4 注意事项

  • 只在计算确实昂贵时使用 useMemo
  • 不要为了“优化”而使用,React 的重新渲染本身并不慢。

7. useRef:获取 DOM 节点或保存可变值

7.1 基本概念

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。ref 对象在组件的整个生命周期内保持不变。

const refContainer = useRef(initialValue);

7.2 使用场景

场景一:访问 DOM 元素

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

function TextInputWithFocusButton() {
    const inputEl = useRef(null);

    const onButtonClick = () => {
        // 直接操作 DOM
        inputEl.current.focus();
    };

    return (
        <>
            <input ref={inputEl} type="text" />
            <button onClick={onButtonClick}>
                聚焦输入框
            </button>
        </>
    );
}

场景二:保存可变值(不触发重新渲染)

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

function Timer() {
    const [count, setCount] = useState(0);
    // 使用 ref 保存计时器 ID
    const intervalRef = useRef();

    useEffect(() => {
        intervalRef.current = setInterval(() => {
            setCount(prev => prev + 1);
        }, 1000);

        return () => {
            // 清理时使用 ref
            clearInterval(intervalRef.current);
        };
    }, []);

    return <div>计数: {count}</div>;
}

7.3 手写实现(简化版)

function useRef(initialValue) {
    const refObject = { current: initialValue };
    return refObject;
}

7.4 注意事项

  • useRef 返回的对象在组件生命周期内是同一个。
  • 修改 .current 不会触发组件重新渲染。
  • 可以用于保存任何可变值,如定时器 ID、上一次的 props 等。

8. 常见面试题

  1. useStatesetState 是同步还是异步?

    • 在 React 事件处理中是异步批处理的;在 setTimeout 或原生事件中是同步的。
  2. useEffect 的清理函数在什么时候执行?

    • 在组件卸载时,或在下一次 useEffect 执行前(如果依赖变化)。
  3. useCallbackuseMemo 的区别是什么?

    • useCallback 缓存函数,useMemo 缓存值。useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
  4. 如何在 useEffect 中使用 async 函数?

    • useEffect 回调内部定义并立即调用一个 async 函数。
  5. useRefcreateRef 的区别?

    • useRef 在函数组件中使用,每次渲染返回同一个对象;createRef 每次调用都返回新对象,通常在类组件中使用。
  6. 为什么不能在条件中使用 Hook?

    • React 依赖 Hook 的调用顺序来正确匹配状态,条件使用会破坏这个顺序。
  7. useReducer 适合什么场景?

    • 状态逻辑复杂、包含多个子值、或下一个状态依赖于前一个状态时。
  8. useContext 会导致性能问题吗?

    • 如果 Providervalue 频繁变化,可能会导致所有订阅的组件重新渲染。可以使用 useMemo 缓存 value 或拆分 Context

结语

React Hook 让函数组件变得强大而灵活。掌握这些核心 Hook 的用法、原理和最佳实践,是成为一名优秀 React 开发者的关键。记住:

  • useState:管理简单状态。
  • useEffect:处理副作用,注意依赖和清理。
  • useContext:跨层级传递数据。
  • useReducer:管理复杂状态逻辑。
  • useCallback / useMemo:性能优化,按需使用。
  • useRef:访问 DOM 或保存可变值。

在实际项目中,多思考、多实践,你会发现 Hook 的组合能解决各种复杂的 UI 问题。希望这篇博客能成为你深入 React 的坚实阶梯。

深入理解 Promise 的高阶用法:从入门到手写实现

2025年8月16日 14:00

今天,我想和大家深入聊聊 Promise 的几个高阶方法:Promise.allPromise.racePromise.allSettledPromise.any。这些方法在处理多个异步任务时非常有用,也是面试中的常客。我会从基本用法讲起,结合实际例子,最后手写实现这些方法,并附上常见的面试题。

1. Promise.all:等待所有任务完成

1.1 基本概念

Promise.all 是一个静态方法,它接收一个可迭代对象(通常是数组),其中包含多个 Promise 实例。它的作用是等待所有 Promise 都成功完成,然后返回一个新 Promise,该 Promise 的结果是一个包含所有 Promise 结果的数组。

关键点:

  • 所有 Promise 都必须成功,Promise.all 返回的 Promise 才会成功。
  • 如果其中任何一个 Promise 失败(rejected),Promise.all 会立即失败,并返回第一个失败的 Promise 的错误。

1.2 使用场景

想象一下,你正在开发一个电商网站,需要同时获取用户信息、购物车数据和推荐商品列表。这三个请求是独立的,但你希望在所有数据都准备好后,再渲染页面。

// 模拟三个异步请求
function fetchUserInfo() {
    return new Promise((resolve) => {
        setTimeout(() => resolve({ name: '张三', age: 25 }), 1000);
    });
}

function fetchCartData() {
    return new Promise((resolve) => {
        setTimeout(() => resolve(['商品A', '商品B']), 800);
    });
}

function fetchRecommendations() {
    return new Promise((resolve) => {
        setTimeout(() => resolve(['商品C', '商品D']), 1200);
    });
}

// 使用 Promise.all
Promise.all([fetchUserInfo(), fetchCartData(), fetchRecommendations()])
    .then(([userInfo, cartData, recommendations]) => {
        console.log('用户信息:', userInfo);
        console.log('购物车:', cartData);
        console.log('推荐商品:', recommendations);
        // 渲染页面
    })
    .catch(error => {
        console.error('获取数据失败:', error);
        // 处理错误,比如显示错误提示
    });

在这个例子中,Promise.all 等待所有三个请求都成功后,才执行 .then 回调。如果任何一个请求失败(比如网络错误),Promise.all 会立即进入 .catch 分支。

1.3 手写实现

现在我们来手写一个 Promise.all 的简化版本:

function myPromiseAll(promises) {
    // 返回一个新的 Promise
    return new Promise((resolve, reject) => {
        // 如果传入的不是可迭代对象,直接 resolve 空数组
        if (!Array.isArray(promises)) {
            return resolve([]);
        }

        const results = [];
        let completedCount = 0;

        // 如果传入空数组,直接 resolve
        if (promises.length === 0) {
            return resolve(results);
        }

        promises.forEach((promise, index) => {
            // 使用 Promise.resolve 确保每个元素都是 Promise
            Promise.resolve(promise)
                .then(value => {
                    results[index] = value;
                    completedCount++;

                    // 当所有 Promise 都完成时,resolve 结果数组
                    if (completedCount === promises.length) {
                        resolve(results);
                    }
                })
                .catch(error => {
                    // 任何一个 Promise 失败,立即 reject
                    reject(error);
                });
        });
    });
}

// 测试
myPromiseAll([
    Promise.resolve('第一个'),
    new Promise(resolve => setTimeout(() => resolve('第二个'), 500)),
    '第三个' // 普通值也会被包装成 Promise
]).then(results => {
    console.log(results); // ['第一个', '第二个', '第三个']
});

1.4 注意事项

  • Promise.all 的结果数组顺序与传入的 Promise 数组顺序一致,不保证执行顺序
  • 如果传入的 Promise 中有 rejectPromise.all 会立即失败,不会等待其他 Promise 完成。

2. Promise.race:谁先完成就用谁

2.1 基本概念

Promise.race 也是接收一个 Promise 数组,但它只关心第一个完成的 Promise,无论是成功还是失败。一旦有 Promise 完成,Promise.race 就会立即返回那个结果。

关键点:

  • 返回第一个完成的 Promise 的结果或错误。
  • 适用于超时控制或竞态场景。

2.2 使用场景

最常见的用途是实现请求超时。比如,我们希望一个请求在 3 秒内完成,否则就认为它失败了。

// 模拟一个可能很慢的请求
function slowRequest() {
    return new Promise(resolve => {
        setTimeout(() => resolve('请求成功'), 5000); // 5秒
    });
}

// 创建一个超时的 Promise
function timeout(ms) {
    return new Promise((_, reject) => {
        setTimeout(() => reject(new Error(`请求超时: ${ms}ms`)), ms);
    });
}

// 使用 Promise.race 实现超时
Promise.race([slowRequest(), timeout(3000)])
    .then(result => {
        console.log('成功:', result);
    })
    .catch(error => {
        console.error('失败:', error.message); // 输出: 失败: 请求超时: 3000ms
    });

在这个例子中,timeout(3000) 会在 3 秒后 reject,而 slowRequest() 需要 5 秒。由于 timeout 先完成,Promise.race 会立即进入 .catch 分支。

2.3 手写实现

function myPromiseRace(promises) {
    return new Promise((resolve, reject) => {
        if (!Array.isArray(promises) || promises.length === 0) {
            return reject(new Error('Promise.race requires a non-empty iterable'));
        }

        promises.forEach(promise => {
            Promise.resolve(promise)
                .then(resolve)  // 第一个 resolve 或 reject 会决定最终结果
                .catch(reject);
        });
    });
}

// 测试
myPromiseRace([
    new Promise(resolve => setTimeout(() => resolve('A'), 100)),
    new Promise(resolve => setTimeout(() => resolve('B'), 50)),
    new Promise((_, reject) => setTimeout(() => reject(new Error('C')), 30))
])
.then(result => {
    console.log(result); // 不会执行,因为 C 先 reject
})
.catch(error => {
    console.error(error.message); // 输出: C
});

2.4 注意事项

  • Promise.race 的“完成”包括 resolvereject。只要有一个 Promise 结束,它就结束。
  • 适合用于竞态场景,比如多个数据源中取最快的一个。

3. Promise.allSettled:等待所有任务尘埃落定

3.1 基本概念

Promise.allSettled 是 ES2020 引入的新方法。它和 Promise.all 类似,都会等待所有 Promise 完成,但关键区别在于:它不会因为某个 Promise 失败而中断。无论成功还是失败,它都会等待所有 Promise 结束,并返回一个描述每个 Promise 结果的对象数组。

返回值结构:

  • 成功的 Promise{ status: 'fulfilled', value: result }
  • 失败的 Promise{ status: 'rejected', reason: error }

3.2 使用场景

当你需要知道所有异步任务的结果,而不关心它们是否成功时,Promise.allSettled 就派上用场了。比如批量上传文件,你希望知道每个文件的上传结果,而不是因为一个失败就放弃。

function uploadFile(filename) {
    return new Promise((resolve, reject) => {
        const success = Math.random() > 0.3; // 模拟 70% 成功率
        setTimeout(() => {
            success ? resolve(`${filename} 上传成功`) : reject(new Error(`${filename} 上传失败`));
        }, 1000);
    });
}

const files = ['file1.txt', 'file2.txt', 'file3.txt', 'file4.txt'];

Promise.allSettled(files.map(uploadFile))
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`${files[index]}: ✅ ${result.value}`);
            } else {
                console.log(`${files[index]}: ❌ ${result.reason.message}`);
            }
        });
    });

输出可能类似于:

file1.txt: ✅ file1.txt 上传成功
file2.txt: ❌ file2.txt 上传失败
file3.txt: ✅ file3.txt 上传成功
file4.txt: ✅ file4.txt 上传成功

3.3 手写实现

function myPromiseAllSettled(promises) {
    return new Promise((resolve) => {
        if (!Array.isArray(promises)) {
            return resolve([]);
        }

        const results = [];
        let settledCount = 0;

        if (promises.length === 0) {
            return resolve(results);
        }

        promises.forEach((promise, index) => {
            Promise.resolve(promise)
                .then(value => {
                    results[index] = { status: 'fulfilled', value };
                    settledCount++;
                    if (settledCount === promises.length) {
                        resolve(results);
                    }
                })
                .catch(reason => {
                    results[index] = { status: 'rejected', reason };
                    settledCount++;
                    if (settledCount === promises.length) {
                        resolve(results);
                    }
                });
        });
    });
}

// 测试
myPromiseAllSettled([
    Promise.resolve('成功1'),
    Promise.reject(new Error('失败1')),
    '普通值'
]).then(results => {
    console.log(results);
    // [
    //   { status: 'fulfilled', value: '成功1' },
    //   { status: 'rejected', reason: Error: 失败1 },
    //   { status: 'fulfilled', value: '普通值' }
    // ]
});

3.4 注意事项

  • Promise.allSettled 永远不会 reject,它总是 resolve 一个结果数组。
  • 适合用于批量操作,需要收集所有结果的场景。

4. Promise.any:至少有一个成功

4.1 基本概念

Promise.any 是 ES2021 引入的方法。它会等待第一个成功(fulfilled)的 Promise,并返回其结果。只有当所有 Promise 都失败时,它才会 reject,并返回一个 AggregateError,其中包含所有失败的原因。

关键点:

  • 返回第一个成功的 Promise 的结果。
  • 只有当所有 Promise 都失败时,才会 reject。

4.2 使用场景

想象你需要从多个镜像源下载一个文件,只要有一个源成功,就可以下载。这比 Promise.race 更合适,因为 race 会返回第一个结果(可能是失败),而 any 只关心成功。

function downloadFromSource(source) {
    return new Promise((resolve, reject) => {
        const success = Math.random() > 0.6; // 模拟 40% 成功率
        setTimeout(() => {
            success ? resolve(`从 ${source} 下载成功`) : reject(new Error(`从 ${source} 下载失败`));
        }, 1000 + Math.random() * 1000);
    });
}

const sources = ['源A', '源B', '源C', '源D'];

Promise.any(sources.map(downloadFromSource))
    .then(result => {
        console.log('下载成功:', result);
    })
    .catch(error => {
        console.error('所有源都失败:', error.errors); // AggregateError
    });

如果至少有一个源成功,就会进入 .then;如果全部失败,才会进入 .catch

4.3 手写实现

function myPromiseAny(promises) {
    return new Promise((resolve, reject) => {
        if (!Array.isArray(promises) || promises.length === 0) {
            return reject(new AggregateError([], 'All promises were rejected'));
        }

        const errors = [];
        let rejectedCount = 0;

        promises.forEach(promise => {
            Promise.resolve(promise)
                .then(resolve) // 第一个成功就 resolve
                .catch(error => {
                    errors.push(error);
                    rejectedCount++;
                    if (rejectedCount === promises.length) {
                        reject(new AggregateError(errors, 'All promises were rejected'));
                    }
                });
        });
    });
}

// 测试
myPromiseAny([
    Promise.reject(new Error('失败1')),
    new Promise(resolve => setTimeout(() => resolve('成功2'), 500)),
    Promise.reject(new Error('失败3'))
])
.then(result => {
    console.log(result); // 成功2
})
.catch(error => {
    console.error('全部失败:', error.errors);
});

4.4 注意事项

  • Promise.any 会忽略失败,直到找到第一个成功。
  • 如果所有 Promise 都失败,它会 reject 一个 AggregateError

5. 四种方法对比总结

方法 成功条件 失败条件 返回值 适用场景
Promise.all 所有成功 任一失败 成功值数组 所有任务必须成功
Promise.race 第一个完成(无论成功/失败) 第一个完成(无论成功/失败) 第一个结果 竞态、超时
Promise.allSettled 所有完成(无论成功/失败) 永不失败 结果对象数组 收集所有结果
Promise.any 第一个成功 所有失败 第一个成功值 至少一个成功

6. 常见面试题

  1. Promise.allPromise.race 的区别是什么?

    • all 等待所有成功,race 等待第一个完成。
  2. 如果 Promise.all 中有一个 Promise 失败,会发生什么?

    • Promise.all 会立即 reject,并返回第一个失败的 Promise 的错误。
  3. Promise.allSettledPromise.all 有什么不同?

    • allSettled 不会因为某个 Promise 失败而中断,它会等待所有 Promise 完成并返回结果。
  4. Promise.any 在什么情况下会 reject?

    • 只有当所有 Promise 都 reject 时,Promise.any 才会 reject。
  5. 手写 Promise.all 的实现。

    • 如上文所示,注意处理空数组、非数组输入和 Promise.resolve 包装。
  6. 如何实现一个带超时功能的 Promise

    • 使用 Promise.race,结合一个定时 reject 的 Promise
  7. Promise.all 的结果数组顺序是否与传入数组一致?

    • 是的,顺序与传入的 Promise 数组顺序一致。
  8. 为什么 Promise.all 要用 Promise.resolve 包装每个元素?

    • 为了确保每个元素都是 Promise,兼容普通值或 thenable 对象。

结语

Promise 的这些高阶方法极大地丰富了我们处理异步操作的能力。理解它们的差异和适用场景,不仅能写出更健壮的代码,也能在面试中游刃有余。记住,选择哪个方法取决于你的业务需求:

  • 全部成功?用 all
  • 第一个结果?用 race
  • 所有结果?用 allSettled
  • 至少一个成功?用 any

希望这篇博客能帮你彻底掌握这些 Promise 的高阶用法。在实际开发中多尝试使用它们,你会发现异步编程可以如此优雅。

❌
❌