news 2026/9/19 7:41:41

React核心概念深度解析:虚拟DOM、Fiber与生命周期一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React核心概念深度解析:虚拟DOM、Fiber与生命周期一次讲透

我一直觉得,React 是一个“入门容易,学明白难”的东西。很多人搭好环境、写完几个组件、跑通了 ToDoList,就以为自己会 React 了,结果面试一问生命周期、Fiber、为什么函数组件每次都要重新执行,瞬间卡壳。这个“React系列-1”就把这些绕不开的核心问题一次性讲透,不为赶进度,而是帮你把 React 的心智模型建起来。适合刚学完 HTML/CSS/JavaScript 基础知识、准备系统学习 React 的初学者,也适合已经用 React 写过业务、但始终觉得底层概念不够扎实的前端开发者。

在这个系列里,我会尽量少讲“怎么调用”,多讲“为什么这么设计”。因为我在实际面试和带新人的过程中发现,大部分人写不好 React 代码,并不是API记得不熟,而是没想明白 React 背后的那一套逻辑。这篇文章作为第一篇,先从 React 到底解决了什么问题讲起,接着把虚拟 DOM、Fiber、JSX、生命周期这些高频概念一次说清,再配合环境搭建和实操项目,最后聊聊那些让人头皮发麻的常见报错和低端机卡顿问题。

1. 先想明白:React 到底在解决什么问题

1.1 前端开发的“DOM地狱”时代

在 React 出现之前,我们写前端页面基本都是“命令式”操作 DOM。什么意思呢?就是页面上的每一个状态变化,都需要你手动找到对应的 DOM 节点,然后去修改它。比如用户输入了一个数字,你不仅要拿到输入框的值,还得把计算结果更新到几个不同的元素里;用户点击了按钮,你要先给按钮绑定事件,再在回调里逐条修改页面上相关的多个位置。这种开发方式在小页面里勉强能跑,但一旦业务逻辑复杂起来,代码就变成了一团乱麻:到处都是getElementByIdappendChildinnerHTML,数据和页面之间的同步关系全靠人脑硬记。

我记得自己第一次维护一个老项目时,光是改一个“用户头像上传后需要刷新三个角标”的需求,就翻了十几个函数。因为页面上的同一份数据可能散落在七八个 DOM 节点里,你漏更新一个,就会出现“数据变了、页面没变”的诡异 bug。这种痛点不是哪个团队独有的,而是整个前端行业都面临的问题:状态一变,我们就要手工去同步视图,成本太高,而且特别容易出错。

1.2 React 给出的解法:组件化、声明式、单向数据流

React 的核心价值,就是把“状态到视图的同步”这件事变成自动化的。你只需要告诉 React:“当我的数据是这样的,页面就应该长成那样”。至于数据变了之后,具体该更新哪个节点、怎么更新,React 会帮你去算,去执行。这种开发方式叫“声明式”编程,跟传统的“命令式”编程恰好相反。拿做饭来类比的话,命令式是“你盯着锅里,感觉差不多了就翻勺,再感觉差不多了就加盐”;声明式则更像是你告诉厨师“我要一份七分熟的牛排”,剩下的烹饪过程由他负责。

除此之外,React 还引入了两个很关键的思路。第一个是组件化,把页面拆成一个个独立的小块,比如搜索框是一个组件、列表是一个组件、评论区是一个组件。每个组件只管自己的状态和渲染逻辑,互相之间通过 props 传数据,这样代码就天然有了边界,出了问题能很快定位到是哪个组件。第二个是单向数据流,数据从父组件流向子组件,子组件不能直接改父组件传下来的 props,只能通过回调事件通知父组件去改。这个设计在一开始看起来有些麻烦,但正是它保证了数据变化的方向是清晰可追踪的,不至于像是“多个组件互相交叉修改”那样失控。

1.3 声明式 UI 的核心:render 函数与渲染输出

