Rect深入学习

React核心机制

虚拟 DOM 和 Diff 算法

什么是虚拟DOM

虚拟DOM可以理解为模拟了DOM树的JS对象树

比如

js 复制代码
var element = {
    element: 'ul',
    props: {
        id:"ulist"
    },
    children: [
    { element: 'li', props: { id:"first" }, children: ['这是第一个List元素'] },
    { element: 'li', props: { id:"second" }, children: ['这是第二个List元素'] }
    ]
}

为什么需要虚拟DOM

传统DOM更新方式的问题:

  • 在原生JS中,更新DOM的方式往往是粗颗粒度的更新,直接替换整颗子树,容易造成性能的浪费
  • 如果要做到细颗粒度更新,则需要自己决定修改哪一部分,但这种手动diff很麻烦

虚拟DOM更新的优势:

  • 框架自动对新旧虚拟DOM树进行diff算法
  • 然后精准更新DOM树中变化的部分,大幅度提升性能

举例:

比如有一个列表,我对其进行了修改

html 复制代码
//旧UI
<ul id="list">
  <li>苹果</li>
  <li>香蕉</li>
</ul>

//新UI
<ul id="list">
  <li>苹果</li>
  <li>橘子</li> <!-- 改动 -->
</ul>
  • 传统DOM更新:

    • 粗暴做法list.innerHTML = render(items) → 把整个 <ul> 清空并重建 <li>,即使"苹果"没变也会被销毁重建。

    • 精细做法 :你必须写逻辑找到第 2 个 <li> 并替换它的文本

      • list.children[1].textContent = '橘子';
      • 但这种手动 diff 很麻烦,开发者必须自己维护 UI 和数据的一致性。
  • 虚拟DOM更新:

    • 框架自动比较新旧虚拟 DOM:

      • 第 1 个 <li> 一样 → 复用。
      • 第 2 个 <li> 文本不同 → 只更新文本。
    • 最终只执行一条 DOM 操作:list.children[1].textContent = '橘子';

diff算法

传统diff算法的时间复杂度是O(n³)

  • 我们要把旧树变成新树,找到最小修改路径

为什么时间复杂度是O(n³)?

  1. 遍历旧树的每个节点(n次)

  2. 遍历新树的每个节点(n次)

    • 对比每个旧树中的节点,找到新树中可能对应的新节点
  3. 比较两个节点的子结构是否完全相同

    • 因为判断"是否同一个节点"不仅要看标签名,还要看它的整个子结构是否相同。 这就需要再深入进去比较它们的子树。

    • 每对匹配节点都可能有一整棵子树;

      每棵子树的节点数也可能接近 n;

      所以在最坏情况下,每一对匹配都要再递归比较一遍整棵子树。

于是复杂度变成:

O(n)(旧树) × O(n)(新树) × O(n)(子树递归) = O(n³)

第 3 层递归比较子树的复杂度,是因为每一对匹配节点 都还要递归地比较它们的子树结构

前两层只是找出"候选节点对",第三层才是深入检查"它们真的一样吗"。

所以整体复杂度是: 旧树节点数 × 新树节点数 × 子树递归 = O(n³)


React的diff算法的时间复杂度是O(n)

React 把问题简化成了三条"经验规则",正是这三条规则让复杂度从 O(n³) → O(n)。

  1. 同层比较,不跨层

    • 复杂度就从 O(n³)O(n²)
  2. 不同类型节点,直接替换整棵子树

    • 也就是说,不同类型的节点永远不去比较子树 。 这避免了对子树的递归匹配,进一步从 O(n²)O(n)
  3. 通过 key 标识子节点的稳定性

    • 对于同一层的子节点列表,React 通过 key 来判断哪些节点是"同一个节点"
hmtl 复制代码
//旧
<ul>
  <li key="A">A</li>
  <li key="B">B</li>
  <li key="C">C</li>
</ul>
//新
<ul>
  <li key="B">B</li>
  <li key="A">A</li>
  <li key="C">C</li>
</ul>

React 会通过 key 识别出:

  • A、B、C 都还在;
  • 只是顺序变了;
  • 所以只需调整位置,不需要删除重建

这就让 同层的节点比较只需一次线性扫描

👉 因此,同层 diff 的复杂度变为 O(n)

