阅读视图

发现新文章,点击刷新页面。

深度解析:fetch 与 Promise 结合实战及面试重点

在前端异步编程领域,Promise 是解决回调地狱的核心方案,而 fetch 作为现代浏览器原生支持的网络请求 API,其设计本身就深度依赖 Promise。掌握二者的结合使用,不仅是日常开发的基础,更是前端面试的高频考点。本文将从核心概念联动、实战场景应用、面试重点拆解三个维度,帮你彻底搞懂 fetch 与 Promise 的协同逻辑。

一、核心概念:为什么 fetch 天然适配 Promise?

在讲解结合用法前,我们先明确两个核心概念的关联:

1.1 Promise 的核心价值

Promise 是一种用于处理异步操作的对象,它将异步操作的结果(成功/失败)封装为可预测的状态流转,避免了多层嵌套的回调地狱。其核心特性包括:

  • 三种状态:pending(进行中)、fulfilled(成功)、rejected(失败),状态一旦改变不可逆;
  • 链式调用:通过 then() 接收成功结果,catch() 捕获错误,支持链式串联多个异步操作;
  • 异步穿透:允许在链式调用中跳过部分 then(),直接由后续 catch() 捕获错误。

1.2 fetch 的设计本质

fetch 是浏览器提供的用于替代 XMLHttpRequest 的网络请求 API,其核心设计理念就是“基于 Promise 封装异步请求”。与 XHR 不同,fetch 调用后会直接返回一个 Promise 对象:

  • 当请求发出后,Promise 处于 pending 状态;
  • 当服务器返回响应(无论 HTTP 状态码是否为 2xx),Promise 会变为 fulfilled,并将响应对象(Response)传递给 then()
  • 只有当网络故障(如断网、跨域未授权等)导致请求无法发出时,Promise 才会变为 rejected,错误会被 catch() 捕获。

注意:fetch 的 Promise 不会因 HTTP 错误状态码(如 404、500)而 reject,这是面试高频易错点!

二、实战场景:fetch 与 Promise 结合用法全解析

基于二者的天然适配性,实际开发中我们通过 Promise 的链式调用,就能优雅地处理 fetch 的请求、响应解析、错误捕获全流程。以下是高频实战场景:

2.1 基础场景:GET 请求与响应解析

fetch 默认发起 GET 请求,需通过 Response 对象的方法(如 json()text())解析响应体,而这些方法本身也返回 Promise,因此需要嵌套一层 then()

// 基础 GET 请求
fetch('https://api.example.com/data')
  // 第一次 then:接收响应对象,解析为 JSON(返回 Promise)
  .then(response => {
    // 手动处理 HTTP 错误状态码
    if (!response.ok) {
      throw new Error(`HTTP 错误:${response.status}`);
    }
    return response.json(); // 解析 JSON 格式响应体,返回 Promise
  })
  // 第二次 then:接收解析后的业务数据
  .then(data => {
    console.log('请求成功:', data);
  })
  // catch:捕获所有错误(网络错误 + 手动抛出的 HTTP 错误)
  .catch(error => {
    console.error('请求失败:', error);
  });

核心逻辑:外层 Promise 处理请求发起与响应接收,内层 Promise 处理响应体解析,通过链式调用串联,代码清晰无嵌套。

2.2 进阶场景:POST 请求与请求配置

发起 POST 请求时,需通过 fetch 的第二个参数配置请求方法、请求头、请求体等,结合 Promise 处理复杂异步逻辑:

// 定义请求数据
const postData = { username: 'test', password: '123456' };

// POST 请求
fetch('https://api.example.com/login', {
  method: 'POST', // 请求方法
  headers: {
    'Content-Type': 'application/json', // 声明请求体格式为 JSON
  },
  body: JSON.stringify(postData), // 序列化请求体(必须为字符串)
})
  .then(response => {//这里是处理fetch返回的promise对象
    if (!response.ok) throw new Error(`状态码:${response.status}`);
    return response.json();
  })
  .then(result => {//response.json也是一个promise对象
    if (result.code === 200) {
      console.log('登录成功:', result.data.token);
      // 可继续发起后续请求(如获取用户信息),形成 Promise 链式串联
      return fetch('https://api.example.com/userInfo', {
        headers: { Authorization: `Bearer ${result.data.token}` }
      });
    } else {
      throw new Error(result.msg);
    }
  })
    // 这里是获取用户对象fetch 返回的promise
  .then(userRes => userRes.json())
    // 处理获取用户信息接口返回的数据
  .then(userData => console.log('用户信息:', userData))
  .catch(error => console.error('流程失败:', error));

