重读 React 教程
React 是 Meta(原 Facebook)开源、用于构建用户界面的 JavaScript 库。
React 起源于 Facebook 内部项目,因对市面上已有的 MVC 框架不满意,团队自研用于 Instagram 业务,2013 年 5 月正式对外开源。
React 重点解决大规模数据集渲染难题。依靠虚拟 DOM + Diff 算法,数据变更时只更新 DOM 差异部分,获得优秀渲染性能。同时顺应前端组件化趋势,允许开发者将独立、简短的代码片段封装为组件,通过组合组件构建复杂交互式 UI。
备注:React 不属于完整 MVVM 框架;多数观点认为它并不是完整 MVC,仅仅承担 MVC 模式中的 View(视图层),是一套以状态驱动视图、单向数据流的视图库。
React 核心特点:
- 声明式设计:声明期望的 UI 状态,代码可读性高,便于调试;
- 高性能:通过虚拟 DOM,最大限度降低直接操作真实 DOM 的频次;
- 灵活性:可以和其他库、框架混合使用,渐进接入现有项目;
- JSX 语法:JSX 是 JS 语法扩展,非强制,但官方推荐使用;
- 组件化:组件可复用,适合大型项目协作开发;
- 单向响应数据流:数据流向清晰,减少重复代码,相比双向绑定逻辑更简单。
React 基本原理
React 作为 UI 库,核心目标就是状态变更后的页面更新,在兼顾更新能力的同时保障用户体验。
前端开发中,频繁、复杂的 DOM 操作是主要性能瓶颈,也是衡量前端工程师能力的一个重要方向。
为此 React 引入虚拟 DOM(Virtual DOM)机制:在 JS 内存中实现一套 DOM 抽象 API。
在 React 开发中,所有 DOM 变更先作用在虚拟 DOM;每当组件数据发生变化,React 会完整构建新的虚拟 DOM 树,将新树和上一次缓存的虚拟 DOM 树做对比,找出差异补丁,只把差异部分应用到浏览器真实 DOM。
虽然每次状态变化都要重建完整虚拟 DOM 树,但虚拟 DOM 只是普通 JS 对象,内存运算速度极快;只有 Diff 得到的差异才会去操作真实 DOM,以此提升整体性能。
React 还对 DOM 刷新做批处理优化:同一个事件循环内多次数据变更会被合并。
举例:连续把内容从 A 改成 B,又改回 A,经过批处理合并,最终不会产生任何真实 DOM 修改。
开发者不需要关心某条数据变化要修改哪几个 DOM 节点,只需要描述任意数据状态下页面应该长什么样,由 React 完成后续 DOM 更新工作。
一句话总结:React 将浏览器 DOM 抽象为 JavaScript 对象(虚拟 DOM),业务代码操作虚拟 DOM,Diff 算法对比新旧虚拟 DOM,最后把差异翻译成真实 DOM 操作更新页面。
React 的几个重要概念
React 的核心设计思想:封装组件。各个组件维护自身状态与 UI;状态发生变化,自动重渲染组件。开发者不需要手动查找、操作 DOM 元素。
虚拟 DOM
虚拟 DOM 是一种编程概念:UI 以理想化的虚拟形式保存在内存,再通过 ReactDOM 这类库同步到真实 DOM,同步过程叫做协调(Reconciliation)。
React 虚拟 DOM 本质:JS 库基于浏览器 API 实现的一套 JS 对象,是真实 DOM 的内存抽象,兼顾性能、跨浏览器兼容。
浏览器 HTML 文档解析之后生成 DOM 树,JS 通过浏览器提供 DOM API 操作页面 DOM;但是频繁操作 DOM 开销很大。
React 的出发点就是简化 DOM 更新工作:开发者不再直接调用 DOM API,而是操作虚拟 DOM 对象;React 内部高效完成虚拟 DOM 到真实 DOM 的映射。
引入虚拟 DOM 两大价值:
- 性能优化:JS 对象运算远快于 DOM API;搭配 Diff 算法减少真实 DOM 操作次数;
- 跨平台一致性:同一套虚拟 DOM 描述,可以运行在 Web、SSR、React Native 移动端,抹平平台差异。
组件
React 整个应用由组件构建。组件就是封装完成独立功能的 UI 部件。
组件化思维:把 UI 中每一块功能独立模块定义成组件,小组件嵌套、组合形成大组件,最终构成完整页面。
概念类比:组件就像 JS 函数,接收入参 props,返回描述页面 UI 的 React 元素。
组件三大特征:
- 可组合:组件可以互相嵌套、互相调用,复杂 UI 拆分为若干简单组件;
- 可复用:一套组件可以在多处业务场景重复使用;
- 可维护:每个组件只关心自身业务逻辑,逻辑隔离,更容易阅读理解与修改。
Diff 算法(协调算法)
Diff 全称 Difference,作用是计算两棵虚拟 DOM 树之间的差异,本质就是两个 JS 对象的对比。Diff 是 React 内部开销最大的模块,也是性能优化的核心关注点。
当调用 render() 生成一棵 React 元素树;state、props 更新后,再次调用 render 得到一棵全新的树。Diff 算法对比两棵树,计算出最小变更,更新真实 DOM,保证页面和最新虚拟 DOM 保持一致。
理论上两棵树完整对比最优算法复杂度是 O(n³),n 代表节点数量,1000 个节点就要十亿次运算,完全无法用于线上。
复杂度简单理解:完整比对所有节点 O(n²),删除、插入节点又要额外 O(n),合计 O(n³)。
React 基于两条实践假设,实现启发式 Diff,把复杂度降低到 O(n):
- 不同类型元素(标签、组件),会生成完全不同的子树;
- 同级子节点使用
key属性标记,标识渲染前后节点身份。
绝大多数业务场景,这两条假设都成立。
Diff 算法执行逻辑:
- 元素类型不同:标签名、组件类型不一样,直接卸载旧节点,把以该节点为根的整棵子树全部销毁重建。
- 元素类型相同:DOM 标签一样,保留真实 DOM 节点,只更新有变化的属性(className、title、style 等),递归处理子节点。style 会细粒度对比,只更新变化的样式属性。
- 组件类型相同:props 更新,组件实例保留,调用组件生命周期,执行 render,再递归 Diff 处理 render 返回的子树。
- 子节点列表递归对比:默认遍历新旧两组子节点,逐个对比生成变更。
- 如果只是列表末尾新增节点,Diff 开销很小;
- 如果在列表头部插入新节点,不加 key 时会顺序比对,导致大量节点销毁重建,性能很差。
key 的作用:给同级列表子元素设置 key,Diff 根据 key 识别节点身份,尽可能移动、复用已有节点,避免不必要销毁重建。
key 规范:列表内必须唯一、稳定可预测。不推荐数组下标 index 做 key:插入、删除数组元素会打乱下标,导致 Diff 逻辑错乱;不推荐随机数做 key:每次渲染 key 变化,节点全部重建,完全失去复用能力。
单向数据流
单向数据流:数据只能从父组件流向子组件,不允许反向流动。
单向数据绑定理念:可变数据拥有唯一数据源。父组件把自身 state 作为 props 向下传递给子组件;每个组件管理自己内部 state;组件 state 只能影响它组件树向下的后代组件。
当多个组件需要使用同一份变化数据,需要做状态提升:把共享状态提升到它们最近的共同父组件。
状态变更流程:子组件触发回调通知父组件更新 state;父组件 state 变化,自动向下更新所有子组件 props,触发子组件重渲染。
单向数据流相比双向绑定需要写更多代码,但优势是 bug 排查简单清晰:state 只能被拥有它的组件修改,数据变更来源可控。
JSX
JSX 是 JavaScript 语法扩展,是 React 提出的书写形式,长得像 HTML,但拥有 JavaScript 全部能力。
JSX 最终编译为 React.createElement() 调用,产出普通 JS 对象,也就是 React 元素。
JSX 让 UI 描述和交互逻辑写在一起,可以直观看到页面最终形态。
function formatName(user) {
return user.firstName + ' ' + user.lastName
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
}
const element = <h1> Hello, {formatName(user)}! </h1>
ReactDOM.render(element, document.getElementById('root'))
JSX 关键特性:
- 使用
{}嵌入 JS 表达式; - JSX 本身是表达式,可以赋值、作为函数返回值;
- 属性支持双引号字符串,或者
{}传入 JS 值; - 无子元素必须使用自闭合标签;
- 自带防 XSS 注入攻击;
- Babel 编译工具将 JSX 转为 JS 对象。
备注:React 不强制使用 JSX;JSX 只是 createElement 的语法糖,完全可以手写原生 JS。
组件状态:props 和 state
React 把 UI 看作状态机:不同状态渲染不同 UI,保证界面和数据保持同步。
React 提供两套模型管理组件数据:props、state。
props
组件接收的属性、子元素会合并成一个对象,就是 props。props 由父组件传递给子组件。
重要原则:props 只读。组件不能修改收到的 props。所有 React 组件要像纯函数一样保护 props 不被篡改。
props 可以传递任意类型:基础类型、React 元素、函数。
子组件可以把收到的 props 继续向下传递给后代组件。
state
state 是组件私有状态,只属于当前组件,其他组件无法直接访问。state 保存组件内部会发生变化的数据,是一个普通 JS 对象。
组件可以把自己的 state 通过 props 向下传递给子组件:
<MyComponent data={this.state.data} />
<MyComponent {...this.state} />
也就是“自上而下单向数据流”:state 产生的数据只会流向子树下面的组件。
state 正确使用规则:
- class 组件构造函数是唯一直接赋值 state 的地方;
- 禁止直接修改 state,使用
setState()提交状态更新; - state 更新可能异步:React 会合并多次 setState 做性能优化;
- setState 是浅合并:传入对象会和原有 state 做浅层合并。
逆向通信:子组件向父组件传值
React 数据自上而下单向流动,子组件不能直接修改父组件 state。
实现逆向通信思路:父组件通过 props 向下传递回调函数,子组件调用这个回调,通知父组件修改自身 state。
function ChildComponent(props) {
return (
<>
<p>Hello, {props.name}!</p>
<button onClick={() => props.updateStateName('Lizh')}>点击修改</button>
</>
)
}
function ParentComponent() {
const [state, setState] = React.useState({ name: 'World'})
const updateStateName = value => {
setState({ name: value })
}
return <ChildComponent name={state.name} updateStateName={value => updateStateName(value)} />
}
props 和 state 的区别
props 和 state 都是普通 JS 对象,都可以控制组件渲染输出。
- props:来自父组件传入,类似函数入参;组件不能修改 props;
- state:组件内部私有,类似函数内部局部变量;组件可以调用
setState修改 state。
类组件
类组件使用 ES6 class 语法定义,继承 React.Component,必须实现 render() 返回 React 元素。
class MyComponent extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>
}
}
state
构造函数中可以初始化 state,也可以使用类字段简写;
备注:不推荐在构造函数用 props 赋值 state,后续 props 更新不会同步 state,容易产生数据不同步 bug。
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
name: props.name
}
}
// 简写方式
// state = { date: new Date() }
}
setState
setState() 将 state 变更放入更新队列,通知 React 重渲染组件以及子组件。
setState(updater, [callback])
第一个参数支持对象,也支持更新函数;更新函数可以拿到上一轮 state、props,保证取值最新;返回对象会做浅合并。
第二个参数是可选回调,在 state 更新、渲染完成之后执行;官方更推荐使用 componentDidUpdate 生命周期代替回调。
// 对象形式
this.setState({ name: 'lizh' })
// updater 函数形式
this.setState((state, props) => ({
name: state.name + '01'
}), () => {
console.log('渲染完成回调')
})
forceUpdate
正常情况下,state、props 改变组件才会重渲染。如果 render 依赖 state、props 之外的数据,可以调用 forceUpdate() 强制重渲染。
备注:forceUpdate 会跳过当前组件 shouldComponentUpdate,但是子组件生命周期不受影响,依旧正常执行。
类组件生命周期
setState、forceUpdate 是开发者主动调用更新;生命周期是组件不同阶段自动执行的回调函数。
核心生命周期:
render():class 组件唯一必须实现,返回 React 元素、数组、字符串、null 等。constructor():挂载前执行;初始化 state、绑定事件处理函数。componentDidMount():组件已经插入 DOM 树后执行,适合发请求、操作 DOM、设置定时器。componentWillUnmount():组件卸载销毁前执行,清除定时器、取消订阅。shouldComponentUpdate(nextProps, nextState):state、props 变更,渲染前执行;返回 false 可以跳过本次渲染;首次渲染、forceUpdate 不会触发该函数。注意:返回 false 不会阻止子组件因为自身 state 变化而重渲染。componentDidUpdate(prevProps, prevState, snapshot):组件更新完成 DOM 变更后执行,首次挂载不触发;getSnapshotBeforeUpdate返回值作为第三个参数 snapshot。shouldComponentUpdate 返回 false 不会执行。static getDerivedStateFromProps(props, state):静态方法,挂载、每次更新渲染前都执行;返回对象更新 state,返回 null 不修改;不能访问组件 this 实例。getSnapshotBeforeUpdate():DOM 提交更新前执行,可以获取更新前 DOM 信息,返回值传给componentDidUpdate第三个参数。
挂载执行顺序:constructor() → static getDerivedStateFromProps() → render() → componentDidMount()
更新执行顺序:static getDerivedStateFromProps() → shouldComponentUpdate() → render() → getSnapshotBeforeUpdate() → componentDidUpdate()
函数组件
函数组件就是普通 JS 函数,接收 props 对象,返回 React 元素。
function MyComponent(props) {
return <h1>Hello, {props.name}</h1>
}
备注:React 16.8 之前,函数组件只能做 UI 渲染,没有内部 state,没有生命周期,也被叫做无状态组件。Hook 出现之后函数组件也可以拥有状态、副作用能力。
Hook
Hook 是 React 16.8 新增特性,可以在不写 class 的前提下,在函数组件中使用 state、生命周期等 React 能力。
Hook 本质是一类特殊函数,同时支持开发者编写自定义 Hook 复用状态逻辑。
Hook 两条硬性规则:
- 只在函数最顶层调用 Hook,禁止循环、条件、嵌套函数内部调用 Hook;
- 只能在 React 函数组件、自定义 Hook 内部调用 Hook;普通 JS 函数不允许。
注意:Hook 不能在 class 组件内使用。
useState
给函数组件增加内部状态。
// 基础用法
const [state, setState] = useState(initialState)
// 惰性初始state,函数仅执行一次
const [state, setState] = useState(() => {
const init = null
return init
})
initialState 只在组件第一次渲染生效。返回数组:当前状态、状态更新函数。
setState 支持直接传值,或者传更新函数(拿到上一轮 state);一个组件可以多次调用 useState,维护多组状态。
注意:useState 更新是替换 state,不是浅合并,和 class 组件 setState 行为不一样。
如果新 state 和旧 state 使用 Object.is 判定完全相等,React 会跳过本次渲染(可跳过部分子组件渲染,但组件函数体仍然会执行)。
setState(newState)
setState(prevState => {
return prevState
})
useEffect
用于处理副作用:数据请求、订阅、DOM 操作、定时器等。
等价 class 中 componentDidMount + componentDidUpdate + componentWillUnmount 三者能力的集合。
// 不写依赖数组:每次渲染都执行
useEffect(() => {
// 副作用逻辑
})
第二个参数为依赖数组,数组内变量发生变化才执行 effect:
useEffect(() => {
const sub = props.source.subscribe();
}, [props.source])
备注:空依赖数组 []:仅挂载执行一次,卸载执行清理函数,模拟挂载 + 卸载生命周期。
effect 可以返回清理函数,组件卸载、effect 重新执行前会执行清理,对应 componentWillUnmount。
useEffect(() => {
const timer = setInterval(()=>{},1000)
return ()=>{
clearInterval(timer)
}
},[])
备注:useEffect 不会阻塞浏览器绘制,页面响应更快。同一个组件可以写多个 useEffect,按顺序执行。
useReducer
useState 的替代方案,适合 state 逻辑复杂、存在多个子状态、下一个 state 依赖上一个 state 的场景。返回 state 与 dispatch;dispatch 引用稳定,传递给子组件不会引发额外重渲染。
const initialState = {count:0}
function reducer(state, action){
switch(action.type){
case 'increment': return { count:state.count + 1 }
case 'decrement': return { count: state.count - 1 }
default: throw new Error()
}
}
function Counter(){
const [state,dispatch] = useReducer(reducer, initialState)
return (
<>
Count:{state.count}
<button onClick={()=>dispatch({type:'decrement'})}>-</button>
<button onClick={()=>dispatch({type:'increment'})}>+</button>
</>
)
}
useCallback
缓存回调函数。
依赖数组不变,返回同一个函数引用;配合 React.memo 子组件,避免因函数引用变化触发不必要重渲染。
const memoizedCallback = useCallback(()=>{
console.log(name)
},[name])
useMemo
缓存计算结果。
依赖不变,直接复用上次计算值,避免昂贵计算每次渲染重复执行。
const memoizedValue = useMemo(()=>computeExpensiveValue(a,b),[a,b])
备注:useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
useLayoutEffect
API 签名与 useEffect 一致;DOM 更新完成之后、浏览器绘制之前同步执行副作用,适合读取 DOM 布局、同步修改 DOM,会阻塞页面渲染。
自定义 Hook
把组件复用逻辑抽取成函数;函数名必须以 use 开头,内部可以调用其他 Hook。
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
}
}, [friendID])
return isOnline
}
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
return isOnline ? 'Online' : 'Offline';
}
备注:自定义 Hook 和普通 JS 函数区别:可以调用其他 Hook;React 内部机制跟踪 Hook 调用,关联对应组件实例。
类组件 vs 函数组件
心智模型关键差异:函数组件捕获渲染时的值;类组件读取 this,总是拿到组件实例最新的值。
- 编程范式:类组件面向对象;函数组件偏向函数式编程;
- 复用方式:类组件支持继承;React 倡导组合优于继承,函数组件依靠 Hook 做逻辑复用,不使用继承;
- 语法形式:类组件继承
React.Component,实现 render;函数组件是普通 JS 函数接收 props 返回元素; - 状态与生命周期:类组件依靠 this.state、生命周期钩子;函数组件原生无状态,16.8+ 通过 Hook 获得 state、副作用能力;
- 发展趋势:React 官方主推 Hook 函数组件,class 组件仍然完全兼容,不会废弃。
示例:同样设置 3 秒定时器打印 props.propA;挂载传入 propA="word",挂载后父组件把 propA 改成 "lizhao"。
// 函数组件,捕获渲染快照,输出 word
function MyComponents (props) {
useEffect(() => {
setTimeout(() => {
alert(props.propA)
}, 3000)
}, [])
}
// class组件,this指向实例,读取最新,输出 lizhao
class MyComponents extends React.Component {
componentDidMount () {
setTimeout(() => {
alert(this.props.propA)
}, 3000)
}
}
两种模式都可以模拟对方行为:class 可以保存快照;函数组件借助 useRef 拿到最新值,只是默认行为不同,根据业务场景选择。
为什么引入 Hook?
- 组件状态逻辑复用困难:高阶组件、render-props 会造成组件嵌套地狱;Hook 可以抽取状态逻辑,不改动组件结构;
- 复杂组件难以维护:相互无关业务逻辑散落在同一个生命周期;Hook 按照业务关注点拆分代码;
- class 的 this 指向容易迷惑,学习成本高;
- class 不利于代码压缩、热更新工具稳定性差。
备注:Hook 目标覆盖 class 全部业务场景,但 React 不会移除 class 组件支持。
性能对比
Hook 消除 class 实例、构造函数绑定 this 的额外开销;同时减少高阶组件、render-props 带来的组件嵌套层级。
但性能差距不是巨大鸿沟;真正性能瓶颈取决于业务逻辑,而不是组件写法。
优缺点总结
类组件缺点:this 容易混淆;业务逻辑打散在各个生命周期;缺少统一逻辑拆分标准;
函数组件(Hook)优点:函数式输入输出更纯粹;闭包捕获渲染快照,值确定;逻辑按业务拆分;适配并发、时间切片。
事件处理
React 合成事件机制成因:Fiber 节点生成时,对应的真实 DOM 未必已经挂载;事件回调不能直接绑定 DOM。
React 实现一套合成事件(SyntheticEvent):顶层注册、事件收集、统一派发。基于 W3C 标准封装,抹平浏览器差异。
JSX 事件命名使用小驼峰;传入回调函数而不是字符串;阻止默认行为必须调用 e.preventDefault(),不能 return false。
可以通过 e.nativeEvent 访问浏览器原生事件对象。
const handleClick = e => console.log(e.nativeEvent)
const Button = <button onClick={handleClick}>按钮</button>
备注:onClick 等事件不是直接绑定 DOM,而是事件委托挂载在容器根节点;事件触发后,React 内部向上收集组件树的事件回调,批量执行。
捕获事件:事件名后面加上 Capture,例如 onClickCapture。
受控组件与非受控组件
核心区分:表单数据由谁掌管。
受控组件:表单输入的值完全由 React 组件 state 管理;用户输入触发 onChange,更新 state,state 再映射到表单 value。
class NameForm extends React.Component {
constructor(props) {
super(props)
this.state = { value: '' }
}
handleChange(event) {
this.setState({value: event.target.value})
}
render() {
return (
<form>
<input type="text" value={this.state.value} onChange={e => this.handleChange(e)} />
</form>
)
}
}
非受控组件:表单数据保存在真实 DOM 中,React 不去接管;使用 ref 读取 DOM 元素拿到表单值。
class NameForm extends React.Component {
constructor(props) {
super(props)
this.inputRef = React.createRef()
}
handleSubmit(event) {
alert('A name was submitted: ' + this.inputRef.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<input type="text" ref={this.inputRef} />
</form>
)
}
}
Context
Context 用于跨多层组件传递全局数据,避免一层层手动传递 props。
class 组件使用 createContext:
const ThemeContext = React.createContext({color:'red'})
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value={{color:'red'}}>
<Toolbar />
</ThemeContext.Provider>
)
}
}
function Toolbar() {
return <ThemedButton />
}
class ThemedButton extends React.Component {
static contextType = ThemeContext
render() {
return <div style={{color: this.context.color}}>Context</div>
}
}
另外两种接收 context:static contextType 静态属性;<ThemeContext.Consumer> render 回调。
注意:Context 会降低组件可复用性,谨慎使用;优先考虑组件组合代替 context。
函数组件 useContext:接收 context 对象,返回最近 Provider 提供的 value;Provider value 发生变化,组件重渲染。
function ThemedButton () {
const theme = useContext(ThemeContext)
return <div style={{color: theme.color}}>Context</div>
}
Refs
Refs 用来绕开 state、props 数据流,直接访问 DOM 元素或者 class 组件实例;适用场景:管理焦点、文本选择、媒体控制、调用第三方 DOM 库。
备注:优先考虑 state 驱动 UI,Refs 作为逃逸出口,不要过度使用。
class 组件 createRef
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
}
render() {
return <div ref={this.myRef} />
}
}
ref 绑定普通 HTML 标签时 current 是 DOM;绑定 class 组件时 current 是组件实例。
函数组件 useRef
useRef 返回同一个可变容器对象,.current 可以存任意值;赋值给 DOM ref 属性,current 保存 DOM 节点。.current 修改不会触发组件重渲染。
function MyComponent() {
const inputEl = useRef(null)
const onButtonClick = () => inputEl.current.focus()
return <>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus input</button>
</>
}
回调 Refs
ref 传入回调函数;挂载传入 DOM、实例,卸载传入 null,方便手动管理 DOM 引用。
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.buttonEl = null
}
getButtonRef (inst) {
this.buttonEl = inst
}
render() {
return <button ref={this.getButtonRef.bind(this)}>点击</button>
}
}
forwardRef 转发 ref
ref 和 key 一样,属于特殊属性,不会被 props 透传。React.forwardRef 让 ref 可以转发到子组件内部 DOM。
const MyRefComponent = React.forwardRef((props, ref) => (
<div ref={ref}>转发 ref 示例</div>
))
class App extends React.Component {
ref = React.createRef()
render() {
return (
<>
<MyRefComponent ref={this.ref} />
</>
)
}
}
替代方案:自定义 props 属性传递 ref 对象,例如 myRef={this.ref}。
高阶组件 HOC
HOC 不是 React 内置 API,是一种组件复用设计模式。
高阶组件是函数:入参是组件,返回全新包装后的组件,组件把 props 转为 UI;HOC 把组件包装成新组件。
规范:HOC 不修改原组件,通过容器组件包裹;属于纯函数,无副作用。
// HOC伪代码示例
const AutoCalcSize = WrappedComponent => props => {
const innerRef = useRef()
const [size, setSize] = React.useState({width:0,height:0})
React.useEffect(()=>{
// 计算dom尺寸
},[])
return <WrappedComponent {...props} innerRef={innerRef} calcSize={size}/>
}
使用 HOC 注意事项:
- 不要在 render 内部调用 HOC,每次 render 生成全新组件实例,状态全部丢失;
- 需要拷贝原组件静态方法;
- ref 不会自动透传,需要 forwardRef 处理;
- props 透传,避免覆盖原有 props。
相关问题
Virtual DOM 一定比直接操作原生 DOM 快吗?
Virtual DOM 优势不在“比手动 DOM 操作更快”,而在于性能稳定性和开发可维护性。
当业务 DOM 变更很复杂、变更点不确定时,虚拟 DOM + Diff 保证性能下限;但开发者写的极致优化原生 DOM 代码,在特定场景下会比虚拟 DOM 更快。
虚拟 DOM 解决的核心痛点:不需要开发者人工分析哪些 DOM 要改。