声明:GLM4.5生成,辅助复习
好的!咱们用最接地气的大白话,把 React 的组件基础 这个"地基"给夯得结结实实!想象一下,React 就像一套超级好玩的乐高积木,而组件就是这套积木里最基本的零件块。
🧩 一、组件是什么?------ 乐高积木块!
- 大白话: 组件就是一个独立、可复用的小零件!就像乐高积木里的一块砖、一个轮子、一个窗户。你可以用这些小零件拼出大汽车、大房子(整个应用)。
- 为什么重要? React 的核心思想就是**"万物皆组件"**!把复杂的页面拆成一个个功能明确的小组件,这样:
- 好维护: 哪块砖坏了,换掉那块就行,不用拆整栋楼。
- 好复用: 一个轮子组件,可以在汽车上用,可以在飞机上用,不用每次都重新造轮子。
- 好理解: 看到一堆小零件组合,比看一坨乱麻的代码清晰多了。
- 两种写法(现在主流是第一种):
-
函数组件 (最常用!像普通员工):
jsxfunction Welcome(props) { return <h1>你好, {props.name}!</h1>; // 返回要显示的内容 }
- 大白话: 就是一个普通的 JavaScript 函数!它接收一个叫
props
的"快递包裹"(后面讲),然后返回 一段用 JSX(后面讲)写的"界面代码"。简单、直接、现在最流行!
- 大白话: 就是一个普通的 JavaScript 函数!它接收一个叫
-
类组件 (像部门经理,带状态和生命周期):
jsxclass Welcome extends React.Component { render() { return <h1>你好, {this.props.name}!</h1>; } }
- 大白话: 是一个 ES6 的类!它必须继承
React.Component
,并且必须有一个render()
方法,这个方法返回 JSX 写的界面。类组件有自己的"小金库"(state
)和"人生阶段"(生命周期),但现在函数组件 + Hooks 也能做到,而且更简单,所以新项目基本都用函数组件了。
- 大白话: 是一个 ES6 的类!它必须继承
-
✍️ 二、JSX 是什么?------ 给 JavaScript 加了"画皮"!
- 大白话: JSX 看起来像 HTML,但它不是真正的 HTML !它是一种 JavaScript 的语法扩展,让你能在 JS 代码里直接写"类似 HTML"的标签。React 会把它翻译成真正的 JavaScript 对象(虚拟 DOM),最后再变成浏览器能看的 HTML。
- 为什么重要? 让写界面变得超级直观!不用再像以前那样用一堆 JS 命令去拼凑 HTML 字符串了。
- 关键点(新手容易踩坑):
-
用
{}
包 JS 表达式: 在 JSX 里想写 JS 变量、计算、函数调用,必须用花括号{}
包起来!jsxconst name = "小明"; const element = <h1>你好, {name}!</h1>; // 正确! // const element = <h1>你好, name!</h1>; // 错误!会显示字符串 "name"
-
className
代替class
: 因为class
是 JavaScript 的关键字(用来定义类),所以 JSX 里写样式类名要用className
。jsx// 错误! // <div class="container">内容</div> // 正确! <div className="container">内容</div>
-
标签必须闭合: 像
<img>
,<br>
这种 HTML 里可以不闭合的标签,在 JSX 里必须 写成自闭合形式<img />
,<br />
。 -
根元素只能有一个: 一个组件的 JSX 返回,最外层必须 有且只有一个父标签。如果不想多一个无意义的
div
,可以用<>
和</>
(Fragment) 包起来。jsx// 错误! // return <h1>标题</h1><p>段落</p>; // 正确!用 div 包 return ( <div> <h1>标题</h1> <p>段落</p> </div> ); // 正确!用 Fragment 包(推荐,不会在DOM里多加div) return ( <> <h1>标题</h1> <p>段落</p> </> );
-
📦 三、props
(属性) ------ 爸爸给儿子的"快递包裹"
-
大白话:
props
(properties 的缩写) 是从父组件传递给子组件的数据 !就像爸爸(父组件)给儿子(子组件)寄一个快递包裹,里面装着儿子需要的东西(数据)。子组件只能读,不能改! -
为什么重要? 这是组件间通信最基本、最常用 的方式!让组件变得可配置 、可复用 。同一个按钮组件,通过不同的
props
(文字、颜色、大小),可以变成不同的按钮。 -
怎么用?
-
父组件传值: 在子组件标签上写属性名=值,就像给快递贴单子。
jsxfunction App() { return <Welcome name="小红" age={18} />; // 传字符串用引号,传数字用{} }
-
子组件接收: 函数组件通过参数接收(通常叫
props
),类组件通过this.props
接收。jsx// 函数组件接收 function Welcome(props) { return ( <div> <p>名字: {props.name}</p> <p>年龄: {props.age}</p> </div> ); } // 类组件接收 class Welcome extends React.Component { render() { return ( <div> <p>名字: {this.props.name}</p> <p>年龄: {this.props.age}</p> </div> ); } }
-
-
核心规则:只读! 子组件拿到
props
后,绝对不能直接修改它!就像儿子不能拆爸爸寄来的包裹再寄回去(除非爸爸允许)。想改变数据?得让爸爸(父组件)自己改,然后重新传新的包裹过来。
💰 四、state
(状态) ------ 组件自己的"小金库"
- 大白话:
state
是组件自己内部管理的数据 !就像组件自己口袋里的钱,想怎么花(改)就怎么花(改)。state
一改变,组件就会自动重新渲染(刷新界面)! - 为什么重要? 这是让组件"活起来 "的关键!比如按钮的点击次数、输入框的内容、开关的开关状态、列表的数据... 这些会变化的东西,都应该放在
state
里。 - 怎么用?
-
函数组件:用
useState
Hook (最常用!)jsximport React, { useState } from 'react'; // 1. 引入 function Counter() { // 2. 声明一个 state 变量 `count`,初始值是 0 // setCount 是用来更新 count 的函数 const [count, setCount] = useState(0); return ( <div> <p>你点了 {count} 次</p> {/* 3. 点击按钮时,调用 setCount 更新 state */} <button onClick={() => setCount(count + 1)}> 点我 +1 </button> </div> ); }
- 关键点:
useState(初始值)
返回一个数组:[当前值, 更新函数]
。- 必须用
setCount
来更新! 直接改count = count + 1
无效!React 感知不到。 - 更新是异步的: React 可能会合并多次更新,不要依赖
setCount
后立刻拿到新值。 - 不可变数据: 更新对象或数组时,要创建新对象/新数组,不要直接改原对象/原数组!(比如
setUser({...user, age: 20})
而不是user.age = 20
)
- 关键点:
-
类组件:用
this.state
和this.setState()
jsxclass Counter extends React.Component { constructor(props) { super(props); // 1. 在构造函数里初始化 state this.state = { count: 0 }; } render() { return ( <div> <p>你点了 {this.state.count} 次</p> {/* 2. 调用 this.setState 更新 state */} <button onClick={() => this.setState({ count: this.state.count + 1 })}> 点我 +1 </button> </div> ); } }
- 关键点:
- 在
constructor
里用this.state = { ... }
初始化。 - 必须用
this.setState()
更新! 直接改this.state.count
无效且是错误实践! this.setState
可以接受对象({ count: newCount }
)或函数((prevState, props) => ({ count: prevState.count + 1 })
),后者更安全(避免依赖旧 state)。- 合并更新:
this.setState
会自动合并你传入的对象到当前 state 中(只更新你指定的属性)。
- 在
- 关键点:
-
🖱️ 五、事件处理 ------ 给组件装上"遥控器"
- 大白话: 就是处理用户在界面上做的操作,比如点击按钮、输入文字、鼠标移入移出等。React 给这些操作绑定了处理函数("遥控器按键")。
- 关键点:
-
命名: React 事件名用驼峰命名法 (
onClick
,onChange
,onMouseOver
),而不是 HTML 的小写(onclick
,onchange
)。 -
绑定函数: 事件处理函数通常用
{}
包裹一个函数。jsx// 函数组件 function Button() { const handleClick = () => { alert('按钮被点了!'); }; return <button onClick={handleClick}>点我</button>; } // 类组件 class Button extends React.Component { handleClick() { alert('按钮被点了!'); } render() { // 注意:这里需要用 this.handleClick,并且通常需要绑定 this (见下) return <button onClick={this.handleClick}>点我</button>; } }
-
类组件的
this
问题(坑!): 在类组件里,事件处理函数里的this
默认是undefined
,不是组件实例!需要绑定this
:-
方法1:构造函数里绑定 (推荐)
jsxclass Button extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); // 绑定! } handleClick() { console.log(this); // 现在是 Button 组件实例了 } // ... render }
-
方法2:使用箭头函数 (类属性语法)
jsxclass Button extends React.Component { handleClick = () => { // 箭头函数自动绑定 this console.log(this); // 是 Button 组件实例 } // ... render }
-
方法3:在 JSX 里用箭头函数 (不推荐,每次渲染都创建新函数,可能影响性能)
jsxrender() { return <button onClick={() => this.handleClick()}>点我</button>; }
-
-
传递参数: 想在事件处理函数里传额外参数?
-
函数组件: 用箭头函数包裹。
jsxfunction DeleteButton({ id }) { const handleDelete = (idToDelete) => { console.log(`要删除 ID 为 ${idToDelete} 的项目`); }; return ( <button onClick={() => handleDelete(id)}> 删除 </button> ); }
-
类组件: 同样用箭头函数包裹,或者用
.bind(this, 参数)
。jsxclass DeleteButton extends React.Component { handleDelete(idToDelete) { console.log(`要删除 ID 为 ${idToDelete} 的项目`); } render() { return ( <button onClick={() => this.handleDelete(this.props.id)}> 删除 </button> ); } }
-
-
🔄 六、条件渲染 ------ 让组件"会变脸"
- 大白话: 根据不同的情况(比如
state
的值、props
的值),让组件显示不同的内容或完全不显示。就像一个演员,根据剧本演不同的角色。 - 常用方法:
-
if
语句: 最直接,在return
之前判断。jsxfunction Greeting({ isLoggedIn }) { if (isLoggedIn) { return <h1>欢迎回来!</h1>; } else { return <h1>请先登录。</h1>; } }
-
三元运算符
? :
: 简洁,适合在 JSX 里直接写。jsxfunction Greeting({ isLoggedIn }) { return ( <div> {isLoggedIn ? ( <h1>欢迎回来!</h1> ) : ( <h1>请先登录。</h1> )} </div> ); }
-
逻辑与
&&
: 当条件为true
时才渲染后面的内容(常用于显示/隐藏)。jsxfunction Notification({ unreadCount }) { return ( <div> <h1>通知中心</h1> {unreadCount > 0 && ( <span className="badge">{unreadCount}</span> )} </div> ); } // 如果 unreadCount 是 0,<span> 就不会渲染
-
📋 七、列表渲染 ------ 批量生产"零件"
-
大白话: 当你有一组数据(比如数组),需要根据这组数据生成一组相同的组件结构(比如列表项)时用的。就像工厂流水线,根据图纸(数据)生产出一堆一模一样的零件(列表项)。
-
核心方法:用数组的
.map()
方法jsxfunction NumberList({ numbers }) { return ( <ul> {numbers.map((number) => ( <li key={number.toString()}>{number}</li> ))} </ul> ); } // 使用 const numbers = [1, 2, 3, 4, 5]; <NumberList numbers={numbers} />
-
超级重要的
key
属性!- 为什么必须? React 需要一个唯一且稳定 的标识(
key
)来区分列表中的每一项。当列表数据变化(增删改排序)时,React 靠key
高效地找出哪些项是新的、哪些项被移动了、哪些项被删除了,从而只更新必要的部分(虚拟 DOM Diff 算法的基础)。 - 怎么选
key
?- 最佳选择: 数据项唯一且稳定 的 ID(比如数据库 id)。
key={item.id}
。 - 次优选择: 如果没有 id,可以用索引
index
:key={index}
。但!只适用于列表顺序永远不会改变**(纯展示,不会增删排序)的情况!** 因为索引会随着列表变化而改变,可能导致渲染问题或性能下降。
- 最佳选择: 数据项唯一且稳定 的 ID(比如数据库 id)。
key
的坑:key
是给 React 内部用的,不要 在组件内部通过props.key
访问它!key
必须在兄弟节点之间是唯一的 (同一个<ul>
里的所有<li>
的key
不能重复)。- 不要 用随机数
Math.random()
或时间戳Date.now()
做key
!它们每次渲染都会变,失去意义,反而会害了 React!
- 为什么必须? React 需要一个唯一且稳定 的标识(
🏁 总结:组件基础 = 乐高积木玩法手册
- 组件是零件: 函数组件(主流)或类组件。
- JSX 是画皮: 在 JS 里写类 HTML,记得
{}
包 JS,className
代替class
,标签要闭合,根元素唯一。 props
是快递: 父传子,子只读不可改,让组件可配置复用。state
是小金库: 组件自己的数据,改了就刷新界面(函数组件用useState
,类组件用this.state
+this.setState
)。- 事件是遥控器: 处理用户操作(驼峰命名,类组件注意
this
绑定)。 - 条件渲染会变脸:
if
/? :
/&&
根据情况显示不同内容。 - 列表渲染靠
.map()
: 批量生成,必须加唯一稳定的key
(优先用 id,慎用 index)。
记住这个核心:React 组件就是接收 props
(输入),管理 state
(内部状态),返回 JSX(输出)的小机器! 把这几个基础概念和用法吃透,你就掌握了 React 80% 的日常开发!剩下的就是不断练习组合这些"零件"啦!💪
- 使用简洁的大白话讲解React 事件机制,React的事件和普通的HTML事件有什么不同,React的事件和普通的HTML事件有什么不同,React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代,对React-Fiber的理解,它解决了什么问题,React.Component 和 React.PureComponent 的区别,Component, Element, Instance 之间有什么区别和联系,React.createClass和extends Component的区别有哪些 好的!咱们用最简洁的大白话,把这些 React 核心概念串起来讲清楚,像剥洋葱一样一层层看明白。
🎯 1. React 事件机制 vs 普通 HTML 事件
-
普通 HTML 事件:
- 大白话: 直接在 DOM 元素上绑事件(比如
onclick="handleClick()"
)。浏览器自己负责监听和触发。每个元素都可能有独立的事件处理器,如果元素很多(比如列表里 1000 个li
都绑click
),就会占用很多内存,性能可能变差。 - 特点: 原生、直接、分散。
- 大白话: 直接在 DOM 元素上绑事件(比如
-
React 事件:
- 大白话: React 搞了个"事件代理 "(也叫"合成事件 ")。它不在每个元素上绑事件 ,而是在最外层的容器(比如
div#root
)上绑一个总事件监听器 。当你在 JSX 里写onClick={...}
时,React 并不是真的给那个元素绑事件,而是记录 下"这个元素被点了,要执行这个函数"。当事件真的发生时(比如你点了个按钮),事件会冒泡到最外层容器,被那个总监听器抓住。然后 React 根据事件发生的目标元素,找到你之前记录的对应函数去执行。 - 关键不同点:
- 事件委托: 只在顶层绑一个监听器,性能更好(尤其列表多时)。
- 合成事件(SyntheticEvent): React 把原生浏览器事件包装了一下,做了一个跨浏览器兼容 的"假事件对象"(
SyntheticEvent
)。你拿到的e
不是原生的,但用法差不多(e.preventDefault()
,e.stopPropagation()
都能照用),并且 React 保证它在所有浏览器里行为一致。 - 事件池(旧版): 以前 React 为了性能,会复用事件对象。事件处理函数执行完,事件对象里的属性会被清空。所以你不能异步访问它(比如
setTimeout
里用e
)。新版 React (v17+) 取消了事件池 ,这个问题没了,e
可以放心异步用。 - 命名: React 用驼峰命名(
onClick
,onChange
),HTML 用小写(onclick
,onchange
)。
- 大白话: React 搞了个"事件代理 "(也叫"合成事件 ")。它不在每个元素上绑事件 ,而是在最外层的容器(比如
🔧 2. 高阶组件 (HOC) vs Render Props vs Hooks (代码复用三兄弟)
-
共同目标: 解决组件逻辑复用的问题(比如多个组件都需要获取数据、监听窗口大小、处理表单等)。
-
高阶组件 (HOC - Higher-Order Component):
- 大白话: 就像给组件"穿衣服 "或"加装备 "。它是一个函数 ,接收一个组件(
WrappedComponent
)作为参数,返回一个增强后的新组件 。新组件包裹了原组件,并注入了额外的功能(props
或state
)。 - 样子:
const EnhancedComponent = withHOC(OriginalComponent);
- 例子:
React-Redux
的connect
就是个 HOC,它给组件注入了state
和dispatch
。 - 缺点: 容易造成"嵌套地狱 "(
withA(withB(withC(Component)))
),调试时组件层级深;props
命名可能冲突;this
指向问题(类组件时代)。
- 大白话: 就像给组件"穿衣服 "或"加装备 "。它是一个函数 ,接收一个组件(
-
Render Props:
- 大白话: 组件不自己渲染内容,而是提供一个"渲染函数"作为
prop
,让调用者(父组件)决定具体渲染什么。这个"渲染函数"会接收组件内部的数据或状态作为参数。 - 样子:
<DataProvider render={(data) => <Child data={data} />} />
或者<DataProvider>{(data) => <Child data={data} />}</DataProvider>
- 例子: React Router v6 的
Outlet
和useOutletContext
有点类似思想;很多动画库、数据获取库用过。 - 优点: 比 HOC 更灵活,避免了嵌套地狱和命名冲突。
- 缺点: 写法稍显"奇怪",可能产生深层嵌套的回调(虽然比 HOC 好点);性能上如果
render
函数每次都是新创建的,可能导致子组件不必要的重渲染(需要React.memo
配合)。
- 大白话: 组件不自己渲染内容,而是提供一个"渲染函数"作为
-
Hooks:
- 大白话: 革命性方案! 让你在不写 class 的情况下 ,也能"钩住 "(Hook) React 的状态(
useState
)、生命周期(useEffect
)、上下文(useContext
)等特性。逻辑复用 通过自定义 Hook 实现(把共享逻辑抽到一个函数里,里面用各种内置 Hook)。 - 样子:
const [data, setData] = useCustomHook();
(自定义 Hook) - 优点:
- 彻底解决嵌套问题: 逻辑是平铺的函数调用,不再有 HOC 的包裹地狱或 Render Props 的回调嵌套。
- 更直观: 逻辑和 UI 更紧密地写在函数组件里,符合直觉。
- 组合更灵活: 自定义 Hook 可以自由组合,像搭积木一样。
- 避免
this
问题: 函数组件没有this
。
- 为什么是迭代? Hooks 是 React 团队吸取 HOC 和 Render Props 的经验教训后,找到的最优解 ,是当前及未来的主流范式。
- 大白话: 革命性方案! 让你在不写 class 的情况下 ,也能"钩住 "(Hook) React 的状态(
⏳ 3. 为什么要不断迭代?(React 的进化史)
- 大白话: 为了解决痛点 、拥抱新标准 、提升性能 和改善开发者体验 !
createClass
->extends Component
: 为了拥抱 ES6class
语法(更标准、更强大),摆脱React
自己造的"类"语法糖。- Mixin -> HOC -> Render Props -> Hooks: 为了更好地复用组件逻辑。Mixin 早期方案,问题多(命名冲突、依赖不透明);HOC 和 Render Props 是进步,但有各自缺点;Hooks 是目前最优雅、最强大的解决方案。
- Stack Reconciler -> Fiber Reconciler: 为了解决性能瓶颈 (主线程阻塞导致的卡顿),实现可中断渲染 和优先级调度(Fiber)。
- 旧版生命周期 -> 新版生命周期 + Hooks: 为了消除隐患 (如
componentWillMount
/componentWillUpdate
在异步渲染下可能不安全),并让逻辑组织更清晰(Hooks 将相关逻辑聚合在一起)。 - 事件池取消: 为了消除陷阱(异步访问事件对象),简化开发。
- Suspense(实验性/逐步稳定): 为了优雅处理异步操作(数据获取、代码分割),让"加载中"状态声明式管理。
核心驱动力: 让 React 更快 (性能)、更强 (能力)、更好用 (开发者体验)、更健壮(稳定性)。
🌿 4. React Fiber 理解 (React 的"心脏手术")
-
大白话: Fiber 是 React 底层协调算法(Reconciliation)的重写 ,是 React 16 的核心引擎升级。你可以把它想象成给 React 做了一次"心脏搭桥手术",让它能处理更复杂的任务而不会"心肌梗塞"(页面卡顿)。
-
解决了什么问题?
- 旧问题(Stack Reconciler): React 更新(比如状态变化导致重新渲染)是一口气做完 的,而且是同步 的。如果更新任务很重(比如渲染一个超长列表),它会霸占主线程 ,直到全部完成。在这期间,浏览器啥也干不了(无法响应用户输入、动画等),页面就卡住了(掉帧)。
- Fiber 的解决方案:
- 可中断渲染: Fiber 把更新任务拆分成很多小单元 (每个 Fiber 节点代表一个工作单元)。React 可以开始一个任务,暂停它,去做更重要的事(比如用户输入),然后再回来继续。就像你写报告,中间可以接个电话再回来写。
- 优先级调度: React 能区分任务的紧急程度 。用户输入、动画这种需要立即响应 的任务,优先级最高;后台数据更新这种不那么急的,优先级低。高优先级任务可以打断低优先级任务。
- 增量更新: 更新可以分批完成,每次只处理一部分,然后把控制权交还给浏览器,让它去处理绘制、响应用户等,避免长时间阻塞。
- 支持并发模式(Concurrent Mode): 这是 Fiber 带来的未来方向 。让 React 能同时准备多套 UI 。比如,新数据来了,React 可以在后台悄悄渲染新界面,等准备好了再无缝切换过去,用户感觉不到"加载中"。或者,在用户快速切换标签时,React 可以放弃未完成的不重要渲染,避免浪费资源。
-
对开发者的影响: 大部分时候你感觉不到 Fiber 的存在(API 基本没变),但你的应用在处理复杂交互和大量数据时,会明显更流畅。理解 Fiber 有助于你写出性能更好的代码(比如避免在渲染路径上做太重的同步计算)。
⚖️ 5. React.Component
vs React.PureComponent
-
大白话: 两者都是类组件 的基类。
PureComponent
是Component
的一个"精打细算版"。 -
核心区别:
shouldComponentUpdate
的实现React.Component
: 默认情况下,只要父组件重渲染,或者组件自己的setState
被调用,它就一定会重渲染 (shouldComponentUpdate
默认返回true
)。它不管props
或state
到底变没变。React.PureComponent
: 它自动实现了shouldComponentUpdate
!在重渲染前,它会浅比较(Shallow Compare) 当前组件的props
和state
与上一次的props
和state
。- 如果
props
和state
(浅层)都没变 :不重渲染 !(shouldComponentUpdate
返回false
) - 如果
props
或state
(浅层)有变 :重渲染 !(shouldComponentUpdate
返回true
)
- 如果
-
浅比较 (Shallow Compare) 是啥?
-
大白话: 只比较第一层 。对于基本类型(数字、字符串、布尔值),直接比值是否相等。对于对象/数组,只比引用地址 是否相同(是不是同一个对象),不会深入比较对象内部属性是否变了 。
js// 浅比较结果 const a = 1; const b = 1; // a === b -> true (基本类型比值) const obj1 = { name: '张三' }; const obj2 = { name: '张三' }; // obj1 === obj2 -> false (对象比引用地址) const obj3 = obj1; // obj3 === obj1 -> true
-
-
PureComponent
的坑(使用注意):- 确保
state
和props
不可变! 如果你直接修改了state
或props
里的对象/数组(比如this.state.user.name = '李四'
),因为引用地址没变,PureComponent
会认为没变化,导致不更新 UI(Bug) !正确做法是创建新对象/新数组 (this.setState({ user: { ...this.state.user, name: '李四' } })
)。 - 避免复杂结构: 如果
props
或state
结构很深,浅比较可能失效(内部变了但引用没变),或者比较本身有性能开销。此时可能需要手动在Component
里实现shouldComponentUpdate
做深比较,或者用React.memo
(函数组件)配合自定义比较函数。
- 确保
-
总结:
PureComponent
是一个性能优化工具 ,适用于props
和state
结构简单、更新不频繁的场景。务必配合不可变数据使用! 函数组件对应的是React.memo
。
📐 6. Component, Element, Instance 的区别与联系
-
大白话: 想象盖房子:
- Element (元素): 设计图纸 。它是一个普通的 JavaScript 对象 ,描述了你想要在屏幕上看到什么 (比如:一个按钮,文字是"点击我",类型是
button
)。它不是 真实的 DOM 节点,只是轻量的描述 。React.createElement()
返回的就是 Element,JSX 最终也会被转成 Element。 - Component (组件): 建造房子的工厂 或建筑师 。它是一个函数或类 ,定义了如何根据输入(
props
)生成设计图纸(Element) 。组件本身不直接出现在屏幕上,它负责创建和管理 Element。 - Instance (实例): 盖好的具体房子 。对于类组件 ,当 React 根据 Component 的"图纸"去"施工"(渲染)时,会创建一个该类的实例 。这个实例持有组件的
state
、生命周期方法等,是组件在运行时的具体体现 。函数组件没有实例(每次渲染都是一次函数调用)。
- Element (元素): 设计图纸 。它是一个普通的 JavaScript 对象 ,描述了你想要在屏幕上看到什么 (比如:一个按钮,文字是"点击我",类型是
-
联系与流程:
- 你写代码定义 Component(函数或类)。
- React 调用你的 Component(函数组件直接调用,类组件创建实例并调用
render
方法)。 - Component 执行后,返回一个或多个 Element(描述 UI 的对象)。
- React 拿到这些 Element,通过 Diff 算法 比较新旧 Element 树,找出需要更新的最小操作。
- React 将这些操作应用到真实的 DOM 上(渲染)。
- 对于类组件 ,在步骤 2 中创建的 Instance 会在组件的整个生命周期中存在,管理
state
和响应生命周期方法。函数组件没有这个持久化的 Instance。
-
简单记忆:
- Element = 图纸 (描述 UI 的 JS 对象)
- Component = 工厂/建筑师 (创建图纸的函数/类)
- Instance = 盖好的房子 (类组件运行时的具体对象,函数组件无)
🏗️ 7. React.createClass
vs extends Component
-
大白话: 这是 React 创建类组件的两种旧方式 vs 新方式 。现在
extends Component
是标准,createClass
已被废弃(v15.5 后移到单独包,v16+ 彻底不用)。 -
核心区别:
特性 | React.createClass (旧) |
extends Component (新) |
---|---|---|
语法基础 | React 自己实现的类语法糖 | 标准 ES6 Class 语法 |
this 绑定 |
自动绑定 !方法里的 this 自动指向组件实例。 |
需要手动绑定 !常用方法: 1. 构造函数里 bind 2. 箭头函数(类属性) 3. JSX 里箭头函数(不推荐) |
状态初始化 | getInitialState() 方法 |
构造函数里 this.state = { ... } |
props 默认值 |
getDefaultProps() 方法 |
类静态属性 static defaultProps = { ... } |
propTypes |
对象属性 propTypes: { ... } |
类静态属性 static propTypes = { ... } |
Mixins | 支持 !mixins: [SomeMixin] (曾是复用逻辑方式) |
不支持!被 HOC / Render Props / Hooks 取代 |
兼容性 | 废弃,需要额外引入包 | 标准,现代 React 唯一方式 |
-
为什么淘汰
createClass
?- 拥抱标准: ES6 Class 是 JavaScript 的官方标准,更通用,学习成本更低(学 JS 就会)。
this
绑定清晰: 虽然createClass
自动绑定方便,但隐藏了this
机制。ES6 Class 要求手动绑定,让开发者更清楚this
的指向问题(虽然麻烦点,但更符合 JS 原生行为)。- Mixins 的弊端: Mixins 是早期复用逻辑的方式,但容易造成命名冲突、依赖不透明、难以维护。React 团队明确转向组合模式(HOC, Render Props, Hooks)。
- 简化 API: 去除 React 自创的 API(
getInitialState
,getDefaultProps
),统一到 ES6 Class 的标准写法上。
-
总结:
extends Component
是现代、标准、推荐 的类组件写法。createClass
是历史遗迹,了解其存在和区别即可,新项目绝对不要用。
🎯 总复习口诀
- 事件: React 搞代理,顶层抓事件,合成跨浏览器。
- 复用: HOC 穿衣服,Render Props 给画笔,Hooks 是万能瑞士军刀(迭代最优解)。
- 迭代: 为解痛点、跟标准、提性能、优体验。
- Fiber: 可中断、分优先、增量更、并发未来(解决卡顿)。
- PureComponent: 浅比较 props/state,省渲染,但需 immutable 配合。
- 三兄弟: Element 是图纸,Component 是工厂,Instance 是类组件盖好的房(函数组件无房)。
- createClass vs Component: 旧糖 vs 新标,自动绑 vs 手绑,Mixin 死了,Component 活着!
把这些概念的核心区别和联系记牢,React 的底层逻辑和设计思想就清晰了一大半!加油!💪
- 使用简洁的大白话讲解React 高阶组件是什么,和普通组件有什么区别,适用什么场景,对componentWillReceiveProps 的理解,哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?React如何判断什么时候重新渲染组件?React声明组件有哪几种方法,有什么不同?对有状态组件和无状态组件的理解及使用场景,对React中Fragment的理解,它的使用场景是什么?React如何获取组件对应的DOM元素?React中可以在render访问refs吗?为什么?对React的插槽(Portals)的理解,如何使用,有哪些使用场景?在React中如何避免不必要的render?
好的!咱们用最简洁的大白话,把这些问题一次性盘明白,像吃火锅一样,一个个涮清楚!
🧩 1. React 高阶组件 (HOC) 是什么?和普通组件区别?适用场景?
-
HOC 是什么?
- 大白话: HOC 是一个函数 ,接收一个组件 作为参数,返回一个增强后的新组件 。就像给组件"穿功能外套 "或"加装插件"。
- 样子:
const EnhancedComponent = withHOC(OriginalComponent);
-
和普通组件的区别?
- 普通组件: 直接渲染 UI,接收
props
,可能管理state
。是"成品"。 - HOC: 不是组件本身 ,是制造组件的函数 。它不渲染 UI,它的产物 (返回的新组件)才渲染 UI。是"组件加工厂"。
- 普通组件: 直接渲染 UI,接收
-
适用场景?
- 代码复用: 把多个组件需要的相同逻辑抽出来(如:权限校验、数据获取、日志记录、路由守卫)。
- 属性劫持/注入: 给被包裹组件添加/修改
props
(如:React-Redux
的connect
注入state
和dispatch
)。 - 渲染劫持: 控制被包裹组件的渲染过程(如:条件渲染、包裹额外 UI)。
- 注意: 现在更推荐用 Hooks 解决逻辑复用,HOC 容易造成"嵌套地狱"。
🔄 2. 对 componentWillReceiveProps
的理解?
- 大白话: 这是类组件 的一个旧生命周期方法 。当组件接收到新的
props
时(注意:首次渲染时不触发 ),在渲染之前 调用。让你有机会根据新的props
去更新组件的state
。 - 关键点:
- 触发时机: 父组件重传
props
导致子组件更新时(不是自己setState
触发)。 - 作用: 比较
nextProps
和this.props
,如果需要,用this.setState()
更新内部状态。 - ⚠️ 已废弃! 因为在 React 16.3+ 的异步渲染机制(Fiber) 下,它可能被多次调用或打断,导致状态更新不可靠或性能问题。
- 触发时机: 父组件重传
- 替代方案:
static getDerivedStateFromProps(nextProps, prevState)
: 静态方法 ,在每次渲染前 (包括首次)都调用。根据props
计算并返回新的state
对象(或null
表示不更新)。纯函数,无副作用。componentDidUpdate(prevProps, prevState)
: 在渲染后 调用。可以在这里执行副作用(如网络请求),并根据prevProps
和this.props
的差异更新state
(但要注意避免无限循环)。
🚀 3. 哪些方法会触发 React 重新渲染?重新渲染 render
会做些什么?
-
触发重新渲染的方法:
this.setState()
(类组件): 最常用! 改变组件内部状态。this.forceUpdate()
(类组件): 强制重渲染 !跳过shouldComponentUpdate
检查 ,直接调用render
。慎用! 通常意味着设计有问题。- 父组件重渲染: 父组件
render
了,会重新渲染所有子组件 (除非子组件做了优化,如PureComponent
/React.memo
)。 useState
的setter
函数 (函数组件): 改变状态,触发函数组件重新执行。useReducer
的dispatch
(函数组件): 触发状态更新,导致重渲染。- Context 的
Provider
value
变化: 会导致所有消费该 Context 的组件重渲染(除非用React.memo
优化)。
-
重新渲染
render
会做些什么?- 大白话:
render
方法(函数组件就是整个函数体)被调用,生成新的 React 元素(虚拟 DOM 树)。 - 具体步骤:
- 调用
render()
: 执行组件的render
方法(类组件)或整个函数组件(函数组件)。 - 生成新 Element 树:
render
返回一个描述 UI 结构的 React Element 对象树(虚拟 DOM)。 - Diff 算法比较: React 拿着新 Element 树 和上一次渲染的 Element 树 进行对比(Diffing)。
- 计算最小更新: 找出两棵树之间最少的 DOM 操作(哪些节点需要添加、删除、更新属性)。
- 提交更新 (Commit): 将计算出的 DOM 操作应用到真实浏览器 DOM 上,完成界面更新。
- 调用
- 大白话:
🤔 4. React 如何判断什么时候重新渲染组件?
- 核心原则:单向数据流 + 状态驱动。
- 判断依据:
- 组件自身的
state
改变了: 通过setState
(类) 或useState
/useReducer
(函数) 触发。 - 组件接收的
props
改变了: 父组件重渲染导致传下来的props
变了(浅比较)。 - 父组件重渲染了: 即使子组件
props
没变,父组件render
了,默认也会导致子组件重渲染(除非子组件做了优化)。 - 强制更新: 调用了
forceUpdate()
(类组件)。
- 组件自身的
- 优化点(避免不必要的渲染):
- 类组件: 继承
PureComponent
(自动浅比较props
/state
)或手动实现shouldComponentUpdate
(返回false
阻止渲染)。 - 函数组件: 用
React.memo
包裹(自动浅比较props
)或结合useMemo
/useCallback
优化传递给子组件的props
。
- 类组件: 继承
🏗️ 5. React 声明组件有哪几种方法?有什么不同?
方法 | 类型 | 语法 | 状态管理 | 生命周期 | this |
适用场景 | 备注 |
---|---|---|---|---|---|---|---|
函数组件 | 函数 | function MyComp() {...} |
Hooks (useState ) |
Hooks (useEffect ) |
无 | 主流! 简单 UI、逻辑复用 | 现代、简洁、推荐 |
类组件 | 类 | class MyComp extends Component |
this.state |
生命周期方法 | 有,需绑定 | 复杂状态/逻辑、旧项目 | 标准、强大 |
React.createClass |
函数(糖) | React.createClass({...}) |
getInitialState |
生命周期方法 | 自动绑定 | 已废弃! 历史项目 | 旧 API,避免使用 |
- 核心不同:
- 函数组件: 更轻量、无
this
、逻辑通过 Hooks 组织。当前及未来主流。 - 类组件: 更传统、有
this
、生命周期方法清晰。适合复杂场景和旧代码。 createClass
: 过时产物 ,有自动绑定this
等特性,但已被 ES6 Class 彻底取代。
- 函数组件: 更轻量、无
🧊 6. 有状态组件 vs 无状态组件?理解及场景?
-
有状态组件 (Stateful Component):
- 大白话: 组件自己管理数据 (
state
)。它有"记忆 ",知道当前的状态(比如按钮点了几次、输入框内容是什么),并且能根据用户交互或时间变化更新自己的状态。 - 特点: 通常使用类组件或带
useState
/useReducer
的函数组件。 - 使用场景:
- 需要处理用户交互(表单、按钮点击)。
- 需要根据数据变化更新 UI(计数器、定时器、动态列表)。
- 需要管理复杂的内部逻辑和状态。
- 大白话: 组件自己管理数据 (
-
无状态组件 (Stateless Component / Presentational Component):
- 大白话: 组件不管理数据 (
state
)。它像个"傻显示器 ",只负责展示 从外面(props
)传进来的数据。它没有"记忆",给什么数据就显示什么,自己不会变。 - 特点: 通常是纯函数组件(或只读
props
的类组件)。只依赖props
进行渲染。 - 使用场景:
- 纯展示 UI(按钮、图标、卡片、列表项)。
- 容器组件(Container)和展示组件(Presentational)分离模式中的展示层。
- 提高可复用性和可测试性(输入输出明确)。
- 大白话: 组件不管理数据 (
-
趋势: 函数组件 + Hooks 让"无状态组件"也能轻松拥有状态(
useState
),界限变得模糊。但思想依然重要:尽量让组件专注于展示(无状态),复杂逻辑和状态提升到上层(有状态组件或自定义 Hook)。
🧩 7. 对 React 中 Fragment 的理解?使用场景?
- 大白话: Fragment 是一个空的包裹标签
<></>
或<React.Fragment></React.Fragment>
。它允许你返回多个元素,但不会在最终 DOM 中添加任何实际的节点。 - 为什么需要? React 组件的
render
方法必须返回单个根元素 。如果不想多包一个无意义的<div>
(可能破坏样式结构,如表格、列表),就用 Fragment。 - 使用场景:
-
避免额外 DOM 节点:
jsx// 错误!需要根元素 // return <td>Hello</td><td>World</td>; // 正确!用 Fragment 包裹,DOM 里只有两个 <td> return ( <> <td>Hello</td> <td>World</td> </> );
-
表格、列表等严格结构: 在
<table>
,<ul>
,<dl>
内部直接放<div>
是非法的,Fragment 是完美解决方案。 -
CSS 样式影响: 避免多余的
<div>
破坏 Flex/Grid 布局或选择器。
-
<></>
vs<React.Fragment>
:<></>
:简洁 ,但不能接受key
属性。<React.Fragment>
:稍长 ,但可以接受key
属性(在渲染列表时有用)。
📍 8. React 如何获取组件对应的 DOM 元素?
-
方法:使用
ref
- 创建
ref
:- 函数组件:
const myRef = useRef(null);
- 类组件:
this.myRef = React.createRef();
(在构造函数) 或myRef = React.createRef();
(类属性)
- 函数组件:
- 绑定到元素:
<div ref={myRef}>...</div>
- 访问 DOM:
- 函数组件:
myRef.current
(指向 DOM 元素) - 类组件:
this.myRef.current
(指向 DOM 元素)
- 函数组件:
- 创建
-
例子:
jsx// 函数组件 import { useRef, useEffect } from 'react'; function MyComponent() { const inputRef = useRef(null); useEffect(() => { // 组件挂载后,inputRef.current 指向 <input> DOM 元素 inputRef.current.focus(); }, []); return <input ref={inputRef} type="text" />; }
🚫 9. React 中可以在 render
访问 refs
吗?为什么?
-
答案:不可以!
-
为什么?
- 大白话:
render
阶段是 React 在**"画图纸"(生成虚拟 DOM),此时 真实的 DOM 元素还没创建出来呢!refs
是用来指向 真实 DOM** 的,你访问它只能得到null
。就像房子还在施工图阶段,你不可能找到房间的门把手。 - 生命周期角度:
render
: 纯计算阶段,不能有副作用(访问 DOM、发网络请求等)。componentDidMount
(类) /useEffect
(函数): 提交阶段后 ,此时 DOM 已创建并挂载 ,refs.current
才会指向真实的 DOM 元素。访问refs
的正确时机!
- 大白话:
-
错误示例:
jsxclass BadComponent extends React.Component { inputRef = React.createRef(); render() { // ❌ 错误!render 里访问 ref.current 是 null this.inputRef.current.focus(); // 报错! return <input ref={this.inputRef} />; } }
🌀 10. 对 React 的插槽 (Portals) 的理解?如何使用?场景?
-
大白话: Portal 是一个"传送门 "!它允许你把一个组件渲染到父组件 DOM 树之外的任意位置 (比如
document.body
),但该组件在 React 组件树中的位置和事件冒泡关系保持不变。 -
如何使用?
-
创建 Portal 容器: 在
public/index.html
里加一个<div id="portal-root"></div>
。 -
使用
ReactDOM.createPortal
:jsximport { ReactDOM } from 'react-dom'; function Modal({ children }) { // 将 children 渲染到 portal-root 这个 DOM 节点 return ReactDOM.createPortal( children, // 要渲染的 React 元素 document.getElementById('portal-root') // 目标 DOM 节点 ); } // 使用 function App() { return ( <div> <h1>普通内容</h1> <Modal> <div className="modal"> <h2>我是 Modal!</h2> <p>虽然我渲染在 body 下,但事件冒泡会回到 App 组件</p> </div> </Modal> </div> ); }
-
-
关键特性: Portal 里的元素,虽然物理上在
#portal-root
里,但在 React 的虚拟 DOM 树中,它仍然是App
组件的子组件。事件(如点击)会沿着 React 组件树冒泡,而不是 DOM 树。 -
使用场景:
- 模态框 (Modal) / 对话框 (Dialog): 需要覆盖在所有内容之上,不受父容器
overflow: hidden
或z-index
影响。渲染到body
下最合适。 - 提示框 (Tooltip) / 弹出菜单 (Popups): 需要定位在特定元素附近,但可能被父容器的
overflow
裁剪。Portal 可以让它"逃逸"出来。 - 全局通知 (Notifications): 固定在屏幕角落,不受页面滚动影响。
- 第三方库集成: 需要将 React 组件渲染到非 React 管理的 DOM 区域。
- 模态框 (Modal) / 对话框 (Dialog): 需要覆盖在所有内容之上,不受父容器
🛡️ 11. 在 React 中如何避免不必要的 render?
- 核心思路: 让 React 知道"即使父组件重渲染了,我的
props
和state
其实没变,不用重新渲染我"。 - 具体方法:
-
函数组件:
React.memo
- 作用: 包裹函数组件,浅比较
props
。如果props
没变(浅层),跳过渲染。 - 用法:
const MemoizedComponent = React.memo(MyComponent);
- 进阶: 可提供自定义比较函数 (
arePropsEqual
) 做深比较或特定逻辑。
- 作用: 包裹函数组件,浅比较
-
类组件:
PureComponent
或shouldComponentUpdate
PureComponent
: 继承它代替Component
。自动浅比较this.props
和this.state
,没变就不渲染。shouldComponentUpdate(nextProps, nextState)
: 手动实现。返回true
(渲染)或false
(不渲染)。可自定义比较逻辑(如深比较特定字段)。
-
优化传递给子组件的
props
(函数组件关键!)- 问题: 父组件每次渲染,如果直接传新函数
{() => doSomething()}
或新对象{ data: { ... } }
,即使内容一样,引用地址也变了,导致子组件(即使React.memo
了)认为props
变了而重渲染。 - 解决方案:
-
useCallback
: 缓存函数 。依赖项不变时,返回同一个函数引用。jsxconst handleClick = useCallback(() => { // 处理点击 }, [dep1, dep2]); // 依赖项 return <Child onClick={handleClick} />;
-
useMemo
: 缓存计算结果 (包括对象)。依赖项不变时,返回同一个对象引用。jsxconst memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); return <Child data={memoizedValue} />;
-
- 问题: 父组件每次渲染,如果直接传新函数
-
状态提升与拆分:
- 将频繁变化的状态下移到更具体的组件,避免导致不相关的上层组件重渲染。
- 将大型组件拆分成更小的组件,让变化的影响范围局部化。
-
Context 优化:
- 避免在
Provider
的value
中传递频繁变化的对象/函数,会导致所有消费组件重渲染。 - 将 Context 拆分成多个,按需消费。
- 消费组件用
React.memo
包裹(但要注意value
变化仍会触发)。
- 避免在
-
列表渲染
key
: 给列表项稳定且唯一 的key
,帮助 React 高效 Diff,避免不必要的重渲染。
-
🎯 总结口诀
- HOC: 组件加工厂,穿外套加功能,现在 Hooks 更香。
- 旧生命周期:
componentWillReceiveProps
已凉,getDerivedStateFromProps
+componentDidUpdate
上。 - 渲染触发:
setState
、props
变、父渲染、forceUpdate
(慎用)。 - 渲染过程:
render
画新图,Diff 找差异,Commit 改 DOM。 - 组件声明: 函数组件(主流)、类组件(传统)、
createClass
(古董)。 - 有/无状态: 有状态管数据(记忆),无状态纯展示(傻显)。
- Fragment: 空包裹
<></>
,不生 DOM 节点,解决多元素和表格问题。 - 取 DOM:
useRef
/createRef
绑元素,useEffect
/componentDidMount
里访问。 render
访问ref
: 不行!图纸阶段 DOM 还没生。- Portals: 传送门!渲染到别处,事件还认家(React 树)。Modal/Tooltip 救星。
- 防重渲染:
React.memo
、PureComponent
、shouldComponentUpdate
、useCallback
/useMemo
优化props
、拆组件、稳定key
。
把这些核心点吃透,React 的性能优化和组件设计就能玩得很溜了!💪
- 使用简洁的大白话讲解对 React-Intl 的理解,它的工作原理?对 React context 的理解,为什么React并不推荐优先考虑使用Context?React中什么是受控组件和非控组件?React中refs的作用是什么?有哪些应用场景?React组件的构造函数有什么作用?它是必须的吗?React.forwardRef是什么?它有什么作用?类组件与函数组件有什么异同?
好的!咱们用最简洁的大白话,把这些 React 核心概念串起来讲清楚,像吃串串一样,一串一串搞定!
🌍 1. 对 React-Intl 的理解?工作原理?
- 大白话: React-Intl 是 React 的**"翻译官"。它帮你轻松实现应用的 国际化(i18n),让你的 App 能根据用户设置自动切换语言**(比如中文、英文、日文)。
- 工作原理:
-
准备"翻译字典": 你把所有需要翻译的文字(按钮文字、提示语等)按不同语言写成 JSON 文件(如
zh-CN.json
,en-US.json
)。json// zh-CN.json { "welcome": "欢迎", "button": "点击" } // en-US.json { "welcome": "Welcome", "button": "Click" }
-
配置"翻译官": 在 App 顶层用
<IntlProvider>
包裹,告诉它当前用什么语言(locale
)和对应的翻译字典(messages
)。jsximport { IntlProvider } from 'react-intl'; import zhMessages from './zh-CN.json'; function App() { return ( <IntlProvider locale="zh-CN" messages={zhMessages}> <MyComponent /> </IntlProvider> ); }
-
组件"点菜": 在组件里,不用写死文字,而是用 React-Intl 提供的组件或 Hook 去"翻译字典"里取文字。
- 用组件:
<FormattedMessage id="welcome" />
-> 显示"欢迎" - 用 Hook:
const formatMessage = useIntl(); formatMessage({ id: 'button' })
-> 得到"点击"
- 用组件:
-
自动切换: 改变
<IntlProvider>
的locale
和messages
,整个 App 的文字就自动换成新语言了!
-
📦 2. 对 React Context 的理解?为什么不推荐优先使用?
- 大白话: Context 是 React 提供的**"全家福"**。它允许你把一些数据(比如用户信息、主题色)直接"广播"给所有后代组件 ,不用一层一层用
props
传(避免"props drilling")。 - 怎么用?
- 创建"全家福相册":
const UserContext = createContext();
- 太爷爷发照片(Provider): 在顶层用
<UserContext.Provider value={userData}>
包裹后代。 - 重孙子看照片(Consumer): 在深层组件用
const user = useContext(UserContext);
直接拿到数据。
- 创建"全家福相册":
- 为什么不推荐优先使用?
- 性能陷阱(主要问题!): Context 的
Provider
的value
一变 ,所有消费这个 Context 的组件 都会强制重渲染 !即使它们只用了value
的一小部分。如果value
是个复杂对象且频繁变化(比如{ user, settings, theme }
),会导致大量无关组件重渲染,性能爆炸。 - 过度设计: 对于简单的父子/兄弟通信,用
props
或状态提升更直接、更清晰。Context 是"核武器",小问题用大炮容易伤及无辜。 - 组件耦合: 使用 Context 的组件和 Context 本身强耦合,降低了组件的复用性和可测试性(脱离 Context 环境就跑不了)。
- 调试困难: 数据来源不直观,不如
props
追踪方便。
- 性能陷阱(主要问题!): Context 的
- 什么时候用? 全局性、不常变 的数据(如用户登录状态、主题、语言)。避免用于频繁变化的局部状态。
🎛️ 3. 受控组件 vs 非受控组件?
- 大白话: 区别在于表单数据由谁管。
- 受控组件 (Controlled Component):
- 特点: 表单数据(如输入框内容)完全由 React 的
state
控制。 - 流程:
- 给表单元素(如
<input>
)设置value={this.state.value}
。 - 监听
onChange
事件。 - 在事件处理函数里,用
setState
更新state
。 state
更新 -> 组件重渲染 -> 输入框显示新value
。
- 给表单元素(如
- 比喻: 像"遥控器 "。输入框的值是"电视画面",
state
是"遥控器"。你按遥控器(setState
)才能换台(改变值)。 - 优点: 数据实时同步到
state
,方便校验、处理、联动。 - 缺点: 每次输入都触发
setState
和重渲染,可能影响性能(大量输入框时)。
- 特点: 表单数据(如输入框内容)完全由 React 的
- 非受控组件 (Uncontrolled Component):
- 特点: 表单数据由浏览器 DOM 自己管理 。React 只在需要时(如提交时)通过
ref
去读取 DOM 的值。 - 流程:
- 给表单元素设置
defaultValue
(初始值)。 - 用
ref
引用 DOM 元素。 - 需要值时(如提交按钮点击),通过
this.inputRef.current.value
读取。
- 给表单元素设置
- 比喻: 像"自由人"。输入框自己管自己的值,React 不管。提交时去"问"它现在值是多少。
- 优点: 代码简单,输入不触发重渲染(性能好),适合简单表单或文件上传。
- 缺点: 实时校验、联动困难,数据不与
state
同步。
- 特点: 表单数据由浏览器 DOM 自己管理 。React 只在需要时(如提交时)通过
- 怎么选? 优先用受控组件 (数据流清晰,可控性强)。非受控组件用于简单场景 (如搜索框、文件上传)或性能敏感的大量输入。
📍 4. React 中 refs
的作用?应用场景?
- 大白话:
ref
是 React 给你的"遥控器 ",让你能直接操作真实的 DOM 元素 或访问类组件实例。 - 作用:
- 操作 DOM: 聚焦输入框、选中文字、播放媒体、测量尺寸位置等。
- 访问类组件实例: 调用类组件的方法(如
childRef.current.handleReset()
)。 - 存储可变值: 类似一个"不触发渲染的
state
"(用useRef
存定时器 ID、上一次的值等)。
- 应用场景:
- 聚焦输入框:
inputRef.current.focus()
- 触发动画: 通过
ref
获取 DOM 元素,调用动画库 API。 - 集成第三方库: 很多非 React 库(如 D3.js, jQuery 插件)需要直接操作 DOM。
- 访问类组件方法: 父组件通过
ref
调用子组件(类组件)的方法。 - 存储值:
const timerRef = useRef(); timerRef.current = setInterval(...);
- 聚焦输入框:
- ⚠️ 注意: 不要过度使用! 优先用
state
和props
驱动 UI。ref
是"逃生舱",用于 React 声明式模型之外的必要操作。
⚙️ 5. React 组件的构造函数有什么作用?它是必须的吗?
- 作用(类组件):
- 初始化
state
:this.state = { count: 0 };
(唯一正确地点 !不要在render
或其他地方直接改this.state
)。 - 绑定事件处理函数的
this
:this.handleClick = this.handleClick.bind(this);
(避免this
指向问题)。 - 初始化
ref
:this.myRef = React.createRef();
(虽然类属性语法更常用)。 - 调用
super(props)
: 必须第一行! 让子组件继承父组件的props
。不写this.props
会是undefined
。
- 初始化
- 它是必须的吗?
- 类组件: 不一定! 只有当你需要做上面提到的初始化工作 (尤其是
state
和this
绑定)时才需要写。- 如果组件没有
state
,且不需要绑定this
(比如用箭头函数定义方法),可以省略构造函数。 - 如果需要初始化
state
或绑定this
,必须写。
- 如果组件没有
- 函数组件: 没有构造函数! 用
useState
Hook 初始化状态。
- 类组件: 不一定! 只有当你需要做上面提到的初始化工作 (尤其是
🔗 6. React.forwardRef
是什么?作用?
-
大白话:
forwardRef
是一个"穿针引线 "的工具。它允许你把父组件传给子组件的ref
,再"转发"给子组件内部的某个元素或更深层的组件。 -
为什么需要? 默认情况下,
ref
不能像props
那样透传。父组件传ref
给子组件<Child ref={parentRef} />
,子组件直接拿不到这个ref
(它被 React 保留用于指向子组件实例/DOM)。forwardRef
解决了这个问题。 -
怎么用?
jsx// 子组件:用 forwardRef 包裹,ref 作为第二个参数接收 const FancyButton = React.forwardRef((props, ref) => { // 把 ref 绑定到内部的 <button> 元素上 return <button ref={ref} className="FancyButton"> {props.children} </button>; }); // 父组件:像平常一样使用 ref const ref = useRef(); <FancyButton ref={ref}>Click me!</FancyButton>; // 现在 ref.current 指向 FancyButton 内部的 <button> DOM 元素!
-
作用:
- 访问子组件内部 DOM: 父组件想直接操作子组件包裹的某个 DOM 元素(如上面的按钮)。
- 透传
ref
到更深层组件: 在高阶组件 (HOC) 或一些库组件中,需要把ref
传递给被包裹的真实组件。 - 保持组件封装性: 子组件暴露一个
ref
给父组件,但内部具体结构可以自由变化,父组件只关心最终指向的元素。
-
常见场景: 可复用的 UI 组件库(按钮、输入框)、动画库(需要操作内部 DOM)、高阶组件。
🔄 7. 类组件 vs 函数组件:异同?
特性 | 类组件 (Class Component) | 函数组件 (Function Component) |
---|---|---|
定义方式 | class MyComp extends React.Component |
function MyComp() 或 const MyComp = () => {} |
状态管理 | this.state / this.setState() |
Hooks : useState , useReducer |
生命周期 | 有明确生命周期方法 (componentDidMount 等) |
Hooks : useEffect (模拟生命周期) |
this 指向 |
有 this ,需手动绑定事件处理函数 |
无 this ,没有绑定问题 |
实例 | 每次渲染创建实例,实例在生命周期中存在 | 无实例 ,每次渲染是一次函数调用 |
ref 访问 |
this.ref 指向组件实例或 DOM |
useRef 创建,.current 指向 DOM 或存储值 |
性能 | 实例化开销略大,优化靠 shouldComponentUpdate |
轻量,优化靠 React.memo / useMemo / useCallback |
逻辑复用 | Mixins (废弃) / HOC / Render Props | 自定义 Hooks (主流,更优雅) |
代码风格 | 面向对象 (OOP) 风格 | 函数式编程 (FP) 风格 |
学习曲线 | 需理解 this 、生命周期 |
需理解 Hooks 规则(闭包、依赖项) |
当前地位 | 传统方式,兼容旧代码 | 主流 & 未来,React 官方推荐 |
- 核心相同点:
- 都是 React 组件,接收
props
,返回 JSX (React Element)。 - 都能管理状态和副作用(只是方式不同)。
- 都能复用逻辑(HOC/Render Props vs Hooks)。
- 都是 React 组件,接收
- 核心不同点:
- 心智模型: 类组件是"有状态的机器 "(面向对象),函数组件是"无状态的函数 + Hooks"(函数式)。
this
: 类组件的"灵魂"也是"坑",函数组件彻底摆脱this
。- Hooks 革命: Hooks 让函数组件拥有了类组件的所有能力(状态、生命周期、上下文),并且逻辑复用更强大(自定义 Hooks)。这是函数组件成为主流的关键。
- 简洁性: 函数组件通常更简洁,没有
this
绑定和生命周期方法的样板代码。
🎯 总结口诀
- React-Intl: 翻译官,字典配,Provider 发,组件取。
- Context: 全家福,免传参,但慎用!性能坑,耦合高。
- 受控/非控: 受控靠
state
(遥控器),非控靠 DOM (自由人)。优先受控。 refs
: 遥控器,操作 DOM/实例。存值不渲染。少用!- 构造函数: 类组件初始化
state
/绑this
。非必须,按需写。 forwardRef
: 穿针线,转发ref
到子组件内部。封装组件神器。- 类 vs 函数: 类有
this
和生命周期,函数靠 Hooks。函数是未来,Hooks 是王道!
把这些核心差异和使用场景记牢,React 的组件设计和状态管理就能玩得转了!💪
好的!咱们用最简洁的大白话,把 React 组件通信的"全家桶"按场景分类讲透,像剥洋葱一样一层层解决你的疑惑!
🏠 一、父子组件通信(最基础、最常用)
🧒 1. 父传子:爸爸给儿子塞零花钱
-
方式:
props
(属性传递) -
大白话: 爸爸(父组件)直接把数据(零花钱)塞到儿子(子组件)口袋里(写在子组件标签的属性上)。儿子拿到钱(
props
)只能花(显示),不能自己印(改)。 -
代码:
jsx// 爸爸 (父组件) function Dad() { const money = 100; return <Son money={money} />; // 塞钱! } // 儿子 (子组件) function Son(props) { return <p>我有 {props.money} 块!</p>; // 花钱! }
👦 2. 子传父:儿子找爸爸要钱
-
方式: 回调函数(Callback Function)
-
大白话: 儿子(子组件)不能直接改爸爸的钱包(
state
)。儿子想买玩具(需要父组件做事),就打电话 (调用父组件传过来的回调函数 )告诉爸爸:"爸,我想买玩具!"。爸爸接到电话(执行回调函数),然后自己 去取钱(更新自己的state
),再把新零花钱(新数据)重新塞给儿子(重新渲染)。 -
代码:
jsx// 爸爸 (父组件) function Dad() { const [totalMoney, setTotalMoney] = 1000); // 1. 爸爸定义"给钱规则"(回调函数) const handleGiveMoney = (amount) => { setTotalMoney(totalMoney - amount); // 爸爸掏钱 }; // 2. 把"规则"传给儿子 return <Son onAskForMoney={handleGiveMoney} />; } // 儿子 (子组件) function Son(props) { const toyPrice = 50; return ( <button onClick={() => props.onAskForMoney(toyPrice)}> 爸,我要买玩具! </button> ); }
🏢 二、跨级组件通信(爷爷 -> 重孙子)
📬 方式 1:Context(轻量级"全家福")
-
适用场景: 数据需要跨越多层组件传递(如主题色、用户信息),中间层组件用不到这些数据。
-
大白话: 太爷爷(顶层组件)想给所有重孙子(深层组件)发红包(共享数据)。他不用一个个爸爸、爷爷地往下传,而是直接在家族群里(创建 Context) 发个公告:"所有重孙子都有红包!"。重孙子们只要加入这个群(使用 Context),就能直接看到公告(拿到数据),不用经过中间的爸爸、爷爷。
-
代码:
jsximport { createContext, useContext } from 'react'; // 1. 创建家族群 (Context) const FamilyContext = createContext(); // 太爷爷 (顶层组件) function GreatGrandpa() { const familyName = "张家"; return ( // 2. 太爷爷在群里发公告 (Provider) <FamilyContext.Provider value={familyName}> <Grandpa /> </FamilyContext.Provider> ); } // 中间层组件 (爷爷/爸爸) - 完全不用知道 Context! function Grandpa() { return <Father />; } function Father() { return <Grandson />; } // 重孙子 (深层组件) function Grandson() { // 3. 重孙子加入群聊,直接看公告 (useContext) const name = useContext(FamilyContext); return <p>我们家姓 {name}</p>; }
-
⚠️ 注意: Context 的
value
一变 ,所有消费它的组件都会强制重渲染 !适合不常变的全局数据。
🏭 方式 2:状态提升 + Props 逐层传递(传统方式)
- 适用场景: 数据需要共享,但层级不算特别深,或者 Context 不适用时。
- 大白话: 把共享数据(
state
)和修改它的函数提升到最近的共同祖先组件 。然后这个祖先组件像"中转站 ",一层一层通过props
把数据和方法传给需要它们的后代组件。 - 代码: 类似"兄弟组件通信"中的状态提升,只是层级更深。中间层组件需要"透传"它们用不到的
props
(有点麻烦)。
🌐 三、非嵌套关系组件通信(远房亲戚)
🏗️ 方式 1:状态提升到共同祖先(最推荐)
- 适用场景: 两个组件在组件树中有共同的祖先(即使很远)。
- 大白话: 就像兄弟组件通信的"放大版"。找到这两个"远房亲戚"最近的共同祖先 ,把共享状态和修改逻辑提升 到这个祖先组件里。祖先组件再通过
props
把数据和方法分别传给这两个组件。 - 优点: 符合 React 单向数据流原则,清晰可控。
📡 方式 2:全局状态管理(Redux / Zustand / MobX)
- 适用场景: 应用复杂,多个不相关的组件 需要共享和频繁修改大量状态(如购物车、用户信息)。状态提升到共同祖先变得困难或低效。
- 大白话: 请个专业的"管家 "(状态管理库)。管家住在独立的大房子里(Store) ,管着全家所有的钱、账本(全局状态)。任何组件(家庭成员)想:
- 看账本(读状态): 直接问管家(通过 Hooks 如
useSelector
)。 - 买东西改账本(改状态): 填申请单(
dispatch Action
)交给管家。管家按规矩(Reducer)审批后修改账本。账本一改,管家通知所有关心这块账本的组件更新。
- 看账本(读状态): 直接问管家(通过 Hooks 如
- 代表库:
- Redux: 最成熟,概念多(Store, Action, Reducer, Middleware),学习曲线陡峭。
- Zustand: 轻量级,API 简洁,基于 Hooks,中小项目首选。
- MobX: 响应式,更接近面向对象思维,自动追踪依赖。
- 优点: 集中管理状态,解决复杂共享问题,调试工具强大(Redux DevTools)。
- 缺点: 增加复杂度和包体积,小项目可能杀鸡用牛刀。
⚡ 方式 3:事件总线(Event Bus / 发布订阅 - 了解即可)
-
适用场景: 极少数特殊场景,如两个完全独立的组件需要临时通信,且不想引入全局状态管理。React 中不推荐!
-
大白话: 像村里的广播站 。组件 A 往广播站发消息(
emit
),组件 B 订阅(on
)这个消息。消息发出后,组件 B 就能收到并执行操作。 -
简单实现:
jsx// 创建一个简单的"事件总线" const eventBus = { listeners: {}, on(event, callback) { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(callback); }, emit(event, data) { if (this.listeners[event]) { this.listeners[event].forEach(callback => callback(data)); } } }; // 组件 A:发消息 function ComponentA() { const handleClick = () => eventBus.emit('messageFromA', '你好!'); return <button onClick={handleClick}>发送消息</button>; } // 组件 B:收消息 function ComponentB() { const [message, setMessage] = useState(''); useEffect(() => { eventBus.on('messageFromA', (data) => setMessage(data)); }, []); return <p>收到消息: {message}</p>; }
-
⚠️ 为什么不推荐?
- 破坏 React 单向数据流,数据流向不清晰,难以调试。
- 容易导致内存泄漏(忘记取消订阅)。
- 组件间隐式耦合,降低可维护性。
- 在 React 生态中,Context 或状态管理库是更符合其设计哲学的方案。
🕳️ 四、如何解决 Props 层级过深的问题?(Props Drilling)
- 大白话: "Props Drilling" 就是为了把数据传给深层组件,中间层被迫接收并传递它们自己根本用不到的
props
。就像送快递,为了把包裹送到 10 楼,2-9 楼都得签收再转交,很麻烦。
✅ 解决方案(按推荐顺序)
-
🥇 首选:React Context
- 适用场景: 数据是全局性、不常变的(主题、用户信息、语言)。
- 优点: React 内置,轻量级,API 简单(尤其
useContext
Hook)。 - 代码: 见上面"跨级通信 - Context"部分。中间层组件完全不用管这些
props
。
-
🥈 次选:状态管理库(Redux / Zustand)
-
适用场景: 应用复杂、状态共享频繁、数据变化多(购物车、复杂表单、实时数据)。
-
优点: 强大、集中管理、调试工具好、社区成熟。
-
代码(以 Zustand 为例):
jsximport { create } from 'zustand'; // 1. 创建 Store (大房子) const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })); // 组件 A (任何地方):读状态 function ComponentA() { const count = useStore((state) => state.count); return <p>Count: {count}</p>; } // 组件 B (任何地方):改状态 function ComponentB() { const increment = useStore((state) => state.increment); return <button onClick={increment}>+1</button>; }
-
优点: 彻底解决 Props Drilling,组件直接访问 Store,无需层层传递。
-
-
🥉 再次:组件拆分与状态提升
- 适用场景: 仔细分析状态的真实归属。有时状态提升的位置可以更合理,或者组件可以拆分得更小,减少传递层级。
- 优点: 符合 React 设计哲学,不引入额外依赖。
- 缺点: 对于深层嵌套且确实需要共享的状态,效果有限。
-
🤔 考虑:组合模式(Composition)
-
适用场景: 利用 JSX 的灵活性,将子组件作为
props.children
或特定prop
传递。 -
大白话: 父组件不直接传数据,而是把"渲染权 "交给子组件。父组件提供"插槽",子组件自己决定怎么渲染。
-
代码(类似 Render Props 思想):
jsx// 父组件:提供"数据"和"渲染位置" function DataProvider({ children }) { const data = { user: "张三" }; // children 是个函数,接收 data 并返回 JSX return <div>{children(data)}</div>; } // 使用 <DataProvider> {(data) => <p>用户: {data.user}</p>} {/* 子组件决定怎么渲染 */} </DataProvider>
-
优点: 避免显式传递
props
,更灵活。 -
缺点: 写法稍显复杂,可能增加嵌套。
-
📊 五、组件通信方式总结(决策树)
子传父: 回调函数] A -->|跨级/多层| C{数据全局/不常变?} C -->|是| D[React Context] C -->|否/复杂| E[状态管理库 Redux/Zustand] A -->|非嵌套/无共同祖先| F{应用复杂/状态多?} F -->|是| E F -->|否/简单| G[状态提升到共同祖先
或 事件总线(不推荐)] B --> H{Props 层级过深?} H -->|是| C H -->|否| I[保持 props 传递]
🎯 核心口诀
- 父子通信:
props
传数据,回调函数传操作(单向数据流!)。 - 跨级通信: Context 是轻量首选(全家福),状态管理库 是复杂利器(大管家)。
- 非嵌套通信: 状态提升 找共同祖先,状态管理库 解决复杂全局状态,事件总线(了解,少用)。
- 解决 Props Drilling: Context 或 状态管理库 是核武器!组件拆分 和状态提升是基本功。
掌握这些通信方式,你就能在 React 的组件世界里游刃有余地"传话办事"啦!💪