重读 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 元素树;stateprops 更新后,再次调用 render 得到一棵全新的树。Diff 算法对比两棵树,计算出最小变更,更新真实 DOM,保证页面和最新虚拟 DOM 保持一致。

理论上两棵树完整对比最优算法复杂度是 O(n³),n 代表节点数量,1000 个节点就要十亿次运算,完全无法用于线上。

复杂度简单理解:完整比对所有节点 O(n²),删除、插入节点又要额外 O(n),合计 O(n³)

React 基于两条实践假设,实现启发式 Diff,把复杂度降低到 O(n)

  • 不同类型元素(标签、组件),会生成完全不同的子树;
  • 同级子节点使用 key 属性标记,标识渲染前后节点身份。

绝大多数业务场景,这两条假设都成立。

Diff 算法执行逻辑:

  1. 元素类型不同:标签名、组件类型不一样,直接卸载旧节点,把以该节点为根的整棵子树全部销毁重建。
  2. 元素类型相同:DOM 标签一样,保留真实 DOM 节点,只更新有变化的属性(className、title、style 等),递归处理子节点。style 会细粒度对比,只更新变化的样式属性。
  3. 组件类型相同:props 更新,组件实例保留,调用组件生命周期,执行 render,再递归 Diff 处理 render 返回的子树。
  4. 子节点列表递归对比:默认遍历新旧两组子节点,逐个对比生成变更。
    • 如果只是列表末尾新增节点,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 提供两套模型管理组件数据:propsstate

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,但是子组件生命周期不受影响,依旧正常执行。

类组件生命周期

setStateforceUpdate 是开发者主动调用更新;生命周期是组件不同阶段自动执行的回调函数

核心生命周期:

  1. render():class 组件唯一必须实现,返回 React 元素、数组、字符串、null 等。
  2. constructor():挂载前执行;初始化 state、绑定事件处理函数。
  3. componentDidMount():组件已经插入 DOM 树后执行,适合发请求、操作 DOM、设置定时器。
  4. componentWillUnmount():组件卸载销毁前执行,清除定时器、取消订阅。
  5. shouldComponentUpdate(nextProps, nextState):state、props 变更,渲染前执行;返回 false 可以跳过本次渲染;首次渲染、forceUpdate 不会触发该函数。注意:返回 false 不会阻止子组件因为自身 state 变化而重渲染。
  6. componentDidUpdate(prevProps, prevState, snapshot):组件更新完成 DOM 变更后执行,首次挂载不触发;getSnapshotBeforeUpdate 返回值作为第三个参数 snapshot。shouldComponentUpdate 返回 false 不会执行。
  7. static getDerivedStateFromProps(props, state):静态方法,挂载、每次更新渲染前都执行;返回对象更新 state,返回 null 不修改;不能访问组件 this 实例。
  8. 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 要改。

参考资料

React 中文文档

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-24 21:30:19

results matching ""

    No results matching ""