key 的作用是什么?

  • 是React对于列表元素的唯一标识

    • 如果key相同,那么认为是同一节点,可以复用DOM元素
    • 如果key不同,则会销毁旧的,创建新的节点

为什么不能用 index 作为 key?

因为会导致错误的复用和性能问题

  • 因为列表内容如果从中间新增或者删除一项,那么index对应的元素将会错误的被复用

React 中 reconciliation 的过程是怎样的?

  • 当组件的 stateprops 变化时,React 会比较新旧虚拟 DOM(Fiber 树) ,找出需要更新的部分并同步到真实 DOM。这个比较与更新过程叫 Reconciliation

React 更新是同步还是异步的?

同步更新

同步模式下,React一旦开始渲染,就会一口气渲染完所有组件,期间不会中断

  • 页面上的表现

    • 当你触发一个大型渲染(比如 setState 导致 1000 个组件更新)时,页面会卡顿一下
    • 浏览器在 React 渲染完成前,无法响应用户操作(比如滚动、点击)
js 复制代码
function App() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    for (let i = 0; i < 10000; i++) {
      setCount(i) // 模拟大规模更新
    }
  }

  return <button onClick={handleClick}>count: {count}</button>
}

在同步更新下,点击按钮后:

  • UI 会"卡死"几百毫秒;
  • 最后一次性更新成最终结果。

异步(Concurrent)更新(React 18 createRoot)

在并发模式下,React会把渲染拆分为小任务,在空闲时间片执行,可以随时暂停、恢复或丢弃

  • 页面上的表现

    • 大型渲染不再卡顿;
    • 页面仍能响应滚动、输入、动画;
    • React 会优先处理用户交互(高优先级),低优先级任务(如列表渲染)可延后执行。
js 复制代码
import { useState, startTransition } from 'react'

function App() {
  const [value, setValue] = useState('')
  const [list, setList] = useState([])

  const handleChange = (e) => {
    const val = e.target.value
    setValue(val)
    startTransition(() => {
      // 模拟高开销任务
      const items = Array.from({ length: 5000 }, (_, i) => `${val}-${i}`)
      setList(items)
    })
  }

  return (
    <>
      <input value={value} onChange={handleChange} placeholder="输入点东西" />
      <ul>{list.map((item) => <li key={item}>{item}</li>)}</ul>
    </>
  )
}

在异步(Concurrent)模式下:

  • 输入框 不会卡顿
  • React 会优先更新输入框的值
  • 再利用空闲时间慢慢渲染列表;
  • 如果你输入更快,React 会丢弃旧的渲染任务,直接开始最新的。

同步场景

React17及以前的全部更新,默认同步
js 复制代码
// React 17 写法(使用 ReactDOM.render)
import ReactDOM from 'react-dom'

function App() {
  const [count, setCount] = React.useState(0)

  console.log('render:', count)

  return (
    <button onClick={() => setCount(count + 1)}>
      Click: {count}
    </button>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

💬 说明:

  • 在 React 17(及更早版本)中,React 没有并发模式(Concurrent Mode)
  • 所有更新(无论大或小)都是同步执行的。
  • 点击按钮时,会立刻执行所有渲染逻辑。

📍页面效果:

即使组件很复杂、渲染耗时,React 也会"卡着"把它一次性渲染完。

React 18 中的旧 Root(非 Concurrent Root)
js 复制代码
// React 18,但仍使用 ReactDOM.render(旧 Root)
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))
  • 即使你使用 React 18,只要还用旧的 ReactDOM.render, React 就不会启用并发模式(仍是同步更新)。
  • 所以这种 root 下的渲染依然会一次性执行完,期间不能被打断。

📍页面效果:

和 React 17 完全一样,仍然是同步阻塞渲染。

在 React 事件回调中调用的更新
js 复制代码
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    console.log('Before:', count)
    setCount(count + 1)
    console.log('After:', count)
  }

  return <button onClick={handleClick}>Click: {count}</button>
}

即使你在 React 18 并发模式下(使用 createRoot), 在 React 事件回调中触发的更新仍是同步批量更新

React 会立即计算新的 Fiber 树,保证交互即时。

异步场景

使用 createRoot()

使用 createRoot()(并发 root)------ 开启异步渲染能力

js 复制代码
// React 18 推荐写法(Concurrent Root)
import ReactDOM from 'react-dom/client'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
  • createRoot() 会启用 Concurrent Mode(并发模式)
  • 在这种 root 下,React 的更新具备可中断、可延迟的能力;
  • 不代表所有更新都异步,但具备异步调度的基础条件