我一直跟团队里的人说,理解 React 的第一个坎,就是明白“组件渲染”这件事。你在组件里写的那一大段 JSX,本质上并不是直接把它塞进浏览器页面里,而是让 React 拿到一个“视图描述对象”。每次状态变化,React 都会重新调用组件函数,生成一份全新的描述,然后跟上一次的描述做对比,找到差异后才会去操作真实 DOM。

这也是为什么很多初级开发者感到奇怪:明明我只改了一个数字,为什么整个组件函数里的代码又跑了一遍?因为在 React 的设计里,组件函数就像一个“根据状态生成视图描述”的工厂,只要状态有任何变化,工厂就得重新开工,哪怕产出的结果跟前一版几乎一模一样。这份“重新生成、再对比、再更新”的流程,才是 React 工作的真正本质。后面讲 Fiber 的时候你也会看到,React 做的那些复杂调度,都是围绕“如何高效地处理这次重新生成和更新”来展开的。

2. 别再背虚拟 DOM 了:从 diff 到 Fiber,搞懂它为什么快

2.1 虚拟 DOM 是什么,为什么需要它

虚拟 DOM 这个概念,面试基本必问,但很多人只是背了个定义,说“虚拟 DOM 就是用 JS 对象来模拟真实 DOM,可以提高性能”。这个说法其实有点模糊。更准确地说,虚拟 DOM 其实是“用 JS 对象描述一份 UI 结构”,它不是一个具体的浏览器 API,而是 React 自己维护的一个普通 JavaScript 对象树。每次渲染时,React 会拿新的对象树和旧的对象树做对比,这个对比过程叫做 diff。

为什么要多此一举?直接操作真实 DOM 不是更直接吗?问题在于,真实 DOM 本身是个很臃肿的东西,一个简单的div节点上就挂着几百个属性和方法。频繁地创建、修改、销毁真实 DOM,代价非常高。而 JS 对象就轻量得多,React 先在内存里算好“最小变更集合”,再一次性应用到真实 DOM 上,就能避免无谓的浏览器重排和重绘。当然,这不是说虚拟 DOM 就绝对比手动操作 DOM 快,而是在高复杂度、高交互频率的场景下,它能帮你从“手工优化 DOM 更新”的重复劳动里解脱出来。

2.2 Fiber 架构解决了什么问题

很多人听说过“Fiber 架构”,但不知道它到底解决了个什么问题。我可以从实际体验讲起:以前 React 的更新过程是同步、递归、不可中断的,也就是说,一旦开始更新整棵组件树,就得一口气跑完。组件一多、层级一深,这个“一口气”就可能让页面卡顿很长时间,用户点击没反应、输入卡顿,体感就是“白屏”或者“卡死”。用个更生活化的说法,旧版 React 更新 UI 像做饭的时候把冰箱里所有菜一次性全拿出来做,中间不能停,其他事全被堵住。

Fiber 要解决的,就是“让更新过程可以拆开、可以暂停、可以优先处理更重要的事情”。它把更新工作切分成一个个小的“工作单元”,每个单元做完后,React 会看看主线程有没有更紧急的任务(比如用户输入、动画),如果有就先让路,等主线程空闲了再继续干。这样页面就不会因为一个大的更新任务而长时间无法响应。这也是面试里常说的“时间切片”和“可中断渲染”。虽然 React 开发者平时不用直接跟 Fiber 打交道,但理解这一点,你就会明白为什么 React 一直强调“不要阻塞主线程”,也更容易理解useTransitionstartTransition这些 API 存在的价值。

2.3 为什么组件每次都要执行并返回 render 结果

这是个高频面试题,也是新手最容易困惑的点。其实答案就在上面的机制里:因为 React 需要通过重新执行组件来获取最新的 UI 描述,用这份新描述跟旧描述做 diff,才能知道到底要更新哪里。你写的函数组件本身就是一个函数,React 每次渲染都会调用它。函数组件没有“记忆”,每次执行都是从头到尾跑一遍,返回一份全新的 JSX 结构。至于函数内部用到的useStateuseMemo这些 hooks,其实是 React 在函数外部的某个数据结构里帮你保存了状态,函数每次执行时,是从这个外部结构里读取值,而不是函数本身记住了什么。