关键亮点:在一个 Promise 链中串联多个异步请求(登录 → 获取用户信息),前一个请求的结果作为后一个请求的参数,实现异步流程的线性化。

2.3 高级场景:Promise 工具函数结合 fetch

利用 Promise 的工具函数(如 Promise.all()Promise.race()),可实现 fetch 的批量请求或超时控制:

// 1. Promise.all():并行发起多个请求,全部成功才返回结果
const request1 = fetch('https://api.example.com/data1');
const request2 = fetch('https://api.example.com/data2');

Promise.all([request1, request2])
  .then(responses => Promise.all(responses.map(res => res.json())))
  .then([data1, data2] => {
    console.log('批量请求成功:', data1, data2);
  })
  .catch(error => console.error('任意一个请求失败:', error));

// 2. Promise.race():超时控制(请求超时时中断并抛出错误)
const timeoutPromise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('请求超时(3秒)')), 3000);
});

Promise.race([fetch('https://api.example.com/slowData'), timeoutPromise])
  .then(res => res.json())
  .then(data => console.log('请求成功:', data))
  .catch(error => console.error('请求失败:', error));

2.4 优化场景:Promise 工具函数结合async,await

利用async,await将请求更加优雅,统一处理error

const search = async() =>{
    try{
        const res = await fetch('/api/test')
        const data = res.json()
        console.log(data,'返回接口数据')
    }catch(err){
        console.log('请求出错',err)
    }
}

三、面试重点:高频问题与核心答案

fetch 与 Promise 的结合是前端面试的核心考点,以下是高频问题及精准回答思路:

3.1 问题 1:fetch 的返回值是什么?它的 Promise 什么时候会 reject?

  • fetch 的返回值是一个 Promise 对象
  • 该 Promise 仅在「网络故障」时才会 reject(如断网、跨域未配置 CORS、域名无法解析等);
  • 即使服务器返回 404、500 等 HTTP 错误状态码,Promise 仍会 resolve,此时需要通过 Response 对象的 ok 属性(true 表示 2xx 状态码)手动判断请求是否成功,并抛出错误。

3.2 问题 2:如何用 Promise 处理 fetch 的错误(包括网络错误和 HTTP 错误)?

核心答案:通过「手动判断 HTTP 状态码 + catch 捕获」的组合实现全量错误处理:

fetch('/api/data')
  .then(res => {
    // 第一步:判断 HTTP 状态码,非 2xx 则抛出错误
    if (!res.ok) throw new Error(`HTTP 错误:${res.status}`);
    return res.json();
  })
  .then(data => console.log('成功', data))
  .catch(err => {
    // 捕获两类错误:网络错误 + 手动抛出的 HTTP 错误
    console.error('失败', err);
  });

3.3 问题 3:fetch 相比 XMLHttpRequest,结合 Promise 有哪些优势?

  1. 代码更简洁:Promise 链式调用替代 XHR 的多层回调,避免回调地狱;
  2. 状态更可控:Promise 的状态不可逆特性,让异步流程更可预测;
  3. 原生适配:fetch 天生返回 Promise,无需手动封装,而 XHR 需要手动用 Promise 包裹才能实现链式调用;
  4. 功能更强大:支持 Promise 工具函数(如 Promise.all())实现批量请求,轻松处理复杂异步场景。

3.4 问题 4:如何用 Promise.race() 给 fetch 设置超时时间?

创建一个超时 Promise(指定时间后 reject),与 fetch 的 Promise 进行 race 竞争,谁先改变状态就以谁的结果为准:

function fetchWithTimeout(url, timeout = 3000) {
  // 超时 Promise
  const timeoutTask = new Promise((_, reject) => {
    setTimeout(() => reject(new Error(`超时 ${timeout}ms`)), timeout);
  });
  // 竞争:fetch 成功/失败 或 超时
  return Promise.race([fetch(url), timeoutTask]);
}

// 使用
fetchWithTimeout('/api/slowData')
  .then(res => res.json())
  .then(data => console.log('成功', data))
  .catch(err => console.error('失败', err));

3.5 问题 5:fetch 中如何实现请求中断?结合 Promise 怎么处理?

fetch 本身不支持中断,但可通过 AbortController 与 Promise 结合实现:

// 1. 创建 AbortController 实例
const controller = new AbortController();
// 2. 获取信号对象
const signal = controller.signal;