📍页面效果:

如果组件渲染量大,React 可以暂停、分段渲染,不会卡死主线程。 用户输入或动画依然流畅。

使用 startTransition()(标记低优先级更新)
js 复制代码
import { useState, startTransition } from 'react'

function App() {
  const [value, setValue] = useState('')
  const [list, setList] = useState([])

  const handleChange = (e) => {
    const val = e.target.value
    setValue(val)
    // 👇 告诉 React:这是低优先级任务,可延迟执行
    startTransition(() => {
      const items = Array.from({ length: 5000 }, (_, i) => `${val}-${i}`)
      setList(items)
    })
  }

  return (
    <>
      <input value={value} onChange={handleChange} placeholder="输入点东西" />
      <ul>{list.map((item) => <li key={item}>{item}</li>)}</ul>
    </>
  )
}

说明:

  • startTransition() 将内部更新标记为可中断任务
  • 高优先级任务(输入框更新)会先执行
  • 低优先级任务(列表渲染)会在空闲时执行;
  • 若用户继续输入,React 会丢弃旧任务、渲染最新的。

📍页面效果:

输入非常流畅,列表延迟更新但不卡顿。

使用 useDeferredValue()(延迟渲染依赖值)
js 复制代码
import { useState, useDeferredValue } from 'react'

function App() {
  const [text, setText] = useState('')
  const deferredText = useDeferredValue(text) // 延迟使用 text 的值

  const list = Array.from({ length: 5000 }, (_, i) => (
    <li key={i}>{deferredText}</li>
  ))

  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <ul>{list}</ul>
    </>
  )

说明:

  • useDeferredValue() 会在高优先级更新(输入)后,延迟执行耗时渲染
  • 输入流畅;
  • 列表内容更新稍后完成。

📍页面效果:

输入框立即响应,列表延迟一点点更新。 类似于"防抖 + 并发更新"的效果

一句话总结:

在 React 18 中,只要使用 createRoot() 启动应用, 你就进入了并发世界。 再搭配 startTransition() / useDeferredValue(), 就能让 React 的更新更智能、更流畅。

Fiber 是为了解决什么问题?

  • Fiber 是 React 为了解决「同步更新导致的卡顿问题」而引入的「可中断、可恢复的虚拟 DOM 架构」
  • Fiber 是 React 的底层重构,用于让虚拟 DOM 更新"可中断、可恢复、可调度",从而提升大规模渲染的流畅度。

背景问题

React15的缺陷,在React15之前,更新流程是这样的:

  1. 状态更新后,React 会从根节点开始,递归遍历整棵虚拟 DOM 树
  2. 每次更新都同步执行到底(不能中断)
  3. 如果组件层级很深、计算复杂,就会长时间占用主线程
  4. 主线程被占用时,浏览器无法响应用户操作(如输入、滚动) → 卡顿、掉帧

fiber的核心目标

React团队为了解决"更新太重,无法中断"的问题,引入了fiber架构

  1. 可中断更新:react更新可以被中断,让浏览器先去响应用户操作
  2. 可分片执行:大任务被拆分为小任务,(每一帧执行一点)
  3. 可恢复与重用:被中断后可以从上次中断的地方继续执行

Fiber的设计思路

React 把每个虚拟 DOM 节点(VNode)包装成一个 Fiber 对象, 这个对象包含:

  • 节点类型、props、state
  • 指向父节点、子节点、兄弟节点的指针(形成链表结构)
  • 更新优先级信息(lane)
  • 副作用标记(如需插入/删除/更新)

➡️ 这样 React 就可以:

  • 用「遍历链表」代替「递归函数调用」(可随时暂停)
  • 在空闲时间片中继续工作(用调度器协调)
  • 动态决定哪部分更新优先(配合 Concurrent Mode)

Fiber 是如何实现可中断渲染的?

  • Fiber 通过把递归改为循环、把组件树改为链表结构、并利用时间片调度机制,使得 React 的渲染可以"暂停---恢复---继续",从而实现了可中断渲染。

在React15中,更新虚拟DOM时使用的是递归遍历整棵树的方式

js 复制代码
function updateComponent(component) {
  component.render()
  component.children.forEach(updateComponent)
}

