Appearance
React 面试题大全
1. React渲染原理
React 的渲染原理可以分为以下几个核心阶段:
初始化渲染阶段
创建虚拟 DOM 树:JSX 被 Babel 编译成
React.createElement()调用,生成虚拟 DOM 树(React Element 树)构建 Fiber 树:React 16+ 引入 Fiber 架构,虚拟 DOM 节点会被转换为 Fiber 节点,形成 Fiber 树
生成副作用链表:在协调阶段,React 会标记需要执行的副作用(如 DOM 更新、生命周期调用等)
提交阶段:将计算出的副作用应用到真实 DOM
更新渲染阶段
触发更新:通过 setState、forceUpdate、ReactDOM.render 或 Hooks 触发
调度更新:React 调度器根据优先级安排更新任务
协调阶段(Reconciliation):
- 生成新的 Fiber 树
- 与旧 Fiber 树进行 diff 比较
- 标记副作用
提交阶段(Commit):
- 不可中断,同步执行
- 将变更应用到真实 DOM
- 执行生命周期和 Hooks
渲染流程图
JSX → React.createElement() → React Element → Fiber Node → DOM关键点
- 批量更新:React 会将多个 setState 合并为一次更新
- 优先级调度:高优先级任务可以打断低优先级任务
- 时间切片:利用浏览器空闲时间执行任务
2. React 虚拟DOM
什么是虚拟 DOM
虚拟 DOM(Virtual DOM)是用 JavaScript 对象来描述真实 DOM 结构的一种技术。它是对 DOM 的轻量级抽象。
虚拟 DOM 的结构
javascript
const element = {
type: 'div',
props: {
id: 'container',
children: [{ type: 'span', props: { children: 'Hello' } }]
}
}虚拟 DOM 的优势
- 跨平台:可以渲染到 DOM、Native、Canvas 等多种平台
- 批量更新:将多次更新合并为一次 DOM 操作
- 声明式编程:开发者只需关注状态,不需要手动操作 DOM
- 性能优化:通过 diff 算法最小化 DOM 操作
虚拟 DOM 的工作流程
State/Props 变化 → 生成新的虚拟 DOM → Diff 比较 → 计算最小变更 → 更新真实 DOM虚拟 DOM 的性能考量
- 虚拟 DOM 本身有计算开销
- 对于简单操作,直接操作 DOM 可能更快
- 对于复杂应用,虚拟 DOM 的批量更新和 diff 优化能带来更好的性能
3. React diff 算法原理
React 的 diff 算法基于三个前提假设:
三个假设
- 不同类型的元素产生不同的树:如
<div>变成<span>会触发完全重建 - 通过 key 标识哪些子元素是稳定的:key 帮助 React 识别元素的身份
- 只对同层级节点进行比较:不会跨层级比较节点
Diff 策略
1. Tree Diff(树层级的比较)
只对同一层级的节点进行比较,时间复杂度 O(n)
旧树: 新树:
A A
/ \ / \
B C → B D
/ \ / \
D E E F2. Component Diff(组件比较)
- 同类型组件:继续比较虚拟 DOM 树
- 不同类型组件:直接替换整个组件
3. Element Diff(元素比较)
当节点处于同一层级时,通过 key 来识别节点:
javascript
// 旧列表
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
// 新列表
<li key="a">A</li>
<li key="c">C</li>
<li key="b">B</li>节点操作类型
- INSERT_MARKUP:插入新节点
- MOVE_EXISTING:移动已有节点
- REMOVE_NODE:移除节点
Key 的作用
javascript
// 没有 key 时,React 会认为所有节点都需要更新
<li>A</li> → <li>A</li> // 重新创建
<li>B</li> → <li>C</li> // 重新创建
<li>C</li> → <li>B</li> // 重新创建
// 有 key 时,React 可以识别并复用节点
<li key="a">A</li> → <li key="a">A</li> // 复用
<li key="b">B</li> → <li key="c">C</li> // 更新
<li key="c">C</li> → <li key="b">B</li> // 更新4. React fiber 是什么,fiber 解决了什么问题能让 React 性能大飞跃?
什么是 Fiber
Fiber 是 React 16 引入的新协调引擎,它是一种数据结构,也是一个执行架构。
Fiber 数据结构
javascript
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 实例相关
this.tag = tag;
this.key = key;
this.type = null;
// Fiber 树结构
this.return = null; // 父节点
this.child = null; // 第一个子节点
this.sibling = null; // 下一个兄弟节点
// 状态
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.memoizedState = null;
// 副作用
this.effectTag = NoEffect;
this.nextEffect = null;
// 优先级
this.lanes = NoLanes;
}Fiber 解决的问题
1. 解决主线程阻塞问题
React 15 及之前版本的问题:
- 更新过程是同步递归的,无法中断
- 大型应用更新时会阻塞主线程
- 导致动画卡顿、交互无响应
Fiber 的解决方案:
- 将工作拆分成小单元
- 每个单元执行后可以暂停
- 将控制权交还给浏览器
2. 实现优先级调度
javascript
// 优先级从高到低
ImmediatePriority // 立即执行(用户输入)
UserBlockingPriority // 用户阻塞(点击、输入)
NormalPriority // 正常优先级(数据请求)
LowPriority // 低优先级(分析)
IdlePriority // 空闲时执行3. 增量渲染
- 将渲染工作拆分成多个帧
- 每帧只做一部分工作
- 利用
requestIdleCallback或MessageChannel实现
Fiber 架构的优势
- 可中断渲染:高优先级任务可以打断低优先级任务
- 可恢复渲染:中断后可以从断点继续
- 优先级调度:重要的更新优先执行
- 并发模式:支持 Concurrent Mode
性能提升原理
React 15: 同步递归 → 阻塞主线程 → 卡顿
React 16+: 异步可中断 → 时间切片 → 流畅5. React Fiber 是如何实现更新过程可控?
可控性的实现方式
1. 工作单元化
将大的渲染任务拆分成小的 Fiber 节点:
javascript
// 每个 Fiber 节点是一个工作单元
function performUnitOfWork(fiber) {
// 1. 处理当前 Fiber
beginWork(fiber)
// 2. 处理子节点
if (fiber.child) {
return fiber.child
}
// 3. 处理兄弟节点
while (fiber) {
completeWork(fiber)
if (fiber.sibling) {
return fiber.sibling
}
fiber = fiber.return
}
}2. 循环可中断
javascript
function workLoop(concurrentMode) {
performWorkUntilDeadline()
function performWorkUntilDeadline() {
if (workInProgress && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress)
performWorkUntilDeadline()
}
}
}3. 优先级队列
javascript
// 调度器根据优先级安排任务
function scheduleUpdateOnFiber(fiber, lane) {
// 检查优先级
const priorityLevel = getCurrentPriorityLevel()
// 加入调度队列
ensureRootIsScheduled(root)
}4. 中断与恢复
javascript
// 保存当前状态
let workInProgress = null
// 中断时保存状态
function interruptWork() {
// 保存当前进度
// 等待下次调度继续
}
// 恢复时继续执行
function resumeWork() {
// 从上次中断的地方继续
}状态保存机制
Fiber 树保存了两份状态:
javascript
// current 树:当前屏幕显示的状态
// workInProgress 树:正在计算的新状态
current.alternate = workInProgress
workInProgress.alternate = current更新流程控制
调度更新 → 检查优先级 → 开始渲染 → 检查是否需要中断 → 中断/继续 → 提交更新6. React Fiber 是如何实现将控制权交还浏览器?
核心机制:时间切片
React 使用时间切片(Time Slicing)来实现控制权交还。
Scheduler 调度器
javascript
// packages/scheduler/src/forks/Scheduler.js
// 每帧的时间预算(通常是 5ms)
const frameInterval = 5
// 开始时间
let startTime = -1
function shouldYieldToHost() {
const currentTime = getCurrentTime()
return currentTime >= startTime + frameInterval
}MessageChannel 实现
React 使用 MessageChannel 而不是 requestIdleCallback:
javascript
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = performWorkUntilDeadline
function schedulePerformWorkUntilDeadline() {
port.postMessage(null)
}
function performWorkUntilDeadline() {
startTime = getCurrentTime()
// 执行工作
while (workInProgress && !shouldYieldToHost()) {
workInProgress = performUnitOfWork(workInProgress)
}
// 如果还有工作,继续调度
if (workInProgress) {
schedulePerformWorkUntilDeadline()
}
}为什么不用 requestIdleCallback
- 浏览器兼容性:部分浏览器不支持
- 执行时机不可控:空闲时间不确定
- 性能问题:可能延迟过高
控制权交还流程
1. 开始渲染工作
2. 检查时间预算(shouldYield)
3. 时间用完 → 暂停工作
4. 通过 MessageChannel 安排下次执行
5. 浏览器处理其他任务(渲染、事件等)
6. 下次事件循环继续渲染时间切片示意
javascript
// 一帧 16.6ms
// React 预留 5ms 用于渲染
// 剩余时间用于浏览器其他工作
帧开始
├── 样式计算
├── 布局
├── 绘制
├── React 渲染(5ms)
│ ├── Fiber 节点 1
│ ├── Fiber 节点 2
│ └── ...(时间到,暂停)
└── 其他 JS 执行7. React 数据流是怎么样的?
单向数据流
React 采用单向数据流(Unidirectional Data Flow),数据从父组件流向子组件。
数据流向图
┌─────────────────────────────┐
│ Parent Component │
│ │
│ State ──→ Props ──→ View │
│ │ │ │
│ └── Callbacks ───┘ │
└──────────────┬───────────────┘
│
▼ Props
┌─────────────────────────────┐
│ Child Component │
│ │
│ Props ──→ View │
└─────────────────────────────┘数据流特点
1. Props 向下传递
javascript
function Parent() {
const [data, setData] = useState('hello')
return <Child data={data} />
}
function Child({ data }) {
return <div>{data}</div>
}2. 回调函数向上传递数据
javascript
function Parent() {
const [data, setData] = useState('')
const handleChange = value => {
setData(value)
}
return <Child onChange={handleChange} />
}
function Child({ onChange }) {
return <input onChange={e => onChange(e.target.value)} />
}3. 状态提升
javascript
// 两个子组件需要共享状态,将状态提升到父组件
function Parent() {
const [sharedData, setSharedData] = useState('')
return (
<>
<ChildA data={sharedData} />
<ChildB onDataChange={setSharedData} />
</>
)
}数据流的优势
- 可预测性:数据流向清晰,易于追踪
- 可调试性:状态变化有迹可循
- 可维护性:组件职责明确
状态管理方案
对于复杂应用,可以使用状态管理库:
- Context API:React 内置的跨层级数据传递
- Redux:单一数据源,可预测的状态管理
- MobX:响应式状态管理
- Zustand:轻量级状态管理
- Jotai/Recoil:原子化状态管理
8. React 19 新特性?
React 19 主要新特性
1. Actions(动作)
简化表单处理和异步操作:
javascript
import { useActionState } from 'react'
function Form() {
const [state, formAction] = useActionState(submitForm, null)
return (
<form action={formAction}>
<input name='name' />
<button type='submit'>Submit</button>
{state?.error && <p>{state.error}</p>}
</form>
)
}
async function submitForm(prevState, formData) {
const name = formData.get('name')
const error = await updateName(name)
if (error) {
return { error }
}
return { success: true }
}2. useActionState Hook
用于处理表单状态和异步操作:
javascript
const [state, formAction, isPending] = useActionState(action, initialState)3. useFormStatus Hook
获取表单提交状态:
javascript
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type='submit' disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
)
}4. useOptimistic Hook
乐观更新:
javascript
import { useOptimistic } from 'react'
function TodoList({ todos, updateTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
)
async function handleSubmit(formData) {
const newTodo = { id: Date.now(), text: formData.get('todo') }
addOptimisticTodo(newTodo)
await updateTodo(newTodo)
}
return (
<form action={handleSubmit}>
<input name='todo' />
<button>Add</button>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</form>
)
}5. use() API
统一的资源读取 API:
javascript
import { use } from 'react'
function Comments({ commentsPromise }) {
const comments = use(commentsPromise)
return comments.map(c => <p key={c.id}>{c.text}</p>)
}
// 也可以用于 Context
function Theme() {
const theme = use(ThemeContext)
return <div className={theme}>...</div>
}6. Server Components 稳定版
服务端组件正式稳定:
javascript
// Server Component
async function BlogPost({ slug }) {
const post = await db.posts.find(slug)
return <article>{post.content}</article>
}
// Client Component
;('use client')
function LikeButton({ postId }) {
const [likes, setLikes] = useState(0)
return <button onClick={() => setLikes(l => l + 1)}>{likes}</button>
}7. 文档元数据支持
javascript
import { Metadata } from 'react'
function BlogPost({ post }) {
return (
<>
<title>{post.title}</title>
<meta name='description' content={post.excerpt} />
<article>{post.content}</article>
</>
)
}8. 样式表支持
javascript
import { StyleSheet } from 'react'
function Component() {
return (
<>
<StyleSheet href='styles.css' precedence='high' />
<div className='styled'>Content</div>
</>
)
}9. ref 作为 prop
不再需要 forwardRef:
javascript
// React 19 之前
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />
})
// React 19
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />
}10. 改进的错误处理
更好的错误信息和错误恢复机制。
9. React 组件通信的方式有哪些?
1. Props 传递
最基础的通信方式,父组件向子组件传递数据:
javascript
function Parent() {
const data = 'Hello'
return <Child data={data} />
}
function Child({ data }) {
return <div>{data}</div>
}2. 回调函数
子组件向父组件传递数据:
javascript
function Parent() {
const handleData = data => {
console.log('Received:', data)
}
return <Child onSend={handleData} />
}
function Child({ onSend }) {
return <button onClick={() => onSend('Hello')}>Send</button>
}3. 状态提升
兄弟组件通信,将共享状态提升到共同父组件:
javascript
function Parent() {
const [sharedData, setSharedData] = useState('')
return (
<>
<ChildA data={sharedData} />
<ChildB onChange={setSharedData} />
</>
)
}4. Context API
跨层级组件通信:
javascript
const ThemeContext = createContext('light')
function App() {
return (
<ThemeContext.Provider value='dark'>
<DeepChild />
</ThemeContext.Provider>
)
}
function DeepChild() {
const theme = useContext(ThemeContext)
return <div>{theme}</div>
}5. Refs
父组件访问子组件实例或 DOM:
javascript
function Parent() {
const childRef = useRef()
const handleClick = () => {
childRef.current.focus()
}
return (
<>
<Child ref={childRef} />
<button onClick={handleClick}>Focus</button>
</>
)
}
const Child = forwardRef((props, ref) => {
return <input ref={ref} />
})6. 事件总线(Event Bus)
使用发布订阅模式:
javascript
class EventBus {
constructor() {
this.events = {}
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(data))
}
}
}
const bus = new EventBus()
// 组件 A
bus.emit('message', 'Hello')
// 组件 B
bus.on('message', data => {
console.log(data)
})7. 状态管理库
使用 Redux、MobX、Zustand 等:
javascript
// Redux 示例
import { useSelector, useDispatch } from 'react-redux'
function ComponentA() {
const dispatch = useDispatch()
return <button onClick={() => dispatch({ type: 'UPDATE' })}>Update</button>
}
function ComponentB() {
const data = useSelector(state => state.data)
return <div>{data}</div>
}8. URL 参数
通过路由传递数据:
javascript
import { useParams, useSearchParams } from 'react-router-dom'
function Component() {
const { id } = useParams()
const [searchParams] = useSearchParams()
return <div>ID: {id}</div>
}通信方式对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Props | 父子通信 | 简单直接 | 层级深时繁琐 |
| 回调函数 | 子父通信 | 数据流清晰 | 需要逐层传递 |
| 状态提升 | 兄弟通信 | 逻辑集中 | 父组件臃肿 |
| Context | 跨层级 | 避免逐层传递 | 可能引起不必要的渲染 |
| Refs | 访问实例/DOM | 直接访问 | 破坏数据流 |
| 事件总线 | 任意组件 | 灵活 | 难以追踪 |
| 状态管理 | 复杂应用 | 集中管理 | 学习成本 |
10. React 的生命周期以及每个生命周期都做了什么
React 16.4+ 生命周期
挂载阶段(Mounting)
1. constructor
javascript
constructor(props) {
super(props);
// 初始化 state
this.state = { count: 0 };
// 绑定方法
this.handleClick = this.handleClick.bind(this);
}作用:
- 初始化 state
- 绑定事件处理方法
- 不要在这里调用 setState
2. static getDerivedStateFromProps
javascript
static getDerivedStateFromProps(props, state) {
// 根据 props 更新 state
if (props.userId !== state.prevUserId) {
return {
userId: props.userId,
prevUserId: props.userId,
};
}
return null; // 不更新 state
}作用:
- 在渲染前更新 state
- 返回对象更新 state,返回 null 不更新
- 很少使用,通常可以用其他方式替代
3. render
javascript
render() {
return <div>{this.state.count}</div>;
}作用:
- 返回 JSX 描述 UI
- 必须是纯函数
- 不要在这里调用 setState
4. componentDidMount
javascript
componentDidMount() {
// 发起网络请求
fetch('/api/data').then(data => {
this.setState({ data });
});
// 添加事件监听
window.addEventListener('resize', this.handleResize);
// 操作 DOM
this.container.focus();
}作用:
- 组件挂载后执行
- 适合发起网络请求
- 添加事件监听
- 操作 DOM
- 可以调用 setState
更新阶段(Updating)
1. static getDerivedStateFromProps
同挂载阶段,每次更新前都会调用。
2. shouldComponentUpdate
javascript
shouldComponentUpdate(nextProps, nextState) {
// 返回 false 阻止更新
return this.state.count !== nextState.count;
}作用:
- 性能优化,阻止不必要的渲染
- 返回 true 继续更新,false 阻止更新
- 不要在这里调用 setState
3. render
同挂载阶段。
4. getSnapshotBeforeUpdate
javascript
getSnapshotBeforeUpdate(prevProps, prevState) {
// 获取更新前的 DOM 信息
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}作用:
- 在 DOM 更新前获取快照
- 返回值会传给 componentDidUpdate
- 用于保留滚动位置等场景
5. componentDidUpdate
javascript
componentDidUpdate(prevProps, prevState, snapshot) {
// 对比 props 变化
if (this.props.userId !== prevProps.userId) {
this.fetchData(this.props.userId);
}
// 使用 snapshot
if (snapshot !== null) {
this.listRef.current.scrollTop =
this.listRef.current.scrollHeight - snapshot;
}
// 注意:调用 setState 必须有条件判断,否则会死循环
}作用:
- DOM 更新后执行
- 可以操作 DOM
- 可以发起网络请求
- 可以调用 setState(需要条件判断)
卸载阶段(Unmounting)
componentWillUnmount
javascript
componentWillUnmount() {
// 清除定时器
clearInterval(this.timer);
// 移除事件监听
window.removeEventListener('resize', this.handleResize);
// 取消网络请求
this.abortController.abort();
}作用:
- 组件卸载前执行
- 清理副作用(定时器、事件监听、网络请求)
- 不要在这里调用 setState
错误处理
1. static getDerivedStateFromError
javascript
static getDerivedStateFromError(error) {
// 更新 state,显示降级 UI
return { hasError: true };
}2. componentDidCatch
javascript
componentDidCatch(error, errorInfo) {
// 记录错误日志
logErrorToService(error, errorInfo);
}生命周期图示
挂载阶段:
constructor → getDerivedStateFromProps → render → componentDidMount
更新阶段:
getDerivedStateFromProps → shouldComponentUpdate → render →
getSnapshotBeforeUpdate → componentDidUpdate
卸载阶段:
componentWillUnmount已废弃的生命周期
以下生命周期在 React 17+ 已被移除:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
11. React 有哪几种方式改变 state以及区别
1. setState
类组件中最常用的方式:
javascript
class Component extends React.Component {
state = { count: 0 }
// 对象形式
handleClick = () => {
this.setState({ count: this.state.count + 1 })
}
// 函数形式
handleClick2 = () => {
this.setState((prevState, props) => ({
count: prevState.count + 1
}))
}
// 回调函数
handleClick3 = () => {
this.setState({ count: 1 }, () => {
console.log('State updated:', this.state.count)
})
}
}2. forceUpdate
强制组件重新渲染:
javascript
class Component extends React.Component {
handleClick = () => {
this.forceUpdate()
}
}3. useState Hook
函数组件中使用:
javascript
function Component() {
const [count, setCount] = useState(0)
// 直接设置
const handleClick1 = () => {
setCount(1)
}
// 函数形式
const handleClick2 = () => {
setCount(prev => prev + 1)
}
}4. useReducer Hook
复杂状态管理:
javascript
function Component() {
const [state, dispatch] = useReducer(reducer, initialState)
const handleClick = () => {
dispatch({ type: 'INCREMENT' })
}
}5. 状态管理库
javascript
import { useDispatch, useSelector } from 'react-redux'
function Component() {
const dispatch = useDispatch()
const count = useSelector(state => state.count)
const handleClick = () => {
dispatch({ type: 'INCREMENT' })
}
}区别对比
| 方式 | 适用组件 | 特点 |
|---|---|---|
| setState | 类组件 | 合并更新,异步(大部分情况) |
| forceUpdate | 类组件 | 跳过 shouldComponentUpdate |
| useState | 函数组件 | 替换更新,批处理 |
| useReducer | 函数组件 | 适合复杂状态逻辑 |
| Redux 等 | 所有组件 | 全局状态管理 |
setState 的特点
1. 合并更新
javascript
// 类组件:合并更新
this.setState({ a: 1 })
this.setState({ b: 2 })
// 结果:{ a: 1, b: 2 }
// 函数组件:替换更新
setState({ a: 1 })
setState({ b: 2 })
// 结果:{ b: 2 },a 丢失2. 异步更新
javascript
this.setState({ count: 1 })
console.log(this.state.count) // 可能还是旧值
// 获取最新值
this.setState({ count: 1 }, () => {
console.log(this.state.count) // 1
})3. 批量更新
javascript
handleClick = () => {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// 结果:count 只增加 1
}
// 解决方案:使用函数形式
handleClick = () => {
this.setState(prev => ({ count: prev.count + 1 }))
this.setState(prev => ({ count: prev.count + 1 }))
this.setState(prev => ({ count: prev.count + 1 }))
// 结果:count 增加 3
}12. React 有哪几种创建组件方法以及区别
1. 函数组件
javascript
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
// 箭头函数形式
const Welcome = props => <h1>Hello, {props.name}</h1>特点:
- 简洁,代码量少
- 没有 this
- 没有生命周期(但有 Hooks)
- 性能稍好(不需要实例化)
- 从 React 16.8 开始支持 Hooks
2. 类组件
javascript
class Welcome extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
render() {
return <h1>Hello, {this.props.name}</h1>
}
}特点:
- 有 this
- 有生命周期
- 有 state
- 可以使用 ref
- 代码量较多
3. React.createClass(已废弃)
javascript
const Welcome = React.createClass({
getInitialState() {
return { count: 0 }
},
render() {
return <h1>Hello, {this.props.name}</h1>
}
})特点:
- React 15.5 之前使用
- 现已废弃,需要使用 create-react-class 包
4. PureComponent
javascript
class Welcome extends React.PureComponent {
render() {
return <h1>Hello, {this.props.name}</h1>
}
}特点:
- 自动实现 shouldComponentUpdate
- 浅比较 props 和 state
- 性能优化
5. memo 包装的函数组件
javascript
const Welcome = React.memo(function Welcome(props) {
return <h1>Hello, {props.name}</h1>
})
// 或使用箭头函数
const Welcome = React.memo(props => <h1>Hello, {props.name}</h1>)特点:
- 类似 PureComponent
- 浅比较 props
- 函数组件的性能优化
区别对比
| 特性 | 函数组件 | 类组件 | PureComponent |
|---|---|---|---|
| this | 无 | 有 | 有 |
| state | Hooks | 有 | 有 |
| 生命周期 | Hooks | 有 | 有 |
| 性能 | 较好 | 一般 | 较好 |
| 代码量 | 少 | 多 | 多 |
| 学习成本 | 低 | 高 | 高 |
| Hooks | 支持 | 不支持 | 不支持 |
推荐使用
现代 React 开发推荐使用函数组件 + Hooks:
javascript
function Welcome({ name }) {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('Mounted')
}, [])
return (
<div>
<h1>Hello, {name}</h1>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
</div>
)
}13. react 中 props 和 state 有什么区别
Props(属性)
Props 是从父组件传递给子组件的数据:
javascript
function Parent() {
return <Child name='React' />
}
function Child(props) {
return <h1>Hello, {props.name}</h1>
}特点:
- 只读,子组件不能修改
- 由父组件控制
- 用于组件间通信
- 可以是任何类型的数据
State(状态)
State 是组件内部管理的状态:
javascript
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
}特点:
- 可读可写
- 由组件自己控制
- 改变会触发重新渲染
- 是私有的
对比表格
| 特性 | Props | State |
|---|---|---|
| 可变性 | 只读 | 可变 |
| 所有者 | 父组件 | 当前组件 |
| 用途 | 组件间通信 | 组件内部状态 |
| 修改方式 | 父组件重新传递 | setState/useState |
| 触发渲染 | 父组件传递新值时 | 状态改变时 |
| 初始值来源 | 父组件 | 组件内部定义 |
示例对比
javascript
function Parent() {
const [value, setValue] = useState('')
return (
<div>
<input value={value} onChange={e => setValue(e.target.value)} />
<Child
value={value} // props:从父组件传递
onClear={() => setValue('')}
/>
</div>
)
}
function Child({ value, onClear }) {
const [localState, setLocalState] = useState('')
return (
<div>
<p>Props value: {value}</p>
<p>Local state: {localState}</p>
<button onClick={onClear}>Clear</button>
<button onClick={() => setLocalState('new')}>Update Local</button>
</div>
)
}设计原则
- Props 用于配置:从外部传入,控制组件行为
- State 用于交互:组件内部的可变状态
- 最小化 State:只保留必要的可变状态
- 状态提升:多个组件需要共享的状态提升到共同父组件
14. React 中 keys 、refs的作用是什么?
Keys 的作用
1. 帮助 React 识别元素
javascript
// 列表渲染
const items = ['Apple', 'Banana', 'Orange']
function FruitList() {
return (
<ul>
{items.map((item, index) => (
<li key={item}>{item}</li>
))}
</ul>
)
}2. 提高渲染性能
javascript
// 没有 key:React 无法识别元素,会重新创建所有节点
<li>A</li>
<li>B</li>
<li>C</li>
// 有 key:React 可以识别并复用节点
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>3. 保持组件状态
javascript
function List() {
const [items, setItems] = useState([
{ id: 1, text: 'A' },
{ id: 2, text: 'B' }
])
return items.map(item => <Item key={item.id} text={item.text} />)
}
// 如果使用 index 作为 key,当列表顺序改变时
// 组件状态可能会错乱Key 的选择原则
javascript
// ✅ 好:使用稳定的唯一 ID
items.map(item => <li key={item.id}>{item.name}</li>)
// ❌ 避免:使用 index(除非列表是静态的)
items.map((item, index) => <li key={index}>{item.name}</li>)
// ❌ 避免:使用不稳定的值(如随机数)
items.map(item => <li key={Math.random()}>{item.name}</li>)Refs 的作用
1. 访问 DOM 元素
javascript
function InputFocus() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
return <input ref={inputRef} />
}2. 访问类组件实例
javascript
class Child extends React.Component {
method() {
return 'Hello from child'
}
render() {
return <div>Child</div>
}
}
function Parent() {
const childRef = useRef(null)
const handleClick = () => {
console.log(childRef.current.method())
}
return (
<>
<Child ref={childRef} />
<button onClick={handleClick}>Call Child Method</button>
</>
)
}3. 存储可变值
javascript
function Timer() {
const intervalRef = useRef(null)
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('Tick')
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
return <div>Timer running</div>
}4. forwardRef 转发 Ref
javascript
const FancyButton = forwardRef((props, ref) => (
<button ref={ref} className='fancy'>
{props.children}
</button>
))
function App() {
const buttonRef = useRef(null)
return <FancyButton ref={buttonRef}>Click me</FancyButton>
}5. useImperativeHandle 自定义暴露的方法
javascript
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
clear: () => {
inputRef.current.value = ''
}
}))
return <input ref={inputRef} />
})Refs 使用场景
- 管理焦点、文本选择、媒体播放
- 触发动画
- 集成第三方 DOM 库
- 存储不触发渲染的值
注意事项
- 不要在渲染期间访问或修改 ref.current
- ref 改变不会触发重新渲染
- 函数组件默认不能接收 ref,需要 forwardRef
15. useState 是同步还是异步的?原理是什么,useState 做了什么
同步还是异步?
useState 的更新是异步的(在大多数情况下),但在某些场景下表现为同步。
异步场景(React 18 之前)
javascript
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
console.log(count) // 0,不是 1
}
return <button onClick={handleClick}>{count}</button>
}同步场景(React 18 之前)
javascript
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const element = document.getElementById('btn')
element.addEventListener('click', () => {
setCount(c => c + 1)
console.log(count) // React 18 之前可能是同步的
})
}, [])
return <button id='btn'>{count}</button>
}React 18+ 自动批处理
React 18 引入自动批处理,所有更新都是异步的:
javascript
function Counter() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
useEffect(() => {
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
// React 18 会自动批处理,只渲染一次
}, 1000)
}, [])
return <div>{count}</div>
}useState 原理
数据结构
React 使用链表来存储 Hooks:
javascript
// Fiber 节点上的 hooks 链表
fiber.memoizedState = {
memoizedState: 0, // 状态值
queue: {
// 更新队列
pending: null, // 待处理的更新
dispatch: null // dispatch 函数
},
next: null // 下一个 hook
}useState 执行过程
javascript
function useState(initialState) {
// 1. 获取当前 Fiber 节点
const fiber = currentlyRenderingFiber
// 2. 获取或创建 Hook
let hook = fiber.memoizedState
if (hook === null) {
hook = {
memoizedState: initialState,
queue: { pending: null },
next: null
}
fiber.memoizedState = hook
}
// 3. 处理更新队列
const queue = hook.queue
const pending = queue.pending
if (pending !== null) {
let newState = hook.memoizedState
let update = pending
do {
const action = update.action
newState = typeof action === 'function' ? action(newState) : action
update = update.next
} while (update !== pending)
hook.memoizedState = newState
queue.pending = null
}
// 4. 返回状态和更新函数
return [hook.memoizedState, dispatchAction.bind(null, queue)]
}dispatchAction(setState)
javascript
function dispatchAction(queue, action) {
// 1. 创建更新对象
const update = {
action,
next: null
}
// 2. 将更新加入队列(循环链表)
const pending = queue.pending
if (pending === null) {
update.next = update
} else {
update.next = pending.next
pending.next = update
}
queue.pending = update
// 3. 调度更新
scheduleUpdateOnFiber(fiber)
}useState 做了什么
首次渲染:
- 创建 Hook 对象
- 初始化状态值
- 将 Hook 加入链表
更新渲染:
- 从链表中取出 Hook
- 处理更新队列
- 计算新状态
返回值:
- 返回当前状态值
- 返回 dispatch 函数(setState)
为什么是异步的
- 性能优化:批量更新,减少渲染次数
- 一致性:保证渲染结果与状态一致
- 可预测性:避免中间状态
获取最新值的方法
javascript
function Counter() {
const [count, setCount] = useState(0)
// 方法1:使用函数形式更新
const handleClick = () => {
setCount(prev => {
console.log(prev) // 最新值
return prev + 1
})
}
// 方法2:使用 useRef 保存最新值
const countRef = useRef(count)
countRef.current = count
const handleClick2 = () => {
setCount(c => c + 1)
console.log(countRef.current) // 最新值
}
return <button onClick={handleClick}>{count}</button>
}16. useEffect 作用和原理是什么,有哪些使用场景?
useEffect 作用
useEffect 用于在函数组件中执行副作用操作,相当于类组件的生命周期组合:
- componentDidMount
- componentDidUpdate
- componentWillUnmount
基本用法
javascript
function Component() {
const [count, setCount] = useState(0)
// 每次渲染后执行
useEffect(() => {
console.log('Render')
})
// 只在挂载时执行
useEffect(() => {
console.log('Mounted')
}, [])
// 依赖变化时执行
useEffect(() => {
console.log('Count changed:', count)
}, [count])
// 清理函数
useEffect(() => {
const timer = setInterval(() => {
console.log('Tick')
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
}useEffect 原理
数据结构
javascript
// Hook 对象
{
memoizedState: {
create: () => void, // 副作用函数
destroy: () => void, // 清理函数
deps: Array, // 依赖数组
next: Effect // 下一个 effect
}
}执行流程
javascript
function useEffect(create, deps) {
// 1. 获取当前 Hook
const hook = updateWorkInProgressHook()
// 2. 检查依赖是否变化
const nextDeps = deps === undefined ? null : deps
let destroy = undefined
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState
destroy = prevEffect.destroy
if (nextDeps !== null) {
const prevDeps = prevEffect.deps
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖没变化,跳过
return
}
}
}
// 3. 创建 Effect 对象
const effect = {
create,
destroy,
deps: nextDeps,
next: null
}
// 4. 加入 Effect 链表
hook.memoizedState = effect
// 5. 加入副作用队列
currentlyRenderingFiber.updateQueue = effect
}Effect 执行时机
javascript
// 渲染阶段:不执行 effect,只是收集
function renderWithHooks() {
// 执行组件函数
// useEffect 被调用,effect 被收集
}
// 提交阶段:执行 effect
function commitRoot() {
// 1. 执行 DOM 操作
commitMutationEffects()
// 2. 执行 layout effects(同步)
commitLayoutEffects()
// 3. 调度 effect 执行(异步)
scheduleCallback(() => {
commitPassiveEffects()
})
}
function commitPassiveEffects() {
// 执行清理函数
effect.destroy()
// 执行副作用函数
const destroy = effect.create()
effect.destroy = destroy
}使用场景
1. 数据获取
javascript
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
let ignore = false
async function fetchUser() {
const response = await fetch(`/api/users/${userId}`)
const data = await response.json()
if (!ignore) {
setUser(data)
}
}
fetchUser()
return () => {
ignore = true
}
}, [userId])
return user ? <div>{user.name}</div> : <div>Loading...</div>
}2. 事件监听
javascript
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return (
<div>
{size.width} x {size.height}
</div>
)
}3. 定时器
javascript
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
return <div>{seconds} seconds</div>
}4. 订阅
javascript
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([])
useEffect(() => {
const connection = createConnection(roomId)
connection.connect()
connection.on('message', message => {
setMessages(msgs => [...msgs, message])
})
return () => {
connection.disconnect()
}
}, [roomId])
return (
<ul>
{messages.map(msg => (
<li key={msg.id}>{msg.text}</li>
))}
</ul>
)
}5. DOM 操作
javascript
function InputFocus() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
return <input ref={inputRef} />
}6. 依赖变化时的操作
javascript
function SearchResults({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
if (query) {
search(query).then(setResults)
}
}, [query])
return (
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
)
}注意事项
- 不要在循环、条件或嵌套函数中调用
- 依赖数组要完整
- 清理函数用于取消订阅、清除定时器等
- Effect 在渲染后异步执行
17. UseEffect 依赖为空数组与 componentDidMount 区别
相似之处
两者都在组件首次挂载后执行:
javascript
// 类组件
class Component extends React.Component {
componentDidMount() {
console.log('Mounted')
}
}
// 函数组件
function Component() {
useEffect(() => {
console.log('Mounted')
}, [])
}主要区别
1. 执行时机不同
javascript
// componentDidMount:DOM 更新后同步执行
class Component extends React.Component {
componentDidMount() {
console.log('componentDidMount')
console.log('DOM:', document.getElementById('myDiv'))
// DOM 已经更新,可以同步访问
}
}
// useEffect:DOM 更新后异步执行
function Component() {
useEffect(() => {
console.log('useEffect')
console.log('DOM:', document.getElementById('myDiv'))
// DOM 已经更新,但 effect 是异步执行的
}, [])
return <div id='myDiv'>Hello</div>
}2. 与 useLayoutEffect 的关系
javascript
// useLayoutEffect 更接近 componentDidMount
function Component() {
useLayoutEffect(() => {
// 同步执行,在 DOM 变更后同步调用
// 阻塞浏览器绘制
console.log('useLayoutEffect')
}, [])
useEffect(() => {
// 异步执行,不阻塞绘制
console.log('useEffect')
}, [])
return <div>Content</div>
}
// 执行顺序:
// 1. DOM 变更
// 2. useLayoutEffect 执行
// 3. 浏览器绘制
// 4. useEffect 执行3. 清理函数的时机
javascript
// 类组件
class Component extends React.Component {
componentDidMount() {
this.timer = setInterval(() => {}, 1000)
}
componentWillUnmount() {
clearInterval(this.timer)
}
}
// 函数组件
function Component() {
useEffect(() => {
const timer = setInterval(() => {}, 1000)
return () => clearInterval(timer)
}, [])
}4. 访问更新后的状态
javascript
// 类组件:可以立即获取更新后的状态
class Component extends React.Component {
state = { count: 0 }
componentDidMount() {
this.setState({ count: 1 })
console.log(this.state.count) // 1(同步更新)
}
}
// 函数组件:状态更新是异步的
function Component() {
const [count, setCount] = useState(0)
useEffect(() => {
setCount(1)
console.log(count) // 0(异步更新)
}, [])
}详细对比表
| 特性 | componentDidMount | useEffect([]) | useLayoutEffect([]) |
|---|---|---|---|
| 执行时机 | DOM 更新后同步 | DOM 更新后异步 | DOM 更新后同步 |
| 阻塞绘制 | 是 | 否 | 是 |
| 访问 DOM | 同步 | 异步 | 同步 |
| 清理函数 | componentWillUnmount | return 函数 | return 函数 |
| 状态更新 | 可能同步 | 异步 | 异步 |
实际影响
javascript
// 场景:测量 DOM 元素尺寸
function Measure() {
const ref = useRef(null)
const [height, setHeight] = useState(0)
// ❌ useEffect 可能导致闪烁
useEffect(() => {
setHeight(ref.current.offsetHeight)
}, [])
// ✅ useLayoutEffect 避免闪烁
useLayoutEffect(() => {
setHeight(ref.current.offsetHeight)
}, [])
return (
<div ref={ref} style={{ height: height || 'auto' }}>
Content
</div>
)
}总结
useEffect([])是异步执行,不阻塞浏览器绘制componentDidMount是同步执行,阻塞浏览器绘制- 如果需要同步访问 DOM 或避免视觉闪烁,使用
useLayoutEffect - 大多数情况下,
useEffect是更好的选择
18. React hooks 解决了什么问题?函数组件与类组件的区别
Hooks 解决的问题
1. 状态逻辑复用
类组件的问题:
javascript
// 类组件:需要高阶组件或渲染属性来复用逻辑
function withSubscription(WrappedComponent) {
return class extends React.Component {
state = { data: null }
componentDidMount() {
DataSource.addChangeListener(this.handleChange)
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange)
}
handleChange = () => {
this.setState({ data: DataSource.comments })
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}Hooks 的解决方案:
javascript
// 自定义 Hook:简单直接
function useSubscription() {
const [data, setData] = useState(null)
useEffect(() => {
const handleChange = () => {
setData(DataSource.comments)
}
DataSource.addChangeListener(handleChange)
return () => DataSource.removeChangeListener(handleChange)
}, [])
return data
}
function Component() {
const data = useSubscription()
return <div>{data}</div>
}2. 复杂组件难以理解
类组件的问题:
javascript
class FriendStatus extends React.Component {
state = { count: 0, isOnline: null }
componentDidMount() {
document.title = `Count: ${this.state.count}`
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
)
}
componentDidUpdate() {
document.title = `Count: ${this.state.count}`
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
)
}
handleStatusChange = status => {
this.setState({ isOnline: status.isOnline })
}
render() {
// ...
}
}Hooks 的解决方案:
javascript
function FriendStatus({ friend }) {
const [count, setCount] = useState(0)
const [isOnline, setIsOnline] = useState(null)
// 相关逻辑组织在一起
useEffect(() => {
document.title = `Count: ${count}`
}, [count])
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(friend.id, handleStatusChange)
return () => {
ChatAPI.unsubscribeFromFriendStatus(friend.id, handleStatusChange)
}
}, [friend.id])
// ...
}3. this 的困扰
类组件的问题:
javascript
class Button extends React.Component {
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
console.log(this) // 需要绑定
}
render() {
return <button onClick={this.handleClick}>Click</button>
}
}Hooks 的解决方案:
javascript
function Button() {
const handleClick = () => {
console.log(this) // 不需要 this
}
return <button onClick={onClick}>Click</button>
}函数组件与类组件的区别
1. 定义方式
javascript
// 类组件
class ClassComponent extends React.Component {
state = { count: 0 }
render() {
return <div>{this.state.count}</div>
}
}
// 函数组件
function FunctionComponent() {
const [count, setCount] = useState(0)
return <div>{count}</div>
}2. this 的使用
javascript
// 类组件:需要 this
class ClassComponent extends React.Component {
state = { count: 0 }
handleClick = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
return <button onClick={this.handleClick}>{this.state.count}</button>
}
}
// 函数组件:不需要 this
function FunctionComponent() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}3. 生命周期
javascript
// 类组件:生命周期方法
class ClassComponent extends React.Component {
componentDidMount() {
console.log('Mounted')
}
componentDidUpdate() {
console.log('Updated')
}
componentWillUnmount() {
console.log('Unmounted')
}
render() {
return <div>Content</div>
}
}
// 函数组件:Hooks
function FunctionComponent() {
useEffect(() => {
console.log('Mounted')
return () => {
console.log('Unmounted')
}
}, [])
useEffect(() => {
console.log('Updated')
})
return <div>Content</div>
}4. 状态管理
javascript
// 类组件:state 是对象,setState 合并更新
class ClassComponent extends React.Component {
state = { a: 1, b: 2 }
updateA = () => {
this.setState({ a: 3 }) // b 保持不变
}
}
// 函数组件:state 可以是任何值,setState 替换更新
function FunctionComponent() {
const [a, setA] = useState(1)
const [b, setB] = useState(2)
const updateA = () => {
setA(3)
}
}5. 性能
javascript
// 类组件:需要实例化
const instance = new ClassComponent(props)
// 函数组件:直接调用
const result = FunctionComponent(props)函数组件:
- 不需要实例化,内存占用更小
- 不需要处理 this 绑定
- 更容易进行优化(如内联)
6. 闭包特性
javascript
// 类组件:this 指向最新的实例
class ClassComponent extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user)
}
handleClick = () => {
setTimeout(this.showMessage, 3000)
}
render() {
return <button onClick={this.handleClick}>Follow</button>
}
}
// 函数组件:闭包捕获当时的值
function FunctionComponent({ user }) {
const showMessage = () => {
alert('Followed ' + user)
}
const handleClick = () => {
setTimeout(showMessage, 3000)
}
return <button onClick={handleClick}>Follow</button>
}对比总结
| 特性 | 类组件 | 函数组件 |
|---|---|---|
| 定义方式 | class extends | function |
| this | 需要 | 不需要 |
| 状态 | state 对象 | useState |
| 生命周期 | 方法 | useEffect |
| 代码量 | 较多 | 较少 |
| 学习成本 | 较高 | 较低 |
| 性能 | 一般 | 较好 |
| Hooks | 不支持 | 支持 |
| 实例化 | 需要 | 不需要 |
| 闭包 | this 指向最新 | 捕获渲染时的值 |
19. 简述下 React 的事件代理机制?
什么是事件代理
React 不会将事件处理函数直接绑定到真实的 DOM 节点上,而是使用事件委托(Event Delegation)的方式,将所有事件统一绑定到根容器上(React 17 之前是 document,之后是 React 树的根 DOM 容器)。
事件代理原理
javascript
// 原生事件:每个元素都绑定事件处理函数
document.getElementById('button1').addEventListener('click', handler1)
document.getElementById('button2').addEventListener('click', handler2)
document.getElementById('button3').addEventListener('click', handler3)
// React 事件代理:统一绑定到根容器
rootContainer.addEventListener('click', event => {
// 根据事件目标找到对应的组件和处理函数
const fiberNode = getClosestInstanceFromNode(event.target)
const onClick = fiberNode.memoizedProps.onClick
onClick(event)
})React 事件系统架构
用户点击 → 原生事件触发 → 根容器捕获 → 合成事件 → 事件分发 → 执行处理函数核心实现
1. 事件注册
javascript
// React 在初始化时注册所有支持的事件类型
const allNativeEvents = new Set([
'click', 'change', 'input', 'keydown', 'keyup', 'focus', 'blur', ...
]);
// 为每个事件类型注册监听器
allNativeEvents.forEach(eventType => {
listenToNativeEvent(eventType, rootContainer);
});2. 事件监听
javascript
function listenToNativeEvent(eventType, rootContainer) {
// 捕获阶段监听
rootContainer.addEventListener(eventType, dispatchEvent, true)
// 冒泡阶段监听
rootContainer.addEventListener(eventType, dispatchEvent, false)
}3. 事件分发
javascript
function dispatchEvent(nativeEvent) {
// 1. 创建合成事件对象
const syntheticEvent = createSyntheticEvent(nativeEvent)
// 2. 从事件目标向上遍历,收集所有处理函数
const dispatchQueue = []
let fiberNode = getClosestInstanceFromNode(nativeEvent.target)
while (fiberNode) {
const onClick = fiberNode.memoizedProps.onClick
if (onClick) {
dispatchQueue.push({
instance: fiberNode,
listener: onClick
})
}
fiberNode = fiberNode.return
}
// 3. 执行处理函数
dispatchQueue.forEach(({ listener }) => {
listener(syntheticEvent)
})
}合成事件
React 创建了自己的事件对象——合成事件(Synthetic Event):
javascript
const syntheticEvent = {
nativeEvent, // 原生事件对象
type: nativeEvent.type,
target: nativeEvent.target,
currentTarget: currentTarget,
preventDefault() {
nativeEvent.preventDefault()
},
stopPropagation() {
nativeEvent.stopPropagation()
this.isPropagationStopped = true
},
isPropagationStopped: false,
persist() {
// 异步访问事件时需要调用
}
}事件代理的优势
1. 减少内存占用
javascript
// 原生方式:100 个按钮,100 个事件监听器
buttons.forEach(btn => {
btn.addEventListener('click', handler)
})
// React 方式:100 个按钮,1 个事件监听器
rootContainer.addEventListener('click', dispatchEvent)2. 统一管理
javascript
// 所有事件处理逻辑集中管理
// 方便实现事件优先级、批量更新等3. 跨浏览器兼容
javascript
// React 统一处理浏览器差异
// 开发者不需要关心兼容性问题4. 方便清理
javascript
// 组件卸载时不需要手动移除事件监听
// React 统一管理React 17 的变化
React 17 将事件委托的根容器从 document 改为 React 树的根 DOM 容器:
javascript
// React 16 及之前
document.addEventListener('click', dispatchEvent)
// React 17+
rootContainer.addEventListener('click', dispatchEvent)原因:
- 多个 React 版本共存时不会冲突
- 与其他框架集成更友好
- 事件冒泡行为更符合预期
20. 说说 React 事件和原生事件的执行顺序
执行顺序
- 原生事件捕获阶段
- React 事件捕获阶段
- React 事件冒泡阶段
- 原生事件冒泡阶段
示例演示
javascript
function App() {
useEffect(() => {
// 原生事件
document
.getElementById('parent')
.addEventListener(
'click',
() => console.log('原生事件 - 父元素捕获'),
true
)
document
.getElementById('parent')
.addEventListener(
'click',
() => console.log('原生事件 - 父元素冒泡'),
false
)
document
.getElementById('child')
.addEventListener(
'click',
() => console.log('原生事件 - 子元素捕获'),
true
)
document
.getElementById('child')
.addEventListener(
'click',
() => console.log('原生事件 - 子元素冒泡'),
false
)
}, [])
const handleParentCapture = () => console.log('React 事件 - 父元素捕获')
const handleParentBubble = () => console.log('React 事件 - 父元素冒泡')
const handleChildCapture = () => console.log('React 事件 - 子元素捕获')
const handleChildBubble = () => console.log('React 事件 - 子元素冒泡')
return (
<div
id='parent'
onClickCapture={handleParentCapture}
onClick={handleParentBubble}
>
<div
id='child'
onClickCapture={handleChildCapture}
onClick={handleChildBubble}
>
Click me
</div>
</div>
)
}
// 点击子元素时的输出顺序:
// 原生事件 - 父元素捕获
// 原生事件 - 子元素捕获
// React 事件 - 父元素捕获
// React 事件 - 子元素捕获
// React 事件 - 子元素冒泡
// React 事件 - 父元素冒泡
// 原生事件 - 子元素冒泡
// 原生事件 - 父元素冒泡React 17+ 的变化
React 17 之前,事件委托到 document:
原生捕获 → React 捕获 → React 冒泡 → 原生冒泡React 17+,事件委托到 React 根容器:
javascript
// 如果原生事件绑定在 React 根容器外部
// 执行顺序不变
// 如果原生事件绑定在 React 根容器内部
// 可能会改变执行顺序stopPropagation 的影响
javascript
function Example() {
useEffect(() => {
document.addEventListener('click', () => {
console.log('Document click')
})
}, [])
const handleClick = e => {
e.stopPropagation()
console.log('React click')
}
return <button onClick={handleClick}>Click</button>
}
// 点击按钮:
// React click
// Document click 仍然会执行!原因:React 事件和原生事件是两个独立的事件流,e.stopPropagation() 只能阻止当前事件流。
阻止原生事件
javascript
function Example() {
useEffect(() => {
document.addEventListener(
'click',
e => {
e.stopPropagation()
console.log('Document click')
},
true
) // 捕获阶段
}, [])
const handleClick = () => {
console.log('React click')
}
return <button onClick={handleClick}>Click</button>
}
// 点击按钮:
// Document click
// React click 不会执行!最佳实践
- 避免混用 React 事件和原生事件
- 如果必须混用,注意执行顺序
- 使用
e.nativeEvent.stopImmediatePropagation()阻止所有事件
javascript
const handleClick = e => {
e.nativeEvent.stopImmediatePropagation()
console.log('React click')
}21. React 事件机制和原生 DOM 事件流有什么区别
原生 DOM 事件流
捕获阶段 → 目标阶段 → 冒泡阶段javascript
document.addEventListener(
'click',
() => {
console.log('Document capture')
},
true
)
element.addEventListener(
'click',
() => {
console.log('Element capture')
},
true
)
element.addEventListener(
'click',
() => {
console.log('Element bubble')
},
false
)
document.addEventListener(
'click',
() => {
console.log('Document bubble')
},
false
)
// 点击 element 时:
// Document capture → Element capture → Element bubble → Document bubbleReact 事件机制特点
1. 事件委托
javascript
// 原生:每个元素都绑定事件
element.addEventListener('click', handler)
// React:统一委托到根容器
rootContainer.addEventListener('click', dispatchEvent)2. 合成事件
javascript
// 原生事件
element.addEventListener('click', (e) => {
console.log(e); // 原生事件对象
});
// React 合成事件
<button onClick={(e) => {
console.log(e); // 合成事件对象
console.log(e.nativeEvent); // 原生事件对象
}}>3. 事件池
React 17 之前使用事件池:
javascript
// React 16 及之前
function handleClick(e) {
// e 是从事件池中取出的
// 事件回调执行后,e 会被回收
console.log(e.type) // OK
// 异步访问需要 persist
setTimeout(() => {
console.log(e.type) // 可能报错
}, 100)
}
// 解决方案
function handleClick(e) {
e.persist() // 保留事件对象
setTimeout(() => {
console.log(e.type) // OK
}, 100)
}
// React 17+ 移除了事件池,不再需要 persist主要区别对比
| 特性 | 原生事件 | React 事件 |
|---|---|---|
| 绑定方式 | 直接绑定到元素 | 委托到根容器 |
| 事件对象 | 原生 Event | 合成事件 |
| 事件池 | 无 | React 17 前有 |
| 跨浏览器 | 需要处理兼容 | React 统一处理 |
| 阻止冒泡 | stopPropagation | stopPropagation |
| 阻止默认行为 | preventDefault | preventDefault |
总结
React 事件机制是对原生事件的封装和优化,提供了更好的跨浏览器兼容性和性能优化。
22. 什么是高阶组件(HOC)?如何使用?
什么是高阶组件
高阶组件(Higher-Order Component,HOC)是一个函数,它接收一个组件并返回一个新组件。它是 React 中复用组件逻辑的一种高级技术。
javascript
const EnhancedComponent = higherOrderComponent(WrappedComponent)基本实现
javascript
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>
}
return <WrappedComponent {...props} />
}
}使用场景
1. 复用逻辑
javascript
function withSubscription(WrappedComponent, selectData) {
return class extends React.Component {
constructor(props) {
super(props)
this.state = {
data: selectData(DataSource, props)
}
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange)
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange)
}
handleChange = () => {
this.setState({
data: selectData(DataSource, this.props)
})
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}2. 权限控制
javascript
function withAuth(WrappedComponent) {
return function WithAuthComponent(props) {
const { user } = useAuth()
if (!user) {
return <Redirect to='/login' />
}
return <WrappedComponent {...props} user={user} />
}
}3. 日志记录
javascript
function withLogging(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
console.log('Component mounted:', WrappedComponent.name)
}
render() {
return <WrappedComponent {...this.props} />
}
}
}约定和最佳实践
1. 不要在 render 方法中使用 HOC
javascript
// ❌ 错误
function Component() {
const EnhancedComponent = withLoading(Button)
return <EnhancedComponent />
}
// ✅ 正确
const EnhancedButton = withLoading(Button)
function Component() {
return <EnhancedButton />
}2. 静态方法必须复制
javascript
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {
// ...
}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`
return WithSubscription
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}3. Refs 不会被传递
javascript
function withLogging(WrappedComponent) {
class WithLogging extends React.Component {
render() {
const { forwardRef, ...rest } = this.props
return <WrappedComponent ref={forwardRef} {...rest} />
}
}
return React.forwardRef((props, ref) => {
return <WithLogging {...props} forwardRef={ref} />
})
}HOC 与 Hooks 的对比
javascript
// HOC 方式
const EnhancedComponent = withSubscription(Component)
// Hooks 方式
function Component() {
const data = useSubscription()
return <div>{data}</div>
}Hooks 通常更简洁,推荐优先使用 Hooks。
23. 什么受控组件和非受控组件他们有什么区别以及应用场景?
受控组件
受控组件是指表单元素的值由 React state 控制的组件:
javascript
function ControlledInput() {
const [value, setValue] = useState('')
const handleChange = e => {
setValue(e.target.value)
}
const handleSubmit = () => {
console.log('Submitted value:', value)
}
return (
<form onSubmit={handleSubmit}>
<input value={value} onChange={handleChange} />
<button type='submit'>Submit</button>
</form>
)
}特点:
- 表单值由 React state 管理
- 每次输入都会触发 state 更新
- 可以对输入进行验证和处理
非受控组件
非受控组件是指表单元素的值由 DOM 自身管理的组件:
javascript
function UncontrolledInput() {
const inputRef = useRef()
const handleSubmit = e => {
e.preventDefault()
console.log('Submitted value:', inputRef.current.value)
}
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue='initial value' />
<button type='submit'>Submit</button>
</form>
)
}特点:
- 表单值由 DOM 管理
- 使用 ref 获取表单值
- 使用 defaultValue 设置初始值
区别对比
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 值的管理 | React state | DOM |
| 数据获取 | state | ref |
| 初始值 | value | defaultValue |
| 实时验证 | 支持 | 不支持 |
| 即时反馈 | 支持 | 不支持 |
| 代码量 | 较多 | 较少 |
应用场景
受控组件适用场景
javascript
// 1. 实时验证
function ValidatedInput() {
const [value, setValue] = useState('')
const [error, setError] = useState('')
const handleChange = e => {
const value = e.target.value
setValue(value)
setError(value.length < 3 ? 'Too short' : '')
}
return (
<div>
<input value={value} onChange={handleChange} />
{error && <span>{error}</span>}
</div>
)
}
// 2. 条件禁用提交按钮
function Form() {
const [value, setValue] = useState('')
return (
<form>
<input value={value} onChange={e => setValue(e.target.value)} />
<button disabled={!value}>Submit</button>
</form>
)
}
// 3. 格式化输入
function PhoneInput() {
const [phone, setPhone] = useState('')
const handleChange = e => {
const value = e.target.value.replace(/\D/g, '')
setPhone(value)
}
return <input value={phone} onChange={handleChange} />
}非受控组件适用场景
javascript
// 1. 文件上传
function FileInput() {
const fileRef = useRef()
const handleSubmit = () => {
const file = fileRef.current.files[0]
}
return (
<form onSubmit={handleSubmit}>
<input type='file' ref={fileRef} />
<button type='submit'>Upload</button>
</form>
)
}
// 2. 简单表单
function SimpleForm() {
const nameRef = useRef()
const emailRef = useRef()
const handleSubmit = () => {
console.log({
name: nameRef.current.value,
email: emailRef.current.value
})
}
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} />
<input ref={emailRef} />
<button type='submit'>Submit</button>
</form>
)
}总结
- 大多数情况下推荐使用受控组件
- 文件上传必须使用非受控组件
- 简单表单可以使用非受控组件减少代码量
24. 为什么不能在循环、条件或嵌套函数中调用 Hooks?
原因
React Hooks 依赖于调用顺序来正确关联 state 和 Hook。每次渲染时,Hooks 必须以相同的顺序被调用。
Hooks 的实现原理
javascript
// React 内部使用链表存储 Hooks
let firstHook = null
let currentHook = null
function useState(initialState) {
const hook = currentHook || {
memoizedState: initialState,
next: null
}
currentHook = hook.next
return [hook.memoizedState, dispatchAction]
}错误示例
javascript
// ❌ 错误:在条件语句中调用
function Component({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null)
}
const [count, setCount] = useState(0)
}
// ❌ 错误:在循环中调用
function Component({ items }) {
items.forEach(item => {
const [state, setState] = useState(item)
})
}
// ❌ 错误:在嵌套函数中调用
function Component() {
useEffect(() => {
const [state, setState] = useState(0)
})
}问题分析
javascript
// 第一次渲染(isLoggedIn = true)
// Hook 1: user
// Hook 2: count
// 第二次渲染(isLoggedIn = false)
// Hook 1: count(实际上是 user 的位置)
// 导致 state 错乱正确做法
javascript
// ✅ 正确:在顶层调用
function Component({ isLoggedIn }) {
const [user, setUser] = useState(null)
const [count, setCount] = useState(0)
if (isLoggedIn) {
// 使用 user
}
}
// ✅ 正确:循环中使用数组
function Component({ items }) {
const [states, setStates] = useState(items)
}
// ✅ 正确:提取为自定义 Hook
function useCustomHook() {
const [state, setState] = useState(0)
return [state, setState]
}ESLint 规则
React 提供了 ESLint 规则来帮助检测这个问题:
javascript
// 安装
npm install eslint-plugin-react-hooks --save-dev
// .eslintrc
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}25. 你常用的 React Hooks 有哪些以及他们的作用是什么?
基础 Hooks
1. useState
状态管理:
javascript
const [state, setState] = useState(initialState)2. useEffect
副作用处理:
javascript
useEffect(() => {
return () => {}
}, [dependencies])3. useContext
访问 Context:
javascript
const value = useContext(MyContext)额外 Hooks
4. useReducer
复杂状态管理:
javascript
const [state, dispatch] = useReducer(reducer, initialState)
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
default:
return state
}
}5. useCallback
缓存回调函数:
javascript
const memoizedCallback = useCallback(() => {
doSomething(a, b)
}, [a, b])6. useMemo
缓存计算结果:
javascript
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])7. useRef
访问 DOM 或存储可变值:
javascript
const ref = useRef(initialValue)8. useImperativeHandle
自定义暴露给父组件的实例值:
javascript
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus()
}))9. useLayoutEffect
同步执行副作用:
javascript
useLayoutEffect(() => {
// DOM 更新后同步执行
}, [])10. useDebugValue
自定义 Hook 的调试标签:
javascript
useDebugValue(isOnline ? 'Online' : 'Offline')常用自定义 Hooks
useDebounce
javascript
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}useLocalStorage
javascript
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = value => {
setStoredValue(value)
localStorage.setItem(key, JSON.stringify(value))
}
return [storedValue, setValue]
}useWindowSize
javascript
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}26. useEffect 和 useLayoutEffect 的区别是什么?
执行时机不同
javascript
function Component() {
useEffect(() => {
console.log('useEffect')
})
useLayoutEffect(() => {
console.log('useLayoutEffect')
})
return <div>Content</div>
}
// 执行顺序:
// 1. DOM 变更
// 2. useLayoutEffect 执行(同步)
// 3. 浏览器绘制
// 4. useEffect 执行(异步)useLayoutEffect
- 在 DOM 更新后同步执行
- 阻塞浏览器绘制
- 适合需要同步读取 DOM 的场景
javascript
function Measure() {
const ref = useRef()
const [height, setHeight] = useState(0)
useLayoutEffect(() => {
setHeight(ref.current.offsetHeight)
}, [])
return (
<div ref={ref} style={{ height: height || 'auto' }}>
Content
</div>
)
}useEffect
- 在 DOM 更新后异步执行
- 不阻塞浏览器绘制
- 适合大多数副作用场景
javascript
function Component() {
useEffect(() => {
fetch('/api/data').then(setData)
}, [])
}对比表
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 异步 | 同步 |
| 阻塞绘制 | 否 | 是 |
| 使用场景 | 大多数副作用 | DOM 测量 |
| 性能影响 | 小 | 可能影响 |
27. React.memo、useMemo 和 useCallback 作用和区别以及什么场景下使用它们?
React.memo
用于组件级别的缓存:
javascript
const MemoComponent = React.memo(function Component({ name }) {
console.log('Render')
return <div>{name}</div>
})
// 自定义比较函数
const MemoComponent = React.memo(
function Component({ user }) {
return <div>{user.name}</div>
},
(prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id
}
)useMemo
缓存计算结果:
javascript
function Component({ items, filter }) {
const filteredItems = useMemo(() => {
console.log('Computing...')
return items.filter(item => item.includes(filter))
}, [items, filter])
return (
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
)
}useCallback
缓存函数引用:
javascript
function Parent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
console.log('Clicked')
}, [])
return <Child onClick={handleClick} />
}
const Child = React.memo(function Child({ onClick }) {
console.log('Child render')
return <button onClick={onClick}>Click</button>
})区别对比
| 特性 | React.memo | useMemo | useCallback |
|---|---|---|---|
| 缓存类型 | 组件 | 值 | 函数 |
| 作用对象 | 整个组件 | 计算结果 | 函数引用 |
| 触发条件 | props 变化 | 依赖变化 | 依赖变化 |
使用场景
javascript
// React.memo:组件渲染开销大
const ExpensiveComponent = React.memo(function Component({ data }) {
// 复杂计算
})
// useMemo:计算开销大
const result = useMemo(() => heavyComputation(data), [data])
// useCallback:传递给子组件的回调
const handleClick = useCallback(() => {
doSomething(id)
}, [id])28. 说说你对 useReducer、useContext 的理解
useReducer
用于复杂状态管理:
javascript
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 }
case 'reset':
return initialState
default:
return state
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
)
}useContext
用于跨层级数据传递:
javascript
const ThemeContext = createContext('light')
function App() {
return (
<ThemeContext.Provider value='dark'>
<Toolbar />
</ThemeContext.Provider>
)
}
function Toolbar() {
return <ThemedButton />
}
function ThemedButton() {
const theme = useContext(ThemeContext)
return <button className={theme}>Themed Button</button>
}组合使用
javascript
const StateContext = createContext()
const DispatchContext = createContext()
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
<Components />
</DispatchContext.Provider>
</StateContext.Provider>
)
}
function useAppState() {
return useContext(StateContext)
}
function useAppDispatch() {
return useContext(DispatchContext)
}29. 如何避免使用 context 的时候,引起整个挂载节点树的重新渲染
问题
javascript
const UserContext = createContext()
function App() {
const [user, setUser] = useState({ name: 'John', age: 20 })
return (
<UserContext.Provider value={user}>
<Child />
</UserContext.Provider>
)
}
// user 变化时,所有消费组件都会重新渲染解决方案
1. 拆分 Context
javascript
const NameContext = createContext()
const AgeContext = createContext()
function App() {
const [user, setUser] = useState({ name: 'John', age: 20 })
return (
<NameContext.Provider value={user.name}>
<AgeContext.Provider value={user.age}>
<Child />
</AgeContext.Provider>
</NameContext.Provider>
)
}2. 使用 useMemo
javascript
function App() {
const [user, setUser] = useState({ name: 'John', age: 20 })
const value = useMemo(
() => ({
name: user.name,
age: user.age
}),
[user.name, user.age]
)
return (
<UserContext.Provider value={value}>
<Child />
</UserContext.Provider>
)
}3. 拆分 state 和 dispatch
javascript
const StateContext = createContext()
const DispatchContext = createContext()
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
<Child />
</DispatchContext.Provider>
</StateContext.Provider>
)
}
// dispatch 不会变化,不会触发重新渲染4. 使用选择器模式
javascript
function useContextSelector(context, selector) {
const value = useContext(context)
return useMemo(() => selector(value), [value, selector])
}
function Child() {
const name = useContextSelector(UserContext, state => state.name)
return <div>{name}</div>
}30. useRef /ref/forwardsRef 的区别是什么?
useRef
用于函数组件内部创建 ref:
javascript
function Component() {
const inputRef = useRef(null)
const countRef = useRef(0)
useEffect(() => {
inputRef.current.focus()
}, [])
return <input ref={inputRef} />
}ref
用于访问 DOM 元素或类组件实例:
javascript
// 访问 DOM
function Component() {
const divRef = useRef()
return <div ref={divRef}>Content</div>
}
// 访问类组件实例
class Child extends React.Component {
method() {
return 'Hello'
}
render() {
return <div>Child</div>
}
}
function Parent() {
const childRef = useRef()
const handleClick = () => {
console.log(childRef.current.method())
}
return <Child ref={childRef} />
}forwardRef
用于转发 ref 给子组件:
javascript
const FancyButton = forwardRef((props, ref) => (
<button ref={ref} className='fancy'>
{props.children}
</button>
))
function App() {
const buttonRef = useRef()
return <FancyButton ref={buttonRef}>Click</FancyButton>
}useImperativeHandle
自定义暴露给父组件的方法:
javascript
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
clear: () => {
inputRef.current.value = ''
}
}))
return <input ref={inputRef} />
})区别总结
| 特性 | 用途 | 使用场景 |
|---|---|---|
| useRef | 创建 ref | 函数组件内部 |
| ref | 访问 DOM/实例 | JSX 属性 |
| forwardRef | 转发 ref | 函数组件需要接收 ref |
31. 说说你对 React Hook 的闭包陷阱的理解,有哪些解决方案?
什么是闭包陷阱
闭包陷阱是指 Hook 中的闭包捕获了过时的状态值:
javascript
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log(count) // 始终是 0
}, 1000)
return () => clearInterval(timer)
}, [])
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}解决方案
1. 使用函数式更新
javascript
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1) // 使用函数形式
}, 1000)
return () => clearInterval(timer)
}, [])2. 添加依赖项
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log(count)
}, 1000)
return () => clearInterval(timer)
}, [count]) // 添加依赖3. 使用 useRef
javascript
function Counter() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
countRef.current = count
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current) // 始终是最新的
}, 1000)
return () => clearInterval(timer)
}, [])
}4. 使用 useReducer
javascript
function Counter() {
const [state, dispatch] = useReducer(
(state, action) => {
if (action.type === 'increment') {
return { count: state.count + 1 }
}
return state
},
{ count: 0 }
)
useEffect(() => {
const timer = setInterval(() => {
dispatch({ type: 'increment' })
}, 1000)
return () => clearInterval(timer)
}, [])
}32. useEffect 的第二个参数,传空数组和传依赖数组有什么区别
空数组 []
只在组件挂载和卸载时执行:
javascript
useEffect(() => {
console.log('Mounted')
return () => {
console.log('Unmounted')
}
}, []) // 只执行一次依赖数组 [dep]
依赖变化时执行:
javascript
useEffect(() => {
console.log('count changed:', count)
}, [count]) // count 变化时执行无第二个参数
每次渲染都执行:
javascript
useEffect(() => {
console.log('Render')
}) // 每次渲染都执行对比表
| 参数 | 执行时机 | 使用场景 |
|---|---|---|
| 无 | 每次渲染 | 很少使用 |
| [] | 挂载/卸载 | 初始化、清理 |
| [dep] | 依赖变化 | 响应数据变化 |
33. 说说对 React 中 Element、Component、Node、Instance 四个概念的理解
Element(元素)
描述 UI 的普通对象:
javascript
const element = {
type: 'div',
props: {
children: 'Hello'
}
}
// JSX 创建
const element = <div>Hello</div>Component(组件)
返回 Element 的函数或类:
javascript
// 函数组件
function Component() {
return <div>Hello</div>
}
// 类组件
class Component extends React.Component {
render() {
return <div>Hello</div>
}
}Instance(实例)
组件的实例化对象:
javascript
class Component extends React.Component {
state = { count: 0 }
handleClick = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
return <button onClick={this.handleClick}>{this.state.count}</button>
}
}
// this 就是实例Node(节点)
Fiber 架构中的工作单元:
javascript
const fiberNode = {
type: Component,
key: null,
memoizedState: { count: 0 },
child: null,
sibling: null,
return: null
}关系图
Component → 实例化 → Instance
↓
render()
↓
Element → 转换 → Fiber Node → 渲染 → DOM34. React 和 Vue 在技术层面有哪些区别?
核心理念
| 特性 | React | Vue |
|---|---|---|
| 核心理念 | 函数式、不可变 | 响应式、可变 |
| 模板 | JSX | 模板语法 |
| 状态管理 | setState/useState | data/reactive |
| 组件形式 | 函数/类组件 | Options/Composition API |
响应式原理
javascript
// React:手动触发更新
const [count, setCount] = useState(0)
setCount(1)
// Vue:自动追踪依赖
const count = ref(0)
count.value = 1模板 vs JSX
javascript
// Vue 模板
<template>
<div v-if="show">{{ message }}</div>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>
// React JSX
function Component() {
return (
<div>
{show && <div>{message}</div>}
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
)
}Diff 算法
| 特性 | React | Vue |
|---|---|---|
| 策略 | 同层比较 | 同层比较 + 最长递增子序列 |
| Key | 必须手动指定 | 自动处理 |
| 性能 | O(n) | O(n) |
生态系统
| 特性 | React | Vue |
|---|---|---|
| 路由 | react-router | vue-router |
| 状态管理 | Redux/Zustand | Pinia/Vuex |
| UI 库 | Ant Design/MUI | Element Plus/Ant Design Vue |
| CLI | CRA/Vite | Vue CLI/Vite |
35. 实现 useUpdate 方法,调用时强制组件重新渲染
实现方式
javascript
function useUpdate() {
const [, setState] = useState(0)
return useCallback(() => {
setState(c => c + 1)
}, [])
}
// 使用
function Component() {
const update = useUpdate()
return (
<div>
<button onClick={update}>Force Update</button>
</div>
)
}其他实现
javascript
// 使用 useReducer
function useUpdate() {
const [, dispatch] = useReducer(x => x + 1, 0)
return dispatch
}
// 使用 useRef
function useUpdate() {
const ref = useRef(0)
const [, setState] = useState(0)
return useCallback(() => {
ref.current += 1
setState(ref.current)
}, [])
}36. 说说你对自定义 hook 的理解
什么是自定义 Hook
自定义 Hook 是一个函数,其名称以 "use" 开头,可以调用其他 Hook:
javascript
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}作用
- 复用状态逻辑
- 抽象复杂逻辑
- 简化组件代码
示例
javascript
// useFetch
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
return { data, loading, error }
}
// 使用
function Component() {
const { data, loading, error } = useFetch('/api/users')
}37. 如何让 useEffect 支持 async/await?
问题
javascript
// ❌ 错误:useEffect 不能直接使用 async
useEffect(async () => {
const data = await fetch('/api/data')
}, [])解决方案
1. 在内部定义 async 函数
javascript
useEffect(() => {
async function fetchData() {
const data = await fetch('/api/data')
setData(data)
}
fetchData()
}, [])2. 使用 IIFE
javascript
useEffect(() => {
;(async () => {
const data = await fetch('/api/data')
setData(data)
})()
}, [])3. 自定义 Hook
javascript
function useAsyncEffect(effect, deps) {
useEffect(() => {
const cleanup = effect()
return () => {
cleanup && cleanup.then(cleanup => cleanup && cleanup())
}
}, deps)
}38. React Portals 有什么用?
什么是 Portals
Portals 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的方案:
javascript
ReactDOM.createPortal(child, container)使用场景
1. Modal 对话框
javascript
function Modal({ children }) {
return ReactDOM.createPortal(
<div className='modal'>{children}</div>,
document.getElementById('modal-root')
)
}2. Tooltip
javascript
function Tooltip({ content, children }) {
return (
<>
{children}
{ReactDOM.createPortal(
<div className='tooltip'>{content}</div>,
document.body
)}
</>
)
}特点
- 子组件仍能接收到父组件的 context
- 事件冒泡仍按 React 树进行
39. 为什么 react 需要 fiber 架构,而 Vue 却不需要?
React 需要 Fiber 的原因
- 递归更新不可中断:React 15 使用递归更新,无法暂停
- 大应用卡顿:复杂组件更新会阻塞主线程
- 优先级调度:需要区分高/低优先级任务
Vue 不需要的原因
- 响应式系统:Vue 的响应式系统自动追踪依赖,更新更精确
- 模板编译优化:编译时已确定依赖关系
- 批量更新:Vue 自动批量更新,不需要手动优化
对比
| 特性 | React | Vue |
|---|---|---|
| 更新方式 | 递归/可中断 | 依赖收集 |
| 性能优化 | Fiber | 响应式 |
| 调度 | 手动 | 自动 |
40. 子组件是一个 Portal,发生点击事件能冒泡到父组件吗?
答案:可以
javascript
function Parent() {
const handleClick = () => {
console.log('Parent clicked')
}
return (
<div onClick={handleClick}>
<Modal>
<button>Click me</button>
</Modal>
</div>
)
}
function Modal({ children }) {
return ReactDOM.createPortal(
<div className='modal'>{children}</div>,
document.body
)
}
// 点击按钮会触发 Parent 的 handleClick原因
Portal 只改变 DOM 结构,不改变 React 组件树。事件冒泡按 React 树进行。
41. 说说 React render 方法的原理?在什么时候会被触发?
render 方法原理
javascript
class Component extends React.Component {
render() {
return <div>Hello</div>
}
}- 返回 React Element
- React Element 被转换为 Fiber 节点
- Fiber 节点被渲染为真实 DOM
触发时机
1. 首次渲染
javascript
ReactDOM.render(<App />, root)2. setState
javascript
this.setState({ count: 1 })3. forceUpdate
javascript
this.forceUpdate()4. 父组件重新渲染
javascript
function Parent() {
const [count, setCount] = useState(0)
return <Child /> // Child 会重新渲染
}5. Context 变化
javascript
<ThemeContext.Provider value={theme}>
<Child /> {/* theme 变化时重新渲染 */}
</ThemeContext.Provider>42. React 中,怎么实现父组件调用子组件中的方法?
方法一:useImperativeHandle
javascript
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
sayHello: () => {
console.log('Hello from child')
}
}))
return <div>Child</div>
})
function Parent() {
const childRef = useRef()
const handleClick = () => {
childRef.current.sayHello()
}
return (
<>
<Child ref={childRef} />
<button onClick={handleClick}>Call Child</button>
</>
)
}方法二:回调函数
javascript
function Parent() {
const childMethodRef = useRef()
return (
<>
<Child onMount={method => (childMethodRef.current = method)} />
<button onClick={() => childMethodRef.current?.()}>Call</button>
</>
)
}
function Child({ onMount }) {
const sayHello = () => {
console.log('Hello')
}
useEffect(() => {
onMount(sayHello)
}, [])
return <div>Child</div>
}43. react 如何做到和 vue 中 keep-alive 的缓存效果
方案一:react-activation
javascript
import { AliveScope, KeepAlive } from 'react-activation'
function App() {
return (
<AliveScope>
<KeepAlive id='MyComponent'>
<MyComponent />
</KeepAlive>
</AliveScope>
)
}方案二:display: none
javascript
function KeepAlive({ children, show }) {
return <div style={{ display: show ? 'block' : 'none' }}>{children}</div>
}方案三:自定义实现
javascript
function useKeepAlive(Component, key) {
const cache = useRef(new Map())
if (!cache.current.has(key)) {
cache.current.set(key, <Component />)
}
return cache.current.get(key)
}44. React 如何做路由监听
react-router v6
javascript
import { useLocation, useNavigate } from 'react-router-dom'
function Component() {
const location = useLocation()
const navigate = useNavigate()
useEffect(() => {
console.log('Route changed:', location.pathname)
}, [location])
return <div>Current path: {location.pathname}</div>
}自定义 Hook
javascript
function useRouteChange(callback) {
const location = useLocation()
useEffect(() => {
callback(location)
}, [location])
}45. useState 的原理是什么,背后怎么执行的
原理
useState 使用链表存储状态:
javascript
let workInProgressHook = null
function useState(initialState) {
let hook = workInProgressHook
if (!hook) {
hook = {
memoizedState: initialState,
queue: { pending: null },
next: null
}
workInProgressHook = hook
}
workInProgressHook = hook.next
return [hook.memoizedState, dispatchAction]
}执行流程
- 首次渲染:创建 Hook,初始化状态
- 更新渲染:从链表取出 Hook,处理更新队列
- 返回状态和更新函数
46. React 是如何处理组件更新和渲染的?
更新触发
javascript
// setState
dispatchAction({
action: newState,
next: null
})
// 调度更新
scheduleUpdateOnFiber(fiber)更新流程
触发更新 → 调度更新 → 协调阶段 → 提交阶段 → 渲染协调阶段
- 生成新的 Fiber 树
- Diff 比较
- 标记副作用
提交阶段
- 执行 DOM 操作
- 执行生命周期
- 执行 Hooks
47. React 性能优化有哪些方法?
1. 使用 React.memo
javascript
const MemoComponent = React.memo(Component)2. 使用 useMemo/useCallback
javascript
const memoizedValue = useMemo(() => compute(a, b), [a, b])
const memoizedCallback = useCallback(() => fn(a), [a])3. 列表使用 key
javascript
items.map(item => <li key={item.id}>{item.name}</li>)4. 代码分割
javascript
const LazyComponent = React.lazy(() => import('./Component'))5. 虚拟列表
javascript
import { FixedSizeList } from 'react-window'6. 避免内联函数
javascript
// ❌ 错误
<button onClick={() => handleClick(id)}>
// ✅ 正确
const handleClick = useCallback(() => {}, [id])
<button onClick={handleClick}>48. React 中如何进行代码分割?
React.lazy
javascript
const LazyComponent = React.lazy(() => import('./Component'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
)
}动态 import
javascript
function loadComponent() {
import('./Component').then(module => {
setComponent(module.default)
})
}基于路由分割
javascript
const Home = React.lazy(() => import('./Home'))
const About = React.lazy(() => import('./About'))
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Routes>
</Suspense>
)
}49. 什么是 React 服务端渲染(SSR)
概念
SSR 是指在服务器端将 React 组件渲染为 HTML 字符串,发送给客户端。
优点
- SEO 友好
- 首屏加载快
- 适合内容型网站
实现
javascript
// 服务器端
import { renderToString } from 'react-dom/server'
app.get('*', (req, res) => {
const html = renderToString(<App />)
res.send(`
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>
`)
})
// 客户端
import { hydrateRoot } from 'react-dom/client'
hydrateRoot(document.getElementById('root'), <App />)框架
- Next.js
- Remix
50. 循环渲染中为什么推荐不用 index 做 key?
问题
javascript
// ❌ 使用 index
items.map((item, index) => <li key={index}>{item}</li>)原因
- 列表顺序变化时:key 会变化,导致不必要的重新渲染
- 状态错乱:组件状态可能绑定到错误的元素
示例
javascript
// 列表:[A, B, C],keys: [0, 1, 2]
// 删除 A 后:[B, C],keys: [0, 1]
// B 的 key 从 1 变成 0,React 认为是新元素正确做法
javascript
// ✅ 使用唯一 ID
items.map(item => <li key={item.id}>{item.name}</li>)51. 说说 React Jsx 转换成真实 DOM 过程?
转换流程
JSX → React.createElement() → React Element → Fiber Node → DOM详细过程
1. JSX 编译
javascript
// JSX
;<div className='app'>
<span>Hello</span>
</div>
// 编译后
React.createElement(
'div',
{ className: 'app' },
React.createElement('span', null, 'Hello')
)2. 创建 React Element
javascript
const element = {
$$typeof: Symbol('react.element'),
type: 'div',
key: null,
ref: null,
props: {
className: 'app',
children: {
type: 'span',
props: { children: 'Hello' }
}
}
}3. 创建 Fiber 节点
javascript
const fiber = {
type: 'div',
key: null,
props: { className: 'app' },
stateNode: null, // DOM 节点
child: null,
sibling: null,
return: null
}4. 生成 DOM
javascript
const dom = document.createElement('div')
dom.className = 'app'
// ... 处理 children52. 说说你在 React 项目是如何捕获错误的?
Error Boundaries
javascript
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error)
console.error('Error Info:', errorInfo)
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>
}
return this.props.children
}
}
// 使用
;<ErrorBoundary>
<App />
</ErrorBoundary>try-catch
javascript
// 异步错误
async function fetchData() {
try {
const data = await fetch('/api/data')
return data
} catch (error) {
console.error('Fetch error:', error)
}
}
// 事件处理
function handleClick() {
try {
doSomething()
} catch (error) {
console.error('Click error:', error)
}
}全局错误处理
javascript
window.onerror = (message, source, lineno, colno, error) => {
console.error('Global error:', error)
}
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason)
})53. 说说你对 Redux 的理解?其工作原理?
核心概念
Redux 是一个可预测的状态容器,遵循单向数据流:
Action → Reducer → Store → View三大原则
- 单一数据源:整个应用的 state 存储在一个 store 中
- State 是只读的:只能通过 dispatch action 来修改
- 使用纯函数修改:Reducer 是纯函数
基本使用
javascript
// Action
const increment = { type: 'INCREMENT' }
const decrement = { type: 'DECREMENT' }
// Reducer
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// Store
const store = createStore(counter)
// 订阅
store.subscribe(() => {
console.log(store.getState())
})
// 派发
store.dispatch(increment)工作原理
javascript
function createStore(reducer) {
let state
let listeners = []
function getState() {
return state
}
function subscribe(listener) {
listeners.push(listener)
return () => {
listeners = listeners.filter(l => l !== listener)
}
}
function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
return action
}
dispatch({ type: '@@INIT' })
return { getState, subscribe, dispatch }
}54. 说说对 Redux 中间件的理解?常用的中间件有哪些?实现原理?
什么是中间件
中间件提供了在 action 发出后,到达 reducer 之前的扩展点:
Action → Middleware → Reducer → Store常用中间件
redux-thunk
javascript
// 允许 dispatch 函数
const fetchUser = id => dispatch => {
dispatch({ type: 'FETCH_START' })
fetch(`/api/user/${id}`)
.then(res => res.json())
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
}
// 使用
store.dispatch(fetchUser(1))redux-saga
javascript
import { call, put, takeEvery } from 'redux-saga/effects'
function* fetchUser(action) {
try {
const user = yield call(fetch, `/api/user/${action.payload}`)
yield put({ type: 'FETCH_SUCCESS', payload: user })
} catch (error) {
yield put({ type: 'FETCH_ERROR', error })
}
}
function* userSaga() {
yield takeEvery('FETCH_REQUEST', fetchUser)
}redux-logger
javascript
const logger = store => next => action => {
console.log('dispatching', action)
const result = next(action)
console.log('next state', store.getState())
return result
}实现原理
javascript
function applyMiddleware(...middlewares) {
return createStore => reducer => {
const store = createStore(reducer)
let dispatch = store.dispatch
const middlewareAPI = {
getState: store.getState,
dispatch: action => dispatch(action)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return { ...store, dispatch }
}
}55. 你在 React 项目中是如何使用 Redux 的?项目结构是如何划分?
项目结构
src/
├── store/
│ ├── index.js # store 配置
│ ├── reducers/ # reducers
│ │ ├── user.js
│ │ └── products.js
│ ├── actions/ # actions
│ │ ├── user.js
│ │ └── products.js
│ ├── sagas/ # sagas
│ │ └── index.js
│ └── middleware/ # 中间件
├── components/
├── pages/
└── App.js使用方式
javascript
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './reducers/user'
import productsReducer from './reducers/products'
export const store = configureStore({
reducer: {
user: userReducer,
products: productsReducer
}
})
// 组件中使用
import { useSelector, useDispatch } from 'react-redux'
import { increment } from './store/actions/user'
function Component() {
const user = useSelector(state => state.user)
const dispatch = useDispatch()
return (
<div>
<span>{user.name}</span>
<button onClick={() => dispatch(increment())}>Click</button>
</div>
)
}56. Redux 和 Vuex 有什么区别,它们的共同思想?
共同思想
- 单一数据源
- 状态只读
- 同步更新
区别
| 特性 | Redux | Vuex |
|---|---|---|
| 修改方式 | Reducer | Mutation |
| 异步处理 | Middleware | Action |
| 数据流 | 单向 | 单向 |
| DevTools | 支持 | 支持 |
代码对比
javascript
// Redux
dispatch({ type: 'INCREMENT' })
// Vuex
store.commit('INCREMENT')57. React-Router 工作原理?react-router-dom 有哪些组件
工作原理
React Router 基于 history API:
- 监听 URL 变化
- 匹配路由配置
- 渲染对应组件
常用组件
javascript
import {
BrowserRouter,
Routes,
Route,
Link,
Navigate,
Outlet,
useParams,
useLocation,
useNavigate
} from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<nav>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
</nav>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />}>
<Route path='team' element={<Team />} />
</Route>
<Route path='/user/:id' element={<User />} />
<Route path='*' element={<Navigate to='/' />} />
</Routes>
</BrowserRouter>
)
}58. 说说你对 ReactRouter 的理解?常用的 Router 组件有哪些?
理解
React Router 是 React 的路由库,用于管理 URL 和组件的映射关系。
常用组件
| 组件 | 作用 |
|---|---|
| BrowserRouter | HTML5 history 路由 |
| HashRouter | hash 路由 |
| Routes | 路由容器 |
| Route | 路由配置 |
| Link | 导航链接 |
| Navigate | 编程式导航 |
| Outlet | 子路由出口 |
Hooks
| Hook | 作用 |
|---|---|
| useLocation | 获取当前 URL |
| useNavigate | 编程式导航 |
| useParams | 获取路由参数 |
| useSearchParams | 获取查询参数 |
59. 说说 ReactRouter 有几种模式,以及实现原理?
两种模式
1. BrowserRouter
使用 HTML5 history API:
javascript
// 原理
window.history.pushState(state, title, url)
window.history.replaceState(state, title, url)
window.addEventListener('popstate', callback)2. HashRouter
使用 URL hash:
javascript
// 原理
window.location.hash = '#/path'
window.addEventListener('hashchange', callback)对比
| 特性 | BrowserRouter | HashRouter |
|---|---|---|
| URL 格式 | /path | #/path |
| 服务器配置 | 需要 | 不需要 |
| SEO | 友好 | 不友好 |
| 兼容性 | HTML5 | 更好 |
60. react 和 react-dom 是什么关系?
react
核心库,提供:
- React.createElement
- Component
- Hooks
- 虚拟 DOM 相关
react-dom
渲染库,提供:
- render/hydrate
- createPortal
- findDOMNode
分离原因
- 支持多平台(react-native)
- 关注点分离
- 更好的 tree-shaking
61. React 中在哪捕获错误?
Error Boundaries
javascript
class ErrorBoundary extends React.Component {
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
logError(error, errorInfo)
}
render() {
if (this.state.hasError) {
return <Fallback />
}
return this.props.children
}
}无法捕获的错误
- 事件处理函数中的错误
- 异步代码中的错误
- 服务端渲染中的错误
- Error Boundary 自身的错误
解决方案
javascript
// 事件处理
function handleClick() {
try {
doSomething()
} catch (error) {
// 处理错误
}
}
// 异步
useEffect(() => {
async function fetchData() {
try {
const data = await fetch()
} catch (error) {
// 处理错误
}
}
fetchData()
}, [])62. React 中为什么不直接使用 requestIdleCallback?
原因
- 兼容性问题:部分浏览器不支持
- 精度问题:requestIdleCallback 的 FPS 不可控
- 性能问题:触发频率不稳定
React 的解决方案
React 实现了自己的调度器:
javascript
// 使用 MessageChannel 模拟
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = performWorkUntilDeadline
function scheduleCallback(callback) {
scheduledCallback = callback
port.postMessage(null)
}
function performWorkUntilDeadline() {
const startTime = performance.now()
// 执行工作...
}63. 说说你对 immutable 的理解?如何应用在 react 项目中?
什么是 Immutable
Immutable 数据是指创建后不可修改的数据。每次修改都会返回新的对象。
Immutable.js
javascript
import { Map, List } from 'immutable'
const map1 = Map({ a: 1, b: 2 })
const map2 = map1.set('a', 3) // map1 不变
const list1 = List([1, 2, 3])
const list2 = list1.push(4) // list1 不变在 React 中的应用
javascript
// 使用 Immutable.js
import { Map } from 'immutable'
class Component extends React.Component {
state = {
data: Map({ count: 0 })
}
handleClick = () => {
this.setState(({ data }) => ({
data: data.set('count', data.get('count') + 1)
}))
}
shouldComponentUpdate(nextProps, nextState) {
return !this.state.data.equals(nextState.data)
}
}Immer
javascript
import { produce } from 'immer'
const nextState = produce(currentState, draft => {
draft.count++
})64. 说说 React 服务端渲染怎么做?原理是什么?
实现
javascript
// 服务器端
import { renderToString } from 'react-dom/server'
import App from './App'
app.get('*', (req, res) => {
const html = renderToString(<App />)
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SSR</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>
`)
})
// 客户端 hydrate
import { hydrateRoot } from 'react-dom/client'
import App from './App'
hydrateRoot(document.getElementById('root'), <App />)原理
- 服务器端:将组件渲染为 HTML 字符串
- 客户端:hydrate(水合)接管 DOM
65. React 18 新特性
1. Concurrent Features
javascript
// 自动批处理
function handleClick() {
setCount(c => c + 1)
setFlag(f => !f)
// 只触发一次渲染
}2. useTransition
javascript
const [isPending, startTransition] = useTransition()
startTransition(() => {
setSearchQuery(input) // 低优先级更新
})3. useDeferredValue
javascript
const deferredQuery = useDeferredValue(query)4. Suspense 改进
javascript
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>5. 新的 Root API
javascript
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root'))
root.render(<App />)66. 怎么在代码中判断一个 React 组件是 class component 还是 function component?
方法
javascript
function isClassComponent(component) {
return (
typeof component === 'function' &&
component.prototype &&
component.prototype.isReactComponent
)
}
function isFunctionComponent(component) {
return (
typeof component === 'function' &&
(!component.prototype || !component.prototype.isReactComponent)
)
}使用
javascript
class ClassComp extends React.Component {}
function FuncComp() {}
isClassComponent(ClassComp) // true
isClassComponent(FuncComp) // false
isFunctionComponent(FuncComp) // true67. 讲讲 React.memo 和 JS 的 memorize 函数的区别
React.memo
用于组件缓存:
javascript
const MemoComponent = React.memo(Component)
// 自定义比较
const MemoComponent = React.memo(Component, (prevProps, nextProps) => {
return prevProps.id === nextProps.id
})JS Memorize
用于函数缓存:
javascript
function memoize(fn) {
const cache = new Map()
return function (...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
return cache.get(key)
}
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
const memoizedAdd = memoize((a, b) => a + b)区别
| 特性 | React.memo | JS Memorize |
|---|---|---|
| 作用对象 | React 组件 | 普通函数 |
| 比较方式 | 浅比较 props | 参数序列化 |
| 返回值 | 组件 | 函数结果 |
68. 怎么判断一个对象是否是 React 元素?
方法
javascript
function isReactElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === Symbol.for('react.element')
)
}
// 使用
const element = <div>Hello</div>
isReactElement(element) // true
isReactElement({}) // falseReact 内部实现
javascript
const REACT_ELEMENT_TYPE = Symbol.for('react.element')
function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
)
}69. taro 的实现原理是怎么样的?
架构
Taro 是一套多端开发框架,原理如下:
React/Vue 代码 → Taro 编译 → 小程序/ H5 / RN编译时
- JSX 转换:将 JSX 转换为小程序模板
- 样式转换:将 CSS 转换为 WXSS
- 配置生成:生成小程序配置文件
运行时
- 适配层:封装小程序 API
- 路由管理:模拟页面栈
- 状态管理:实现数据响应
Taro 3.x
使用 React Reconciler:
javascript
import { reconciler } from '@tarojs/react'
const container = reconciler.createContainer(root, false, false)
reconciler.updateContainer(<App />, container, null, callback)70. taro 2.x 和 taro 3 最大区别是什么?
Taro 2.x
- 静态编译
- 模板预编译
- 不支持 React Hooks
Taro 3.x
- 运行时框架
- 支持 React Reconciler
- 支持 React Hooks
- 更好的开发体验
对比
| 特性 | Taro 2.x | Taro 3.x |
|---|---|---|
| 编译方式 | 静态 | 运行时 |
| React 支持 | 有限 | 完整 |
| Hooks | 不支持 | 支持 |
| 性能 | 更好 | 略低 |
| 灵活性 | 低 | 高 |
71. 单页应用如何提高加载速度?
优化策略
1. 代码分割
javascript
const LazyComponent = React.lazy(() => import('./Component'))2. 路由懒加载
javascript
const routes = [
{ path: '/', component: React.lazy(() => import('./Home')) },
{ path: '/about', component: React.lazy(() => import('./About')) }
]3. 预加载
javascript
// 鼠标悬停时预加载
<Link to='/about' onMouseEnter={() => import('./About')}>
About
</Link>4. 骨架屏
javascript
function Skeleton() {
return (
<div className='skeleton'>
<div className='skeleton-header' />
<div className='skeleton-content' />
</div>
)
}
;<Suspense fallback={<Skeleton />}>
<Component />
</Suspense>5. 服务端渲染
javascript
// Next.js
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}6. 资源优化
- 图片懒加载
- 压缩资源
- 使用 CDN
- 开启 Gzip
72. 实现一个 useTimeout Hook
实现
javascript
function useTimeout(callback, delay) {
const savedCallback = useRef(callback)
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay === null) return
const timer = setTimeout(() => {
savedCallback.current()
}, delay)
return () => clearTimeout(timer)
}, [delay])
}
// 使用
function Component() {
const [count, setCount] = useState(0)
useTimeout(() => {
setCount(c => c + 1)
}, 1000)
return <div>{count}</div>
}支持暂停/恢复
javascript
function useTimeout(callback, delay, paused = false) {
const savedCallback = useRef(callback)
const remainingRef = useRef(delay)
const startTimeRef = useRef(null)
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (paused || delay === null) return
startTimeRef.current = Date.now()
const timer = setTimeout(() => {
savedCallback.current()
}, remainingRef.current)
return () => {
clearTimeout(timer)
remainingRef.current -= Date.now() - startTimeRef.current
}
}, [delay, paused])
}73. react 中怎么捕获异常?
Error Boundaries
javascript
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null }
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
logError(error, errorInfo)
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />
}
return this.props.children
}
}try-catch
javascript
// 同步代码
try {
doSomething()
} catch (error) {
handleError(error)
}
// 异步代码
async function fetchData() {
try {
const response = await fetch('/api/data')
return response.json()
} catch (error) {
handleError(error)
}
}全局错误
javascript
// 未捕获的 Promise 错误
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason)
})
// 全局错误
window.onerror = (message, source, lineno, colno, error) => {
console.error('Global error:', error)
}74. 最大子序和
题目
给定一个整数数组 nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
解法:动态规划
javascript
function maxSubArray(nums) {
let maxSum = nums[0]
let currentSum = nums[0]
for (let i = 1; i < nums.length; i++) {
currentSum = Math.max(nums[i], currentSum + nums[i])
maxSum = Math.max(maxSum, currentSum)
}
return maxSum
}
// 示例
maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) // 6解法:分治
javascript
function maxSubArray(nums) {
return helper(nums, 0, nums.length - 1)
}
function helper(nums, left, right) {
if (left === right) return nums[left]
const mid = Math.floor((left + right) / 2)
const leftMax = helper(nums, left, mid)
const rightMax = helper(nums, mid + 1, right)
const crossMax = crossSum(nums, left, right, mid)
return Math.max(leftMax, rightMax, crossMax)
}
function crossSum(nums, left, right, mid) {
let leftSum = -Infinity
let sum = 0
for (let i = mid; i >= left; i--) {
sum += nums[i]
leftSum = Math.max(leftSum, sum)
}
let rightSum = -Infinity
sum = 0
for (let i = mid + 1; i <= right; i++) {
sum += nums[i]
rightSum = Math.max(rightSum, sum)
}
return leftSum + rightSum
}75. 说说 https 的握手过程
TLS 握手流程
客户端 服务器
| |
| 1. Client Hello |
| (支持的加密套件、随机数) |
|--------------------------------------->|
| |
| 2. Server Hello |
| (选择的加密套件、随机数) |
| 3. Certificate (证书) |
| 4. Server Hello Done |
|<---------------------------------------|
| |
| 5. Client Key Exchange |
| (预主密钥加密) |
| 6. Change Cipher Spec |
| 7. Finished |
|--------------------------------------->|
| |
| 8. Change Cipher Spec |
| 9. Finished |
|<---------------------------------------|
| |
| 10. 加密通信开始 |详细步骤
- Client Hello:客户端发送支持的加密套件和随机数
- Server Hello:服务器选择加密套件,发送随机数
- Certificate:服务器发送证书
- Client Key Exchange:客户端验证证书,发送加密的预主密钥
- 生成会话密钥:双方用随机数和预主密钥生成会话密钥
- Finished:双方验证握手完成
76. HTTP2 中,多路复用的原理是什么?
原理
HTTP/2 在一个 TCP 连接上可以并行传输多个请求和响应。
核心概念
1. 流(Stream)
一个双向通信单元,可以承载一个或多个消息。
2. 消息(Message)
一个完整的 HTTP 请求或响应。
3. 帧(Frame)
最小的通信单位,每个帧包含帧头和帧体。
帧结构
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+---------------+---------------+-------------------------------+
| Stream Identifier (31) |
+---------------------------------------------------------------+
| Frame Payload (0...) |
+---------------------------------------------------------------+多路复用优势
- 单连接并行请求
- 解决队头阻塞
- 减少连接数
77. 说说你对"三次握手"、"四次挥手"的理解
三次握手
客户端 服务器
| |
| 1. SYN=1, seq=x |
|--------------------------------------->|
| (请求建立连接) |
| |
| 2. SYN=1, ACK=1, seq=y, ack=x+1 |
|<---------------------------------------|
| (确认并同意连接) |
| |
| 3. ACK=1, seq=x+1, ack=y+1 |
|--------------------------------------->|
| (确认收到) |
| |
| 4. 连接建立 |四次挥手
客户端 服务器
| |
| 1. FIN=1, seq=u |
|--------------------------------------->|
| (请求关闭) |
| |
| 2. ACK=1, seq=v, ack=u+1 |
|<---------------------------------------|
| (确认收到) |
| |
| 3. FIN=1, ACK=1, seq=w, ack=u+1 |
|<---------------------------------------|
| (服务器请求关闭) |
| |
| 4. ACK=1, seq=u+1, ack=w+1 |
|--------------------------------------->|
| (确认收到) |
| |
| 5. 等待 2MSL 后关闭 |为什么是三次/四次
- 三次握手:确保双方都能收发
- 四次挥手:TCP 是全双工,需要双方分别关闭
78. 如何确保你的构造函数只能被 new 调用,而不能被普通调用
方法一:使用 new.target
javascript
function Person(name) {
if (!new.target) {
throw new Error('Person must be called with new')
}
this.name = name
}
// 或者自动转换
function Person(name) {
if (!(this instanceof Person)) {
return new Person(name)
}
this.name = name
}方法二:使用 ES6 class
javascript
class Person {
constructor(name) {
this.name = name
}
}
// class 必须使用 new 调用
Person('John') // TypeError
new Person('John') // OK方法三:使用 Symbol
javascript
const REQUIRED = Symbol('required')
function Person(name, token) {
if (token !== REQUIRED) {
throw new Error('Must use new')
}
this.name = name
}
const createPerson = name => new Person(name, REQUIRED)79. 为什么推荐将静态资源放到 cdn 上?
原因
1. 加载速度更快
CDN 节点分布全球,用户访问最近的节点。
2. 减轻服务器压力
静态资源由 CDN 提供,减少源站请求。
3. 并行下载
浏览器对同一域名有并发限制,CDN 可以突破限制。
4. 缓存优化
CDN 提供更好的缓存策略。
5. 高可用性
CDN 有多节点备份,更稳定。
使用示例
html
<!-- 直接使用 CDN -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<!-- 配置 webpack publicPath -->
module.exports = { output: { publicPath: 'https://cdn.example.com/' } }80. 说说 React 事件和原生事件的执行顺序
执行顺序
原生事件(捕获)→ React 事件 → 原生事件(冒泡)示例
javascript
function App() {
useEffect(() => {
document.addEventListener(
'click',
() => {
console.log('原生事件 - 捕获')
},
true
)
document.addEventListener(
'click',
() => {
console.log('原生事件 - 冒泡')
},
false
)
}, [])
const handleClick = () => {
console.log('React 事件')
}
return (
<div onClick={handleClick}>
<button>Click</button>
</div>
)
}
// 点击按钮输出:
// 原生事件 - 捕获
// React 事件
// 原生事件 - 冒泡原因
React 事件委托到 root 节点,在冒泡阶段处理。
81. Vue 2.0 为什么不能检查数组的变化,该怎么解决?
原因
Vue 2 使用 Object.defineProperty 实现响应式,无法监测:
- 数组索引直接赋值
- 数组长度修改
解决方案
1. 使用 Vue.set
javascript
Vue.set(this.items, index, newValue)
// 或
this.$set(this.items, index, newValue)2. 使用变异方法
javascript
// Vue 包装了这些方法
this.items.push(newValue)
this.items.pop()
this.items.shift()
this.items.unshift(newValue)
this.items.splice(index, 1, newValue)
this.items.sort()
this.items.reverse()3. 替换数组
javascript
this.items = this.items.map((item, i) => (i === index ? newValue : item))82. 说说 Vue 页面渲染流程
渲染流程
模板 → 解析 → AST → 优化 → 代码生成 → 渲染函数 → VNode → DOM详细步骤
1. 解析模板
javascript
const ast = parse(template)2. 优化 AST
javascript
const optimizedAst = optimize(ast)3. 生成渲染函数
javascript
const render = generate(ast)4. 执行渲染函数
javascript
const vnode = render.call(vm)5. 生成 DOM
javascript
vm._update(vnode)流程图
new Vue()
↓
init()
↓
mount()
↓
render() → VNode
↓
patch() → DOM
↓
mounted83. 请简述 == 的机制
比较规则
1. 类型相同
直接比较值
javascript
1 == 1 // true
'abc' == 'abc' // true2. null 和 undefined
javascript
null == undefined // true
null == 0 // false
undefined == 0 // false3. 数字和字符串
字符串转数字
javascript
'1' == 1 // true
'abc' == 1 // false (NaN)4. 布尔值和其他
布尔值转数字
javascript
true == 1 // true
false == 0 // true5. 对象和原始值
对象转原始值
javascript
;([1] == (1)[(1, 2)]) == // true
'1,2' // true转换顺序
对象 → 原始值 → 数字 → 比较84. 怎么做移动端的样式适配?
方案一:rem
javascript
// 设置根元素字体大小
function setRem() {
const baseSize = 16
const scale = document.documentElement.clientWidth / 375
document.documentElement.style.fontSize = baseSize * scale + 'px'
}
setRem()
window.addEventListener('resize', setRem)css
.box {
width: 1rem; /* 相对于根元素字体大小 */
}方案二:vw/vh
css
.box {
width: 50vw; /* 视口宽度的 50% */
height: 100vh; /* 视口高度 */
}方案三:媒体查询
css
@media screen and (max-width: 768px) {
.box {
width: 100%;
}
}方案四:flexible.js
javascript
// lib-flexible
;(function (win, lib) {
var doc = win.document
var docEl = doc.documentElement
var metaEl = doc.querySelector('meta[name="viewport"]')
var dpr = 0
var scale = 0
dpr = win.devicePixelRatio || 1
scale = 1 / dpr
docEl.setAttribute('data-dpr', dpr)
metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale)
})()85. 说说 sourcemap 的原理?
什么是 Source Map
Source Map 是一个映射文件,记录了编译后代码和源代码的对应关系。
结构
json
{
"version": 3,
"file": "bundle.js",
"sourceRoot": "",
"sources": ["index.js", "utils.js"],
"names": ["add", "a", "b"],
"mappings": "AAAA,SAASA,IAAMC,EAAMC"
}mappings 编码
使用 VLQ 编码:
位置 → [生成的行,生成的列,源文件索引,源文件行,源文件列,名称索引]工作原理
- 编译时生成 .map 文件
- 浏览器加载时解析映射
- 报错时定位到源码位置
配置
javascript
// webpack
module.exports = {
devtool: 'source-map'
}86. vue 中 computed 和 watch 区别
computed
计算属性,有缓存:
javascript
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}特点:
- 有缓存,依赖不变不重新计算
- 必须返回值
- 适合派生数据
watch
侦听器,无缓存:
javascript
watch: {
firstName(newVal, oldVal) {
this.fullName = newVal + ' ' + this.lastName
}
}特点:
- 无缓存,每次都执行
- 可以执行异步操作
- 适合副作用操作
对比
| 特性 | computed | watch |
|---|---|---|
| 缓存 | 有 | 无 |
| 异步 | 不支持 | 支持 |
| 返回值 | 必须 | 可选 |
| 场景 | 派生数据 | 副作用 |
87. 什么是 DNS 劫持?
概念
DNS 劫持是指攻击者篡改 DNS 解析结果,将域名解析到错误的 IP 地址。
类型
1. 本地 DNS 劫持
修改本机 hosts 文件或 DNS 设置。
2. 中间人攻击
劫持 DNS 请求,返回伪造响应。
3. DNS 服务器劫持
攻击 DNS 服务器,篡改解析记录。
防范措施
- 使用 HTTPS
- 使用 DNS over HTTPS (DoH)
- 使用可信 DNS 服务器
- 定期检查 hosts 文件
88. 爬楼梯
题目
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。有多少种不同的方法?
解法:动态规划
javascript
function climbStairs(n) {
if (n <= 2) return n
let prev = 1
let curr = 2
for (let i = 3; i <= n; i++) {
const next = prev + curr
prev = curr
curr = next
}
return curr
}
// 示例
climbStairs(3) // 3: (1+1+1), (1+2), (2+1)
climbStairs(4) // 5解法:递归 + 记忆化
javascript
function climbStairs(n, memo = {}) {
if (n <= 2) return n
if (memo[n]) return memo[n]
memo[n] = climbStairs(n - 1, memo) + climbStairs(n - 2, memo)
return memo[n]
}89. 怎么实现图片懒加载?
方法一:IntersectionObserver
javascript
const images = document.querySelectorAll('img[data-src]')
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
})
images.forEach(img => observer.observe(img))方法二:滚动监听
javascript
function lazyLoad() {
const images = document.querySelectorAll('img[data-src]')
const windowHeight = window.innerHeight
images.forEach(img => {
const rect = img.getBoundingClientRect()
if (rect.top < windowHeight) {
img.src = img.dataset.src
}
})
}
window.addEventListener('scroll', lazyLoad)方法三:img loading 属性
html
<img src="image.jpg" loading="lazy" />90. HTTP 报文结构是怎样的?
请求报文
请求行
请求头
空行
请求体示例:
POST /api/user HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 25
{"name": "John", "age": 30}响应报文
状态行
响应头
空行
响应体示例:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15
{"status": "ok"}常见请求头
| 头部 | 说明 |
|---|---|
| Host | 目标主机 |
| Content-Type | 内容类型 |
| Authorization | 认证信息 |
| Cookie | Cookie |
| User-Agent | 客户端信息 |
91. 如果使用 Vue3.0 实现一个 Modal,你会怎么进行设计?
组件设计
vue
<!-- Modal.vue -->
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
<div class="modal-content" :style="{ width }">
<div class="modal-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button class="close-btn" @click="close">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button @click="close">取消</button>
<button @click="confirm">确定</button>
</slot>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { watch } from 'vue'
const props = defineProps({
visible: Boolean,
title: String,
width: { type: String, default: '500px' },
closeOnOverlay: { type: Boolean, default: true }
})
const emit = defineEmits(['update:visible', 'confirm', 'close'])
const close = () => {
emit('update:visible', false)
emit('close')
}
const confirm = () => {
emit('confirm')
close()
}
const handleOverlayClick = () => {
if (props.closeOnOverlay) close()
}
watch(
() => props.visible,
val => {
document.body.style.overflow = val ? 'hidden' : ''
}
)
</script>使用方式
vue
<template>
<button @click="showModal = true">打开</button>
<Modal v-model:visible="showModal" title="提示" @confirm="handleConfirm">
<p>确定要删除吗?</p>
</Modal>
</template>
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
const showModal = ref(false)
const handleConfirm = () => {
console.log('confirmed')
}
</script>92. js 中数组是如何在内存中存储的?
存储方式
JavaScript 数组在 V8 中有两种存储模式:
1. 快数组(Fast Elements)
连续内存存储,类似 C 数组:
javascript
const arr = [1, 2, 3, 4, 5]
// 内存连续2. 慢数组(Dictionary Elements)
哈希表存储,适合稀疏数组:
javascript
const arr = []
arr[10000] = 1
// 使用哈希表转换条件
javascript
// 快数组 → 慢数组
const arr = []
arr[100000] = 1 // 索引太大,转为慢数组
// 慢数组 → 快数组
const arr = []
arr[100000] = 1
arr.fill(0, 0, 100000) // 填充后转为快数组特点
| 类型 | 内存 | 访问速度 | 适用场景 |
|---|---|---|---|
| 快数组 | 连续 | O(1) | 密集数组 |
| 慢数组 | 哈希表 | O(1) 平均 | 稀疏数组 |
93. setTimeout 为什么不能保证能够及时执行?
原因
1. 事件循环机制
JavaScript 是单线程,setTimeout 的回调在事件队列中等待执行。
javascript
console.log(1)
setTimeout(() => console.log(2), 0)
console.log(3)
// 输出: 1, 3, 22. 最小延迟
浏览器有最小延迟限制(通常 4ms):
javascript
setTimeout(() => {}, 0) // 实际至少 4ms3. 主线程阻塞
如果主线程有长任务,会延迟执行:
javascript
setTimeout(() => console.log('timeout'), 0)
// 长任务
for (let i = 0; i < 1e9; i++) {}
// timeout 会等待长任务完成4. 页面不可见
页面在后台时,定时器会被节流:
javascript
// 页面不可见时,最小延迟变为 1000ms
document.hidden // true94. React 中 fiber、DOM、ReactElement、实例对象他们之间关系是什么?
概念关系
ReactElement → Fiber → DOM
↓ ↓
Component Instance详细关系
ReactElement
JSX 编译后的对象:
javascript
const element = {
$$typeof: Symbol('react.element'),
type: Component,
props: { name: 'John' }
}Fiber
React 内部的工作单元:
javascript
const fiber = {
type: Component,
memoizedProps: { name: 'John' },
memoizedState: { count: 0 },
stateNode: instance, // 类组件实例
child: null,
sibling: null,
return: null
}DOM
真实的 DOM 节点:
javascript
const dom = document.createElement('div')Instance
类组件的实例:
javascript
class Component extends React.Component {
state = { count: 0 }
}
const instance = new Component()转换流程
JSX
↓ 编译
ReactElement
↓ 协调
Fiber
↓ 渲染
DOM
类组件:
ReactElement → Fiber → Instance → DOM
函数组件:
ReactElement → Fiber → DOM关系图
ReactElement
↓ type
Component (类/函数)
↓ 实例化
Instance (仅类组件)
↓ render
Fiber
↓ stateNode
DOM