// 3. 发起 fetch 请求,将 signal 传入配置
fetch('/api/data', { signal })
  .then(res => res.json())
  .then(data => console.log('成功', data))
  .catch(err => {
    // 4. 中断时会触发 AbortError
    if (err.name === 'AbortError') {
      console.log('请求已中断');
      return;
    }
    console.error('其他错误', err);
  });

// 5. 主动中断请求(如用户点击取消按钮)
controller.abort();

原理:AbortController 的 signal 对象与 fetch 关联,调用 abort() 时,signal 会触发 abort 事件,fetch 会立即 reject 并抛出 AbortError。

四、总结

fetch 与 Promise 的结合,核心是利用 Promise 的状态管理和链式调用特性,解决 fetch 异步请求的流程控制问题。日常开发中,需重点掌握响应解析、错误处理(尤其是 HTTP 错误)、批量请求、超时控制等场景;面试中,要牢记 fetch 的 Promise 状态规则、错误处理逻辑、与 XHR 的差异及请求中断方案等核心考点。

掌握二者的协同逻辑,不仅能提升异步代码的可读性和可维护性,更能在面试中快速精准地应对高频问题。建议结合实际场景多写多练,加深对 Promise 异步流转和 fetch 核心特性的理解。

React中setState后获取更新后值的完整解决方案

在React开发中,很多新手都会遇到一个常见“坑”:调用setState更新状态后,立即读取状态却拿到旧值。这并非React的bug,而是setState的异步特性导致的。本文将从问题本质出发,分类详解类组件和函数组件中获取setState更新后值的多种方案,并补充版本差异注意事项,帮你彻底解决这个问题。

一、先搞懂:为什么setState后直接读是旧值?

React中的setState(包括类组件的this.setState和函数组件的useState更新函数)默认是异步批量更新的。这是React的性能优化策略——它会将多个setState调用合并成一次DOM更新,避免频繁重渲染带来的性能损耗。

简单说:setState的调用只是“发起更新请求”,而非“立即执行更新”。在React处理完这次更新前,状态依然保持旧值。

1.1 类组件旧值问题示例

import React from 'react';

class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    console.log('当前count:', this.state.count); // 输出:0(旧值)
  };

  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>;
  }
}

export default Counter;

1.2 函数组件旧值问题示例

import { useState } from 'react';

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

  const handleClick = () => {
    setCount(count + 1);
    console.log('当前count:', count); // 输出:0(旧值)
  };

  return <button onClick={handleClick}>{count}</button>;
};

export default Counter;

二、类组件:获取更新后值的3种方案

类组件中this.setState提供了灵活的使用方式,对应不同场景有3种可靠方案,优先推荐函数式更新和回调函数。

方案1:setState的第二个参数(回调函数)

this.setState的完整语法是:this.setState(updater, callback)。其中第二个参数是状态更新完成、DOM重新渲染后的回调函数,在这个回调内可以安全获取最新状态。

适用场景:简单状态更新后,需要立即执行依赖最新状态的逻辑(如打印、接口请求)。

class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    this.setState(
      { count: this.state.count + 1 },
      // 状态更新完成后的回调
      () => {
        console.log('更新后count:', this.state.count); // 输出:1(最新值)
        // 这里可执行依赖最新状态的逻辑,如调用接口
        // this.fetchData(this.state.count);
      }
    );
  };

  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>;
  }
}

方案2:函数式更新(依赖旧状态时优先)

如果新状态依赖于旧状态(如计数、累加),推荐将setState的第一个参数改为函数。该函数接收两个参数:prevState(更新前的最新状态)和props(当前组件props),返回新的状态对象。

优势:确保拿到的是更新前的最新状态,避免多次setState调用被合并导致的状态偏差。

class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    // 函数式更新:prevState是更新前的最新状态
    this.setState((prevState) => {
      const newCount = prevState.count + 1;
      console.log('新count(函数内):', newCount); // 输出:1(可提前拿到新值)
      return { count: newCount };
    }, () => {
      console.log('更新后count(回调):', this.state.count); // 输出:1
    });

    // 连续调用也能正确累积(若用对象式更新会只加1)
    this.setState(prev => ({ count: prev.count + 1 })); // 最终count=2
  };

  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>;
  }
}

方案3:componentDidUpdate生命周期(不推荐,冗余)

componentDidUpdate是组件更新完成后的生命周期钩子,在这个钩子内可以获取最新状态。但这种方式会监听所有状态的更新,需要额外判断目标状态是否变化,冗余度较高,仅在特殊场景下使用。