问题是:

  • JS 是单线程的;
  • 一旦进入这段递归逻辑,就无法中途暂停;
  • 如果组件树很大,浏览器主线程会被长期占用;
  • 用户交互、动画、输入都会卡顿。

React 16 重写架构为 Fiber Reconciler 。 目标:让"渲染过程"像执行协程一样 ------ 可中断、可恢复、可调度。

Fiber 的关键设计思想是:

"把每个虚拟 DOM 节点(VNode)变成一个 Fiber 对象,并把组件树改成链表结构。"

这样 React 就可以:

  1. while 循环遍历链表(而非递归);
  2. 每处理一个 Fiber 节点,都检查当前帧是否超时;
  3. 如果时间用完,就暂停渲染,把控制权交还浏览器;
  4. 下一帧(或空闲时)再从上次中断的 Fiber 继续工作。

React Hooks

useState

基础概念

useState 是 React 提供的用于在函数组件中声明状态(state)的 Hook。

const [state, setState] = useState(initialValue)

  • state:当前状态值。
  • setState:更新状态的函数,会触发组件重新渲染。
  • initialValue:初始状态值,只在组件首次渲染时使用。

运行机制

当组件执行时(函数重新运行),useState 并不会重新创建新的状态,而是通过 React 内部的 Hook 链表(Fiber)机制 取回上一次保存的状态值。

也就是说:

  • 虽然函数重新执行了,
  • useState 通过闭包 + 内部索引保存并取回之前的状态。

👉 因此即使多次调用 useState(0)state 也不会回到 0。

React 约束: Hook 调用顺序必须一致,否则状态会错位。

js 复制代码
let x = []
let index = 0
const myUseState = initial => {
	let currentIndex = index 
	x[currenIndex] = x[currentIndex] === undefined ? initial : x[currentIndex]

	const setInitial = value => {
		x[currentIndex] = value
		render()
	}
}

//模拟render函数
const render = () => {
	index = 0
	ReactDOM.render(<App/>, document.querySelector("#root"))
}

const App = () => {
	const [n, setN] = myUseState(0)
	const [m, setM] = myUseState(0)
	return (
		<div>
	         <p>n:{n}</p>
	         <button onClick={()=>setN(n+1)}>+1</button>
	         <p>m:{m}</p>
	         <button onClick={()=>setM(m+1)}>+1</button>
     	</div>
	)
}

异步批处理(Batch Update)

React 会将多个状态更新合并执行(在事件回调中)。

js 复制代码
setCount(count + 1)

setCount(count + 1)

// 实际只增加一次

在 React 18 中,异步任务(如 setTimeout、Promise)中的 setState 不再强制合并。

惰性初始化

初始值可以是一个函数:

const [data, setData] = useState(() => heavyCalculation())

heavyCalculation() 只在首次渲染时执行,避免每次渲染重复计算。

更新函数形式

当新状态依赖旧状态时,用函数式更新:

js 复制代码
setCount(prev => prev + 1)

useEffect

一、作用

useEffect 用于处理 副作用(side effects) ,比如:

  • 网络请求
  • 订阅 / 事件监听
  • 操作 DOM
  • 定时器

这些逻辑不能直接放在渲染阶段,否则会阻塞或污染渲染。

  • 函数组件需要是纯函数:

    • 相同输入 → 永远相同输出
    • 不修改外部变量
    • 不产生额外行为
  • React 设定函数组件必须满足:

    • 相同的 props & state → 必须产生完全相同的 UI
    • 不依赖外部可变环境
    • 没有无法预测的行为
    • 渲染阶段必须同步、快速、纯净

换句话说:

组件函数必须像数学函数一样:输入 → 输出 UI

二、执行时机

  • 初次渲染后 执行(不会阻塞渲染)。
  • 依赖项变化 时重新执行。
  • 组件卸载前 执行清理函数。

三、依赖数组 [deps]

写法 执行时机
useEffect(fn) 每次渲染都执行
useEffect(fn, []) 仅挂载和卸载时执行一次
useEffect(fn, [a, b]) 当依赖项 a 或 b 改变时执行

⚠️ React 比较依赖项是浅比较,如果依赖对象或数组的引用变了,即使内容没变也会触发。

四、清理函数

返回一个函数,用于卸载或重新执行前清理副作用:

javascript 复制代码
useEffect(() => {
  const id = setInterval(() => console.log('tick'), 1000)
  return () => clearInterval(id)
}, [])

执行时机:

  1. 组件卸载时;
  2. 副作用重新执行前。

五、面试常问点

  1. useEffect 为什么在渲染后执行? 为了让渲染过程纯净,不被副作用打断。

  2. 为什么要写依赖数组? 告诉 React 什么时候重新运行副作用,否则可能死循环。

  3. 依赖项写错或少写会怎样? 可能导致状态不同步或逻辑失效(React 会在严格模式下警告)。

  4. useLayoutEffect 和 useEffect 的区别?

    • useEffect:渲染完成后异步执行,不阻塞绘制。
    • useLayoutEffect:DOM 更新后、浏览器绘制前同步执行,可用于测量 DOM。

useRef

核心定义

  • useRef 是一个能在组件整个生命周期内 保持引用不变 的 Hook。
  • 它返回一个可变对象 { current: ... },这个对象在组件的重新渲染中不会被重置
js 复制代码
const ref = useRef(initialValue)
console.log(ref.current) // ref.current 保存的数据在组件多次渲染之间是持久的

应用场景

获取DOM节点
  • 在React中使用useRef获取DOM比原生方式获取DOM更可靠
  • inputRef.current 会指向对应的 DOM 元素。
  • 通常用于:聚焦、滚动、测量宽高、绑定第三方库。
js 复制代码
function App() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current.focus()
  }, [])

  return <input ref={inputRef} />
}
保存 任意可变值(不触发重新渲染)

count.current 的值在组件重渲染时仍然保持;

修改 ref.current 不会引起重新渲染

所以它非常适合存储:

  • 前一次的值(用于比较)
  • 定时器 id
  • 防抖节流计数器
  • 某个状态的缓存值
js 复制代码
function Timer() {
  const count = useRef(0)

  const handleClick = () => {
    count.current += 1
    console.log(count.current)
  }

  return <button onClick={handleClick}>Click</button>
}

useMemo

  • 开发中,我们只要修改了父组件的数据,所有的子组件都会重新渲染,这是十分消耗性能的
  • 如果我们希望子组件不要进行这种没有必要的重新渲染,我们可以将子组件继承PureComponent或者使用memo函数包裹
js 复制代码
import React, { memo, useState, useEffect } from 'react'
const A = (props) => {
  console.log('A1')
  useEffect(() => {
    console.log('A2')
  })
  return <div>A</div>
}

const B = memo((props) => {
  console.log('B1')
  useEffect(() => {
    console.log('B2')
  })
  return <div>B</div>
})

const Home = (props) => {
  const [a, setA] = useState(0)
  useEffect(() => {
    console.log('start')
    setA(1)
  }, [])
  return <div><A n={a} /><B /></div>
}
  • 将子组件B使用memo包裹之后,Home组件中的状态a的变化就不会导致B组件的重新渲染
  • 但是在子组件B使用了父组件的某个引用类型的变量或者函数时,那么当父组件状态更新之后,这些变量和函数就会重新赋值,导致子组件B还是会重新渲染
  • 想解决这个问题,就需要使用useMemo和useCallback了

useCallback

  • 当函数组件重新渲染时,其中的函数也会被重复定义多次
  • 如果使用useCallBack对函数进行包裹,那么在依赖(第二个参数)不变的情况下,会返回同一个函数 这样子组件就不会因为函数的重新定义而导致重新渲染了
  • useMemo和useCallBack相似,缓存的是函数的返回值,一般用来优化变量,但是如果将useMemo的返回值定义为返回一个函数就可以实现useCallBack一样的功能
js 复制代码
//用useMemo实现同useCallback一样的效果
   const increment = useCallback(fn,[])
   const increment2 = useMemo(()=>fn,[])
js 复制代码
import React, { memo, useState, useEffect, useMemo } from 'react'
const Home = (props) => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  useEffect(() => {
    setA(1)
  }, [])

  const add = useCallback(() => {
    console.log('b', b)
  }, [b])

  const name = useMemo(() => {
    return b + 'xuxi'
  }, [b])
  return <div><A n={a} /><B add={add} name={name} /></div>
}

useContext

useContext 是什么?

useContext 是 React 的一个 Hook,用于:

  • 函数组件中直接读取 由上层组件 Context.Provider 提供的值。

