React使用useEffect自定义useFetch时组件为何两次渲染?
问题现象
- 页面最终展示内容:
Hello { "name" : "Jack" } - 控制台打印顺序如下:
- App rendering
- useFetch useEffect
- App rendering
相关代码
App.js
import { useFetch } from './useFetch'; function App() { const { data } = useFetch({ url : "jack.json" }) console.log('App rendering') return ( <div className="App"> <div>Hello</div> <div>{JSON.stringify(data)}</div> </div> ) } export default App;
useFetch.js
import { useState, useEffect } from "react"; export const useFetch = (options) => { const [data, setData] = useState(null); useEffect(() => { console.log("useFetch useEffect"); fetch(options.url) .then( response => response.json()) .then( json => setData(json)) }, []) return { data } }
逐步骤底层执行流程拆解
整个执行逻辑完全遵循React的渲染规则,不存在“渲染速度太快看不到”的玄学,每一步都是可预测的:
首次挂载渲染阶段(同步执行)
- React开始首次挂载App组件,同步调用App函数
- 函数执行到
useFetch调用时,同步进入自定义Hook逻辑:- 首先执行
useState(null),初始化data状态为初始值null - 注册useEffect回调:useEffect属于副作用调度API,不会在渲染阶段同步执行,React只会把这个回调存入待执行的effect队列,等整个组件渲染提交到DOM之后才会运行
- 同步执行到useFetch的return语句,返回
{ data: null }
- 首先执行
- App拿到返回的data值为null,执行console.log打印第一条日志
App rendering,生成首次渲染的虚拟DOM。这里要纠正之前的推测错误:首次渲染时data是null不是undefined,对应页面data区域会渲染null,如果本地接口响应速度极快,这个状态确实可能肉眼无法捕捉,但逻辑上是真实存在的 - React把虚拟DOM转换为真实DOM挂载到页面,首次渲染流程结束
副作用执行阶段(渲染提交后异步非阻塞执行)
- 首次DOM挂载完成后,React开始清空待执行的effect队列,执行useFetch里注册的useEffect回调:
- 首先打印第二条日志
useFetch useEffect - 发起fetch请求,fetch返回的是Promise对象,
.then的回调会被推入微任务队列,不会阻塞主线程,useEffect回调本身执行完毕
- 首先打印第二条日志
- 首次DOM挂载完成后,React开始清空待执行的effect队列,执行useFetch里注册的useEffect回调:
状态更新触发重渲染阶段
- 事件循环执行微任务队列里的fetch回调:先把响应解析为JSON格式的
{ "name": "Jack" },接着调用setData(json)触发状态更新 - React检测到useFetch内部的data状态变化,标记App组件需要重渲染
- React重新调用App函数执行二次渲染:
- 再次进入useFetch逻辑,此时
useState返回的是更新后的data值{ "name": "Jack" } - 因为useEffect的依赖数组为空
[],不会重复注册或执行该副作用 - useFetch返回
{ data: { "name": "Jack" } } - App执行console.log打印第三条日志
App rendering,生成新的虚拟DOM
- 再次进入useFetch逻辑,此时
- React对比新旧虚拟DOM的差异,更新真实DOM中data对应的区域,页面最终展示
Hello { "name" : "Jack" }
- 事件循环执行微任务队列里的fetch回调:先把响应解析为JSON格式的
核心疑问解答
- 为什么App组件会触发2次渲染?
两次渲染的触发原因完全独立:第一次是组件首次挂载的初始化渲染,第二次是useFetch内部data状态从
null更新为接口返回值触发的重渲染。React的重渲染规则只关心组件使用的状态是否发生更新,和状态更新的逻辑写在组件本体还是自定义Hook里没有关系。
- 为什么useFetch不能等待内部fetch执行完成再返回值?
本质原因是React组件渲染和Hook执行都是同步的,没有能力暂停渲染流程等待异步逻辑完成:
- 自定义Hook本质就是普通JavaScript函数,组件渲染时会同步执行Hook内的所有同步代码、拿到返回值后继续后续渲染流程,不可能为了等待异步请求阻塞整个渲染,否则会导致页面长时间白屏,交互无响应。
- 如果要实现“等异步数据加载完再渲染组件内容”的效果,需要配合React的Suspense异步组件能力做上层封装,普通的
useState+useEffect写法无法实现首次返回就拿到异步结果。
内容的提问来源于stack exchange,提问作者John
相关产品推荐
相关产品推荐