class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  // 组件更新完成后执行
  componentDidUpdate(prevProps, prevState) {
    // 仅当count变化时执行逻辑
    if (prevState.count !== this.state.count) {
      console.log('更新后count:', this.state.count); // 输出:1
      // 依赖最新count的逻辑
    }
  }

  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>;
  }
}

三、函数组件:获取更新后值的3种方案

函数组件中没有this.setState,也没有componentDidUpdate生命周期,需结合useState、useEffect、useRef等Hook实现,核心思路与类组件一致,但用法更简洁。

方案1:useEffect监听状态变化(最常用)

useEffect是函数组件的“副作用钩子”,可以监听状态变化。将目标状态放入useEffect的依赖数组,当状态更新时,useEffect的回调函数会执行,此时能拿到最新状态。

适用场景:状态更新后执行后续逻辑(如接口请求、DOM操作),是函数组件中最推荐的方案。

import { useState, useEffect } from 'react';

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

  // 监听count变化,count更新后执行
  useEffect(() => {
    console.log('更新后count:', count); // 每次count变化都输出最新值
    // 依赖最新count的逻辑,如接口请求
    // fetch(`/api/data?count=${count}`);
  }, [count]); // 依赖数组:仅当count变化时触发

  const handleClick = () => {
    setCount(count + 1);
  };

  return <button onClick={handleClick}>{count}</button>;
};

export default Counter;

方案2:函数式更新(依赖旧状态时优先)

与类组件的函数式更新逻辑一致,useState的更新函数也可以接收一个函数,参数是更新前的最新状态(prevState),返回新状态。

优势:避免因异步更新导致的状态偏差,支持连续多次更新。

import { useState } from 'react';

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

  const handleClick = () => {
    // 函数式更新:prevCount是更新前的最新状态
    setCount((prevCount) => {
      const newCount = prevCount + 1;
      console.log('新count(函数内):', newCount); // 输出:1
      return newCount;
    });

    // 连续调用正确累积
    setCount(prev => prev + 1); // 最终count=2
  };

  return <button onClick={handleClick}>{count}</button>;
};

方案3:useRef保存最新值(异步回调场景)

如果需要在setTimeout、Promise等异步回调中随时获取最新状态,推荐使用useRef。useRef的current属性是可变的,不会触发组件重渲染,可用来实时保存状态的最新值。

适用场景:异步回调中需要访问最新状态(React 18中异步场景的批量更新会让直接读状态失效)。

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

const Counter = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(count); // 用ref保存最新count

  // 每次count变化,更新ref的current值
  useEffect(() => {
    countRef.current = count;
  }, [count]);

  const handleClick = () => {
    setCount(count + 1);

    // 异步回调中获取最新值
    setTimeout(() => {
      console.log('异步回调最新count:', countRef.current); // 输出:1(最新值)
      console.log('直接读count(旧值):', count); // 输出:0(旧值)
    }, 1000);
  };

  return <button onClick={handleClick}>{count}</button>;
};

四、关键注意事项(避坑重点)

1. React 18的自动批处理特性

React 18中,所有场景(包括setTimeout、Promise、原生事件、axios回调等)的setState都会被自动批量更新。这意味着即使在异步回调中调用setState,依然是异步的,直接读取状态仍可能拿到旧值。

示例(React 18中):

const handleClick = () => {
  setTimeout(() => {
    setCount(count + 1);
    console.log(count); // 输出:0(旧值,因批量更新异步)
  }, 0);
};

解决方案:使用上述的useRef或useEffect方案。

2. 避免过度依赖setState回调

不要在setState回调中执行大量耗时操作(如复杂计算、循环),否则会阻塞DOM更新,影响组件性能。耗时操作建议放在setTimeout中或使用Web Worker。

3. 状态依赖必用函数式更新

当新状态依赖旧状态(如count += 1、list.push(newItem))时,必须使用函数式更新(prevState => newState),否则可能因多次setState合并导致状态错误。

五、总结:不同场景的最优方案选型

组件类型 推荐方案 适用场景
类组件 setState回调函数 简单状态更新后立即获取最新值
函数式更新 新状态依赖旧状态,或连续多次更新
函数组件 useEffect监听状态 状态更新后执行后续逻辑(如接口请求)
函数式更新 新状态依赖旧状态,或连续多次更新
useRef保存最新值 异步回调中随时获取最新状态

最后

React中setState的异步特性是为了性能优化,理解其本质后,就能根据具体场景选择合适的方案。记住核心原则:不依赖setState后的同步读取,通过回调、Hook监听或函数式更新获取最新状态,就能轻松避坑。

如果你的项目中还有其他setState相关的问题,欢迎在评论区交流~

❌