简单理解:

  • 不用一层层 props 传递,也能让深层组件拿到共享数据。

语法

ini 复制代码
const value = useContext(MyContext)
  • MyContext 是通过 React.createContext() 创建的上下文对象。
  • useContext() 返回最近的 <MyContext.Provider> 提供的 value
  • 当 Provider 的 value 变化时,所有使用该 context 的组件都会重新渲染。

使用步骤

js 复制代码
// context.js
import { createContext } from "react"
export const ThemeContext = createContext("light")

// App.jsx
import { ThemeContext } from "./context"
import Child from "./Child"

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Child />
    </ThemeContext.Provider>
  )
}

// Child.jsx
import { useContext } from "react"
import { ThemeContext } from "./context"

function Child() {
  const theme = useContext(ThemeContext)
  return <div>当前主题:{theme}</div>
}

特性

特性 说明
最近优先 如果组件外层有多个相同类型的 Provider,会取最近一层的 value
响应式更新 Provider 的 value 改变,会触发所有消费该 Context 的组件重新渲染
只能在函数组件或自定义 Hook 中使用 不能在类组件或普通函数中调用
不能脱离 Provider 使用 如果没有 Provider 包裹,会使用 createContext() 时设置的默认值

应用场景

场景 示例
✅ 主题切换 dark / light 模式
✅ 登录状态 用户信息、Token
✅ 多语言切换 中英文语言包
✅ 全局配置 比如分页大小、API地址等

常见问题

useContext 和 props 传递的区别?

对比项 props useContext
数据传递 一层层手动传递 任何层级都可直接拿到
灵活性 高(精确控制) 全局性(可能过度渲染)
适用场景 局部数据传递 全局共享状态

useContext 的缺点是什么?

  • Provider 的 value 改变时,所有消费它的组件都会重新渲染
  • 这可能导致性能问题(无论组件是否使用了 value 的具体字段);
  • 因此大型项目中往往结合 useReducerRedux / Zustand 等状态管理库 一起使用

forwardRef

在React开发中,有些时候我们需要获取DOM或者组件来进行某些操作

  • 如何使用ref来获取DOM

    • 使用createref创建ref对象,并且绑定到DOM元素上
  • forwardRef 解决的问题是ref 不会通props 传递下去,因为ref和key一样被React做了特殊处理

js 复制代码
import React, { PureComponent ,createRef} from 'react'
export class App extends PureComponent {
    //创建ref
    this.titleRef = createRef()
  }
  getNativeDOM(){
    console.log(this.titleRef.current)
  }
  render() {
    return (
      <div>
        <h2 ref={this.titleRef}>hello world</h2>
        <button onClick={e=>this.getNativeDOM()}>获取DOM</button>
      </div>
    )
  }
}
export default App

ref 的值根据节点的类型有所不同:

  1. ref 属性作用于HTML属性时,接收底层DOM元素作为其current属性
  2. ref属性作用于class组件时,接收组件实例作为其current属性
  3. 不能在函数组件上使用ref属性,因为他们没有实例

想将ref挂载到函数组件内部的某个class组件或者HTML元素上时,我们需要使用React.forwardRef将函数组件包裹,从而将ref传递到组件内部

js 复制代码
//获取函数组件的某个DOM
//使用forwardRef之后,可以传入两个参数,第二个为ref,我们可以实现ref转发
const Fun = forwardRef(function (props,ref) {
  return (
    <h1 ref={ref}>hello react</h1>
  )
})

使用ref作用于类组件,并调用类组件实例的方法

js 复制代码
import React, { PureComponent, createRef, forwardRef } from 'react'

//类子组件
class HelloWorld extends PureComponent {
  test() {
    console.log("test---")
  }
  render() {
    return (
      <h1>hello world</h1>
    )
  }
}
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {}
    this.hwRef = createRef()
  }
  getComponent() {
    //调用类组件实例的方法
    console.log(this.hwRef.current)
    this.hwRef.current.test()
  }
  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef} />
        <button onClick={e => this.getComponent()}>获取组件实例</button>
      </div>
    )
  }
}
export default App

useImperativeHandle

  • forwardRef使用带来的问题

    • 直接暴露给父组件,使得某些情况不可控
    • 父组件拿到子组件之后可以进行任意的操作
  • 通过useImperativeHandle可以暴露固定的操作

