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>文本不同 → 只更新文本。
- 第 1 个
-
最终只执行一条 DOM 操作:
list.children[1].textContent = '橘子';
-
diff算法
传统diff算法的时间复杂度是O(n³)
- 我们要把旧树变成新树,找到最小修改路径
为什么时间复杂度是O(n³)?
-
遍历旧树的每个节点(n次)
-
遍历新树的每个节点(n次)
- 对比每个旧树中的节点,找到新树中可能对应的新节点
-
比较两个节点的子结构是否完全相同
-
因为判断"是否同一个节点"不仅要看标签名,还要看它的整个子结构是否相同。 这就需要再深入进去比较它们的子树。
-
每对匹配节点都可能有一整棵子树;
每棵子树的节点数也可能接近 n;
所以在最坏情况下,每一对匹配都要再递归比较一遍整棵子树。
-
于是复杂度变成:
O(n)(旧树) × O(n)(新树) × O(n)(子树递归) = O(n³)
第 3 层递归比较子树的复杂度,是因为每一对匹配节点 都还要递归地比较它们的子树结构。
前两层只是找出"候选节点对",第三层才是深入检查"它们真的一样吗"。
所以整体复杂度是: 旧树节点数 × 新树节点数 × 子树递归 = O(n³) 。
React的diff算法的时间复杂度是O(n)
React 把问题简化成了三条"经验规则",正是这三条规则让复杂度从 O(n³) → O(n)。
-
同层比较,不跨层
- 复杂度就从 O(n³) → O(n²)
-
不同类型节点,直接替换整棵子树
- 也就是说,不同类型的节点永远不去比较子树 。 这避免了对子树的递归匹配,进一步从 O(n²) → O(n) 。
-
通过
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 的过程是怎样的?
- 当组件的
state或props变化时,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之前,更新流程是这样的:
- 状态更新后,React 会从根节点开始,递归遍历整棵虚拟 DOM 树;
- 每次更新都同步执行到底(不能中断)
- 如果组件层级很深、计算复杂,就会长时间占用主线程;
- 主线程被占用时,浏览器无法响应用户操作(如输入、滚动) → 卡顿、掉帧
fiber的核心目标
React团队为了解决"更新太重,无法中断"的问题,引入了fiber架构
- 可中断更新:react更新可以被中断,让浏览器先去响应用户操作
- 可分片执行:大任务被拆分为小任务,(每一帧执行一点)
- 可恢复与重用:被中断后可以从上次中断的地方继续执行
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 就可以:
- 用
while循环遍历链表(而非递归); - 每处理一个 Fiber 节点,都检查当前帧是否超时;
- 如果时间用完,就暂停渲染,把控制权交还浏览器;
- 下一帧(或空闲时)再从上次中断的 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)
}, [])
执行时机:
- 组件卸载时;
- 副作用重新执行前。
五、面试常问点
-
useEffect 为什么在渲染后执行? 为了让渲染过程纯净,不被副作用打断。
-
为什么要写依赖数组? 告诉 React 什么时候重新运行副作用,否则可能死循环。
-
依赖项写错或少写会怎样? 可能导致状态不同步或逻辑失效(React 会在严格模式下警告)。
-
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 的具体字段);
- 因此大型项目中往往结合
useReducer或Redux / 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 的值根据节点的类型有所不同:
- 当ref 属性作用于HTML属性时,接收底层DOM元素作为其current属性
- 当ref属性作用于class组件时,接收组件实例作为其current属性
- 不能在函数组件上使用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 = true → 执行
-
第二次渲染:
- flag = false → 跳过
useState useEffect变成了 Hook 1!
- flag = false → 跳过
🚨 React 内部匹配错位! 本该给
useEffect的 Hook 状态,被错误地分配成了之前的useState状态。
结果可能报错:
r
Rendered fewer hooks than expected
Invalid hook call
Hooks的执行顺序是如何保证的?
- React 通过在每个 Fiber 节点上维护一个 Hook 链表
- 并在每次渲染时按顺序遍历执行
- 从而确保每个 Hook 的状态和顺序一致。
自定义 Hook 怎么避免闭包陷阱?
- 使用函数式更新(最常用)
javascript
const increment = () => setCount(prev => prev + 1)
prev永远是最新 state- 无需依赖闭包捕获的旧值
- 适用于事件回调、定时器、异步请求等
- 用
useRef保存最新值
如果你需要在闭包里访问最新状态而不触发重新渲染: const countRef = useRef(count) useEffect(() => { countRef.current = count }, [count])
const logCount = () => { console.log(countRef.current) // 永远是最新值 }
- 异步函数或事件可以使用
countRef.current - 不影响 React 渲染流程
组件渲染与性能优化
React 组件何时重新渲染?
-
state发生变化:只要你调用
setState产生了新的值(引用变化),组件就会重新渲染。 -
props变化:只要父组件重新渲染 ,子组件也会跟着渲染(除非使用
React.memo)。即使 props 内容没变 ------ 只要父组件 render,子组件也会 render。
-
context变化:当某个 Context Provider 的 value 改变,所有消费该 context 的子组件都会重渲染。
-
父组件重新渲染导致子组件渲染(哪怕 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
inistartIndex = Math.floor(scrollTop / rowHeight) endIndex = startIndex + 可视区域行数 + buffer -
渲染可视区域数据
- 只渲染这一小段数据即可。
-
使用 translateY 把渲染的内容"挪"到正确位置
inistyle="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>
);
}