我见过一个不太恰当但很形象的理解方式:组件就像一台照片打印机,每次打印都会重新走一遍完整的出图流程,给你一张新照片。React 拿到这张新照片,跟上一张对比,发现只有“一角”变了,就只更新那一角。如果你希望“某些计算不要每次渲染都重新做”,就用useMemouseCallback去缓存结果,但这属于优化手段,不能改变“函数每次渲染都要执行”的基本事实。

3. 环境搭建与第一个 React 项目

3.1 用 Vite 还是 Create React App

到了这一步,我强烈建议你用 Vite,而不是老的 Create React App。原因很现实:Create React App(简称 CRA)虽然还是很多教程里的默认选择,但它构建速度慢、配置封装太死,而且官方已经慢慢减少维护精力了。Vite 启动极快,开发体验也顺滑。如果你去找工作,现在的团队新项目也基本都在用 Vite。所以别再纠结“教程用的是 CRA 怎么办”,直接上 Vite 不会错。

初始化一个 React + TypeScript 的项目,正常情况下只需要一条命令:

npm create vite@latest react-demo -- --template react-ts

如果不想用 TypeScript,就把后面的react-ts换成react就行。初始化完成后,依次执行:

cd react-demo npm install npm run dev

浏览器会自动打开http://localhost:5173,看到 Vite 的默认页面就说明环境跑通了。这里要注意一个细节:Node.js 版本尽量选 18 以上,太低的话 Vite 可能直接报错,我见过很多新手卡在这。

3.2 项目的核心结构与入口逻辑

项目跑起来后,打开src目录,你会看到几个关键的文件夹和文件。main.tsx是入口文件,里面做的事情非常直白:把你定义的根组件挂载到index.html中的某个 DOM 节点上。这个过程的官方说法叫“挂载”,也就是 React 从这里开始接管页面。

App.tsx是根组件。它只是一个普通的函数组件,export default function App(),然后return一段 JSX。在 React 的世界里,“组件”本质上就是一个返回 JSX 的函数(或者一个返回 JSX 的类)。你可以在App里再引入别的子组件,一层套一层,形成组件树。还有一个容易被忽略的文件是index.css,里面是全局样式。刚上手时不必急着做 CSS 方案选型,先用普通 CSS 写好样式就够了。

3.3 JSX:一种长得像 HTML 的 JavaScript 语法

JSX 是 React 代码里最直观的语法,但它并不是 HTML。它只是一个语法糖,最终会通过编译工具转成 JavaScript 函数调用。所以在 JSX 里,有一些跟 HTML 不一样的小规则:class要写成className,因为class在 JavaScript 里是关键字;style要接收一个对象,style={{ color: 'red' }},而不是一个字符串;所有的标签都必须闭合,哪怕是没有子元素的<input /><br />

在 JSX 中想要输出变量或表达式,用一对花括号包起来,这个特性叫“插值”。比如:

const name = '张三'; return <h1>你好,{name}</h1>;

如果你要在页面上渲染一组数据,通常用map方法。注意,渲染列表时必须给每个元素加上唯一的key属性,否则 React 会警告,而且更新数据时可能会出现状态错乱的问题。这个key不能随便用数组下标,尤其是列表数据会动态增删、排序的时候,用下标当key很容易出隐藏 bug。正确做法是使用数据中自带的唯一 id,比如用户 id、商品 id 等。我踩过这个坑,一个列表因为 key 总是变化,导致输入框里的内容在每次更新时被重置,排查了大半天才发现是 key 的问题。

4. 生命周期:类组件与函数组件

4.1 类组件的生命周期到底在什么时候触发

React 生命周期这个概念,是从类组件时代一直传下来的。早期 React 组件必须用 class 来写,然后在特定阶段执行你预设的方法。最常用的有几个:constructorrendercomponentDidMountcomponentDidUpdatecomponentWillUnmount