js 复制代码
import React, {
  useRef,
  forwardRef, useImperativeHandle
} from 'react'

const HYInput = forwardRef((props,ref)=> {
  const inputRef = useRef()
  useImperativeHandle(ref,()=> {
    return {
      focus: () => {
        console.log(345);
        inputRef.current.focus()
      }
    } 
  })

  return <input ref={inputRef} type="text"/>
})
export default function UseImperativeHandleHookDemo() {
  const inputRef = useRef()
  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e=>inputRef.current.focus()}>聚焦</button>
    </div>
  )
}

原理题

React Hooks 为什么不能放在条件判断里?

  • React Hooks 不能放在条件判断、循环或嵌套函数中 ,必须在组件的顶层调用。

    • 因为------React 是通过调用顺序来识别每一个 Hook 的。

每个组件渲染时,React 会维护一个"Hook 调用链表"或"数组", 类似这样(简化理解):

js 复制代码
// 第一次渲染
useState('A')   // Hook 1
useEffect(...)  // Hook 2
useState('B')   // Hook 3

React 会按顺序记下每一个 Hook 对应的状态(存在 Fiber 节点上)。 下一次渲染时,React 会再次按相同顺序调用 Hook 来匹配之前的状态。


如果放在条件语句中会发生什么?

scss 复制代码
if (flag) {
  useState(1)
}
useEffect(() => {})
  • 第一次渲染:

    • flag = true → 执行 useState(Hook 1)
    • 执行 useEffect(Hook 2)
  • 第二次渲染:

    • flag = false → 跳过 useState
    • useEffect 变成了 Hook 1!

🚨 React 内部匹配错位! 本该给 useEffect 的 Hook 状态,被错误地分配成了之前的 useState 状态。

结果可能报错:

r 复制代码
Rendered fewer hooks than expected
Invalid hook call

Hooks的执行顺序是如何保证的?

  • React 通过在每个 Fiber 节点上维护一个 Hook 链表
  • 并在每次渲染时按顺序遍历执行
  • 从而确保每个 Hook 的状态和顺序一致。

自定义 Hook 怎么避免闭包陷阱?

  1. 使用函数式更新(最常用)
javascript 复制代码
const increment = () => setCount(prev => prev + 1)
  • prev 永远是最新 state
  • 无需依赖闭包捕获的旧值
  • 适用于事件回调、定时器、异步请求等
  1. useRef 保存最新值

如果你需要在闭包里访问最新状态而不触发重新渲染: const countRef = useRef(count) useEffect(() => { countRef.current = count }, [count])

const logCount = () => { console.log(countRef.current) // 永远是最新值 }

  • 异步函数或事件可以使用 countRef.current
  • 不影响 React 渲染流程

组件渲染与性能优化

React 组件何时重新渲染?

  1. state发生变化:只要你调用 setState 产生了新的值(引用变化),组件就会重新渲染。

  2. props变化:只要父组件重新渲染 ,子组件也会跟着渲染(除非使用 React.memo)。

    即使 props 内容没变 ------ 只要父组件 render,子组件也会 render。

  3. context变化:当某个 Context Provider 的 value 改变,所有消费该 context 的子组件都会重渲染。

  4. 父组件重新渲染导致子组件渲染(哪怕 props 不变)

浅比较会对比:

  • 基本类型值(number / string / boolean / null / undefined) → 直接比较值是否相等。
  • 引用类型(object / array / function)只比较引用地址是否相同,不会比较内部的内容。

React 在性能优化时会用浅比较,比如:

  • React.memo
  • PureComponent
  • shouldComponentUpdate
  • useMemo / useCallback 的依赖项比较
  • useEffect / useCallback 的依赖数组

因为浅比较非常快,不需要深度遍历对象。

  • 浅比较带来的典型问题

    • 使用 inline function 导致子组件重新渲染
scss 复制代码
<Child onClick={() => setCount(count + 1)} />

每次父组件 render 时都会创建新的函数引用 → 浅比较结果:不同 → 子组件重新渲染

  • 解决方法

    • useCallback 固定函数引用
    • useMemo 固定对象 / 数组引用
    • 子组件用 React.memo

如何优化一个大表格或长列表的渲染性能?(虚拟列表)

  • 为什么需要虚拟列表?

    • 当列表有 成百上千甚至上万条数据时:

      • 浏览器会创建大量 DOM(慢)
      • 布局、重排、重绘消耗巨大(卡)
      • 滚动时频繁触发渲染(卡顿)

👉 核心思路 : 只渲染可视区域内的那几十个节点,其余内容用占位高度撑开。

  • 什么时候用虚拟列表

    • 满足任一即可使用虚拟列表:

      • 单页表格数据量 > 200 行
      • 单页列表 > 300 行
      • 存在大量复杂 DOM(图片、按钮、操作列)
      • 有频繁更新、滚动操作
  • 核心原理(一句话版本)

    • 只有可视区域 + 缓冲区的元素真实渲染
    • 其他区域只用一个大容器撑开高度
    • 视觉上像完整列表,但实际 DOM 数量永远保持几十个
  • 虚拟列表关键技术

    • 容器高度:列表容器要固定高度或可计算高度,否则无法计算可视范围。

    • 每行高度:

      • 固定高度:最好实现,可用rowheight直接算
      • 不定高度:需要实时记录高度(难度更高)
    • 计算可视区域的起止 index

      ini 复制代码
      startIndex = Math.floor(scrollTop / rowHeight)
      endIndex = startIndex + 可视区域行数 + buffer
    • 渲染可视区域数据

      • 只渲染这一小段数据即可。
    • 使用 translateY 把渲染的内容"挪"到正确位置

      ini 复制代码
      style="transform: translateY(startIndex * rowHeight px)"
  • 前端常用虚拟列表方案

    • React:react-window

      • 轻量、简单、性能极佳。
      • <FixedSizeList> 固定行高列表
      • <VariableSizeList> 不定高度列表
      • <FixedSizeGrid> 表格(大表格强烈推荐)
ini 复制代码
import { FixedSizeList as List } from "react-window";
​
<List
  height={600}
  width={800}
  itemSize={40}
  itemCount={list.length}
>
  {({ index, style }) => (
    <div style={style}>{list[index].name}</div>
  )}
</List>
  • Ant Design v5
ini 复制代码
<Table
  scroll={{ y: 600 }}
  virtual
  columns={columns}
  dataSource={data}
/>
  • 自己实现
ini 复制代码
import React, { useRef, useState, useEffect } from "react";
​
export default function VirtualList({ itemHeight, height, data, renderItem }) {
  const containerRef = useRef(null);
  const [startIndex, setStartIndex] = useState(0);
​
  const visibleCount = Math.ceil(height / itemHeight); // 可视区域展示多少条
​
  // 滚动事件
  const onScroll = () => {
    const scrollTop = containerRef.current.scrollTop;
    const newStartIndex = Math.floor(scrollTop / itemHeight);
    setStartIndex(newStartIndex);
  };
​
  // 当前需要渲染的数据
  const endIndex = startIndex + visibleCount;
  const visibleData = data.slice(startIndex, endIndex);
​
  // 用两个 padding 占位本来应该存在的高度
  const paddingTop = startIndex * itemHeight;
  const paddingBottom = (data.length - endIndex) * itemHeight;
​
  return (
    <div
      ref={containerRef}
      style={{
        height,
        overflowY: "auto",
        border: "1px solid #ccc",
      }}
      onScroll={onScroll}
    >
      <div style={{ paddingTop, paddingBottom }}>
        {visibleData.map((item, i) =>
          renderItem(item, startIndex + i)
        )}
      </div>
    </div>
  );
}
相关推荐
北辰alk43 分钟前
跨域难题终结者:Vue项目中优雅解决跨域问题的完整指南
前端
吹水一流43 分钟前
为什么 SVG 能在现代前端中胜出?
前端
小皮虾43 分钟前
拒绝卡顿!小程序图片本地“极速”旋转与格式转换,离屏 Canvas 性能调优实战
前端·javascript·微信小程序
小熊哥72243 分钟前
一个有趣的CSS题目
前端
小时前端44 分钟前
性能优化:从“用户想走”到“愿意留下”的1.8秒
前端·面试
进阶的鱼1 小时前
关于微前端框架wujie的一次企业级应用实践demo?
前端·vue.js·react.js
凯心1 小时前
React 中没有 v-model,如何优雅地处理表单输入
前端·vue.js·react.js
南雨北斗1 小时前
vue3 Composable介绍
前端
x***B4111 小时前
TypeScript项目引用
前端·javascript·typescript