用大家容易理解的话说:componentDidMount是组件第一次被插入页面后触发,适合在这里发网络请求、初始化第三方库;componentDidUpdate是组件更新之后触发,可以拿到更新前后的 props 和 state;componentWillUnmount是组件从页面移除前触发,适合清理定时器、取消订阅。过去的开发中,这三兄弟是“请求数据”、“响应更新”、“清理现场”的标准配置。但现在新项目里,再用 class 组件写代码的人很少了,它更多是作为历史遗产存在,面试可能会问,但实操中你主要跟函数组件打交道。

4.2 用 useEffect 接管一切:函数组件时代的生命周期

在函数组件里,没有componentDidMount这些方法,取而代之的是useEffect。这个 hooks 的设计非常有意思,它把所有“组件跟外部世界打交道”的副作用统一收拢到一个 API 里。你可以把useEffect理解成“在渲染之后执行一些事情”,具体什么时候执行、执行多少次,取决于第二个参数——依赖数组。

import { useEffect, useState } from 'react'; function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/user/${userId}`) .then((res) => res.json()) .then((data) => setUser(data)); }, [userId]); if (!user) return <p>加载中...</p>; return <div>{user.name}</div>; }

依赖数组是[userId],意思是:这个 effect 只在组件首次渲染和userId变化之后执行。如果你写成空数组[],那就只会在首次渲染后执行;如果完全不写第二个参数,那每次渲染后都会执行,这在大多数情况下不是你想要的,还可能引发性能问题。

4.3 常见误区:依赖数组别乱写

我在 review 别人代码时,见过最多的坑就是依赖数组写错了。写少了,会导致 effect 里用了某个变量但没声明依赖,拿到的是上一次渲染的旧值;写多了,会导致 effect 莫名其妙频繁执行。尤其是当你用到useEffect去监听某个 props 变化时,一定要把“这个 effect 里用到了哪些外部变量”想清楚,再填进依赖数组。

还有一个新手容易犯的错误:在useEffect里调用setState,然后又把这个 state 加进依赖数组,结果造成无限循环。举个例子,如果上面代码里依赖数组写成了[user],每次请求完都会设置 user,user 一变 effect 又重新执行,又发一次请求,又设置 user。页面就会疯狂请求接口。解决办法是理清因果关系:useEffect的触发应该是“数据变化导致副作用”,而不是“副作用的结果继续触发副作用”。如果实在纠缠不清,建议把数据逻辑抽出来,用useMemo或者自定义 hook 来管理,而不是把所有东西都塞进useEffect

5. React 和 Vue 怎么选:写给纠结的你

5.1 两者解决的是同一个问题,但思路不同

React 和 Vue 都是现在最主流的前端框架,也都能做组件化、响应式更新,但底层思路差异很大。Vue 的响应式系统是“自动追踪依赖”的,你在data里声明一个变量,模板里用到它,它一旦变化,Vue 会自动精准更新相关位置。React 则更偏向“手动”一点:你要自己用useState声明状态,状态变化后组件函数重新执行,React 再通过 diff 去找出变化。因此,Vue 的上手曲线通常更平缓,写起来更接近“直觉”;React 的设计则更强调“数据流”的显眼,代码的推导性更强,但学习成本相对高一些。

拿“响应式更新”来打个比方:Vue 像是给每个数据装了一个小喇叭,数据一变,喇叭就通知用到它的人;React 则是每次数据一变,整栋楼的广播都响一遍,再由一个高效的调度员决定哪些人需要真的行动。这个比喻不太严谨,但很能说明两者在感受上的差异。

5.2 上手难度、生态、团队因素对比

从学习路线上看,Vue 因为有官方提供的vue-routerPinia等全家桶,新手不太需要在工具选型上纠结。React 的生态更开放,官方只提供核心库,路由用React Router,状态管理可能选ReduxZustandJotai,图表有EChartsRecharts等,选择很多,但也容易让人选择困难。React Native 的存在,是 React 生态里一个很重要的加分项:如果你学完 React 还想做移动端 App,可以直接用 React Native 复用大量既有知识。这也是很多团队选择 React 的原因之一。

关于“到底选哪个”这个问题,我的建议是:如果你是自学、为了找工作,React 的岗位数量在市场上通常更多,而且 React 对 JS 功底的锻炼更明显,面试时也更有话题可聊。如果你是在公司里快速开发中后台项目,团队又没人有强 React 偏好,Vue 其实足够用,开发效率很高。说到底,框架只是工具,真正重要的还是你对组件化设计、数据流管理、性能优化这些通用能力的理解。把这些玩明白了,框架之间的切换并没有你想象中那么难。

5.3 面试这么答:React 和 Vue 的核心区别

面试里被问到 React 和 Vue 的区别时,别只说“Vue 是模板语法,React 是 JSX”这种表面回答。更好的切入点是:响应式原理不同、组件更新粒度不同、生态与设计哲学不同。举一个典型的例子:在 Vue 中,数据被修改后,依赖这个数据的组件会立刻得到通知;而在 React 中,setState后组件的重新渲染是整体性的,React 需要靠虚拟 DOM diff 来“找出变化”。虽然两者最后都会更新到真实 DOM,但中间那套调度机制完全不同。再加上 Vue 官方支持模板编译期的静态优化,React 则是把优化主动权更多交还给开发者,这些差异会直接影响你对“性能优化”的具体操作方式。

6. 新手最容易踩的坑:白屏、报错和卡顿

6.1 React 项目启动白屏,问题多半出在挂载节点

白屏问题在 React 项目里太常见了,而且往往是最打击人的,因为浏览器一片空白,什么提示都没有。大多数情况下,问题出在挂载节点上:入口的index.html里可能找不到<div id="root"></div>,或者main.tsx里用的挂载 id 跟 html 里的不一致。React 找不到挂载节点,自然就成了白屏。解决方法是先打开浏览器的开发者工具,看看控制台有没有报错,再检查index.htmlmain.tsx的 id 是否对得上。

另一种白屏原因是组件内部报错,导致整个渲染中断。React 默认没有一个兜底的错误边界,错误一旦在渲染过程中抛出,可能整个页面都挂掉。解决方案是新增一个错误边界组件,用componentDidCatch捕获异常并显示备用 UI。虽然这个 API 在类组件时代才有,但函数组件时代一个常见的做法是封装一个ErrorBoundary类组件,然后用它包裹你的路由页面或者业务模块,这样即使某个组件出错了,也不至于整个应用白屏。

6.2 Minified React Error #130:先开开发模式

很多人在部署后的浏览器控制台里看到类似Minified React error #130的错误,瞬间就慌了。这种“#编号”错误,是 React 在生产环境下的压缩报错信息,因为生产包会把完整的错误信息去掉以减小体积,只保留一个编号。真正有用的完整错误信息,只有在开发模式下才能看到。所以排查他的第一步,不是去查这个编号是什么意思,而是先把项目跑在开发模式里,复现一遍同样的操作,控制台就会显示出完整、可读的错误描述。如果你用的是 Vite,开发模式下默认就是非压缩的,报错信息会很完整;如果是打包部署后的产品,就记得先看 source map 或者本地开发环境里复现。

除了 #130,React 还经常会出现一个经典报错:Each child in a list should have a unique key prop。这个报错就是前面说的列表渲染时缺少唯一 key。它不是致命错误,页面还能显示,但会导致某些组件状态错乱,而且控制台会一直刷警告。好的习惯是渲染列表时第一时间把 key 写对,别等报错了再补。

6.3 React Native 低端机卡顿的优化思路

如果你往移动端走,React Native 是绕不开的。“React Native 在安卓低端机很卡”是很多人的抱怨,这个问题的根源很复杂,但核心矛盾在于 React Native 的 bridge 通信机制:每次状态更新都要在 JavaScript 线程和原生线程之间来回通信,低端机性能弱,就容易出现掉帧、白屏、触控延迟。

优化思路一般有几个方向:减少不必要的组件重新渲染(用React.memo包裹列表项、在 FlatList 里使用getItemLayout提前计算布局高度);把复杂的计算放到原生模块里做,而不是塞在 JS 线程里;图片加载用适当的分辨率,避免低端机解码大图;以及尽量少用Inline style,减少每次渲染需要传递的样式数据量。这些优化技巧,任何一个 React Native 项目里都可能用得上,绝不是“等出问题了再优化”的事情,而是初期设计时就应该考虑的。

6.4 React 图表库怎么选:兼顾中文生态与性能

如果你在 React 项目里需要画图表,最常被提到的几个库是RechartsEChartsAnt Design ChartsvisxRecharts声明式写法跟 React 的组件化思想很一致,文档清晰,适合做常见折线图、柱状图、饼图。ECharts的优势是功能全面、中文文档丰富、社区方案多,虽然不是为 React 量身定制,但通过echarts-for-react封装后也比较好用。如果你产品里有大量复杂图表、大数据量渲染场景,我建议直接用ECharts那套,占内存稍大但功能兜底能力强。选图表库时还要注意打包体积,如果只是展示一两个简单图表,引入一个重型图表库并不划算。

6.5 排查思路小结:把问题定位到“渲染阶段”还是“副作用阶段”

最后我想分享一个排查 React 问题的核心思路:遇到任何一个 bug,先判断它发生在“渲染阶段”还是“副作用阶段”。渲染阶段就是组件函数执行、生成 JSX 的过程,这个阶段应该纯净,不该有网络请求、定时器、DOM 操作。如果你发现数据在渲染阶段被修改了,那就可能触发了无限循环渲染。副作用阶段则是useEffect里的事情,比如请求数据、订阅事件。如果界面显示正常但数据不更新,优先查useEffect的依赖数组和请求逻辑;如果渲染结果本身不对,优先查 props 和 state 是否传对了。这个思路特别管用,能帮你把五花八门的报错快速归类,不至于东试一下西试一下。

写在最后

React 系列的第一篇,我们先把这些基础但关键的概念理清楚。如果你是从零开始,不用着急把所有 API 记下来,重点是把“状态驱动视图”、“组件树”、“虚拟 DOM 与 diff”、“Fiber 调度”这四件事想透彻。后续我会继续在这个系列里拆解组件通信、性能优化、React Router、状态管理和服务端渲染这些进阶话题。我自己带过不少新人,发现凡是基础打得稳的,后面学什么都快;反之基础不牢,看再多项目源码都是事倍功半。所以第一篇文章我给的建议很简单:把 JS 基础再夯实一些,然后亲手跟着实践写一个小组件,遇到报错别急着搜答案,先按“渲染阶段还是副作用阶段”这个思路自己推理一遍。React 这东西,多错几次、多修几次,比看十篇文章都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 7:40:49

Omarchy 研发基础设施迁移至 DigitalOcean 云平台实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 7:40:16

ESP32-P4 USB从设备实现稳定MSC读卡器

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 7:36:11

连锁超市进销存系统设计:UML建模与数据库账实一致实践

简介&#xff1a;面向连锁超市进销存管理场景的信息系统分析与设计课程设计报告&#xff0c;适合计算机、信息管理相关专业学生作为课程设计或毕业设计的参考资料。内容覆盖系统背景、可行性分析、系统分析与设计、系统实施测试全流程&#xff0c;结构完整&#xff0c;具有较强…

作者头像 李华
网站建设 2026/9/19 7:34:43

AI在药物靶点识别中的应用与开源工具生态

1. 靶点识别技术演进与AI赋能药物研发领域正在经历一场由人工智能驱动的范式变革。在传统药物发现流程中&#xff0c;靶点识别阶段平均需要3-6年时间&#xff0c;消耗整个研发预算的30%以上。而现代AI技术正在将这个周期压缩到数月级别&#xff0c;同时显著降低试错成本。1.1 传…

作者头像 李华