Skip to content

React 面试题大全

1. React渲染原理

React 的渲染原理可以分为以下几个核心阶段:

初始化渲染阶段

  1. 创建虚拟 DOM 树:JSX 被 Babel 编译成 React.createElement() 调用,生成虚拟 DOM 树(React Element 树)

  2. 构建 Fiber 树:React 16+ 引入 Fiber 架构,虚拟 DOM 节点会被转换为 Fiber 节点,形成 Fiber 树

  3. 生成副作用链表:在协调阶段,React 会标记需要执行的副作用(如 DOM 更新、生命周期调用等)

  4. 提交阶段:将计算出的副作用应用到真实 DOM

更新渲染阶段

  1. 触发更新:通过 setState、forceUpdate、ReactDOM.render 或 Hooks 触发

  2. 调度更新:React 调度器根据优先级安排更新任务

  3. 协调阶段(Reconciliation)

    • 生成新的 Fiber 树
    • 与旧 Fiber 树进行 diff 比较
    • 标记副作用
  4. 提交阶段(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 的优势

  1. 跨平台:可以渲染到 DOM、Native、Canvas 等多种平台
  2. 批量更新:将多次更新合并为一次 DOM 操作
  3. 声明式编程:开发者只需关注状态,不需要手动操作 DOM
  4. 性能优化:通过 diff 算法最小化 DOM 操作

虚拟 DOM 的工作流程

State/Props 变化 → 生成新的虚拟 DOM → Diff 比较 → 计算最小变更 → 更新真实 DOM

虚拟 DOM 的性能考量

  • 虚拟 DOM 本身有计算开销
  • 对于简单操作,直接操作 DOM 可能更快
  • 对于复杂应用,虚拟 DOM 的批量更新和 diff 优化能带来更好的性能

3. React diff 算法原理

React 的 diff 算法基于三个前提假设:

三个假设

  1. 不同类型的元素产生不同的树:如 <div> 变成 <span> 会触发完全重建
  2. 通过 key 标识哪些子元素是稳定的:key 帮助 React 识别元素的身份
  3. 只对同层级节点进行比较:不会跨层级比较节点

Diff 策略

1. Tree Diff(树层级的比较)

只对同一层级的节点进行比较,时间复杂度 O(n)

旧树:          新树:
  A              A
 / \            / \
B   C    →     B   D
   / \            / \
  D   E          E   F

2. 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. 增量渲染

  • 将渲染工作拆分成多个帧
  • 每帧只做一部分工作
  • 利用 requestIdleCallbackMessageChannel 实现

Fiber 架构的优势

  1. 可中断渲染:高优先级任务可以打断低优先级任务
  2. 可恢复渲染:中断后可以从断点继续
  3. 优先级调度:重要的更新优先执行
  4. 并发模式:支持 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. 执行时机不可控:空闲时间不确定
  3. 性能问题:可能延迟过高

控制权交还流程

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} />
    </>
  )
}

数据流的优势

  1. 可预测性:数据流向清晰,易于追踪
  2. 可调试性:状态变化有迹可循
  3. 可维护性:组件职责明确

状态管理方案

对于复杂应用,可以使用状态管理库:

  • 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
stateHooks
生命周期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>
}

特点:

  • 可读可写
  • 由组件自己控制
  • 改变会触发重新渲染
  • 是私有的

对比表格

特性PropsState
可变性只读可变
所有者父组件当前组件
用途组件间通信组件内部状态
修改方式父组件重新传递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>
  )
}

设计原则

  1. Props 用于配置:从外部传入,控制组件行为
  2. State 用于交互:组件内部的可变状态
  3. 最小化 State:只保留必要的可变状态
  4. 状态提升:多个组件需要共享的状态提升到共同父组件

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 使用场景

  1. 管理焦点、文本选择、媒体播放
  2. 触发动画
  3. 集成第三方 DOM 库
  4. 存储不触发渲染的值

注意事项

  • 不要在渲染期间访问或修改 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 做了什么

  1. 首次渲染

    • 创建 Hook 对象
    • 初始化状态值
    • 将 Hook 加入链表
  2. 更新渲染

    • 从链表中取出 Hook
    • 处理更新队列
    • 计算新状态
  3. 返回值

    • 返回当前状态值
    • 返回 dispatch 函数(setState)

为什么是异步的

  1. 性能优化:批量更新,减少渲染次数
  2. 一致性:保证渲染结果与状态一致
  3. 可预测性:避免中间状态

获取最新值的方法

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>
  )
}

注意事项

  1. 不要在循环、条件或嵌套函数中调用
  2. 依赖数组要完整
  3. 清理函数用于取消订阅、清除定时器等
  4. 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(异步更新)
  }, [])
}

详细对比表

特性componentDidMountuseEffect([])useLayoutEffect([])
执行时机DOM 更新后同步DOM 更新后异步DOM 更新后同步
阻塞绘制
访问 DOM同步异步同步
清理函数componentWillUnmountreturn 函数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 extendsfunction
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 事件和原生事件的执行顺序

执行顺序

  1. 原生事件捕获阶段
  2. React 事件捕获阶段
  3. React 事件冒泡阶段
  4. 原生事件冒泡阶段

示例演示

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 不会执行!

最佳实践

  1. 避免混用 React 事件和原生事件
  2. 如果必须混用,注意执行顺序
  3. 使用 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 bubble

React 事件机制特点

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 统一处理
阻止冒泡stopPropagationstopPropagation
阻止默认行为preventDefaultpreventDefault

总结

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 stateDOM
数据获取stateref
初始值valuedefaultValue
实时验证支持不支持
即时反馈支持不支持
代码量较多较少

应用场景

受控组件适用场景

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)
  }, [])
}

对比表

特性useEffectuseLayoutEffect
执行时机异步同步
阻塞绘制
使用场景大多数副作用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.memouseMemouseCallback
缓存类型组件函数
作用对象整个组件计算结果函数引用
触发条件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 → 渲染 → DOM

34. React 和 Vue 在技术层面有哪些区别?

核心理念

特性ReactVue
核心理念函数式、不可变响应式、可变
模板JSX模板语法
状态管理setState/useStatedata/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 算法

特性ReactVue
策略同层比较同层比较 + 最长递增子序列
Key必须手动指定自动处理
性能O(n)O(n)

生态系统

特性ReactVue
路由react-routervue-router
状态管理Redux/ZustandPinia/Vuex
UI 库Ant Design/MUIElement Plus/Ant Design Vue
CLICRA/ViteVue 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
}

作用

  1. 复用状态逻辑
  2. 抽象复杂逻辑
  3. 简化组件代码

示例

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 的原因

  1. 递归更新不可中断:React 15 使用递归更新,无法暂停
  2. 大应用卡顿:复杂组件更新会阻塞主线程
  3. 优先级调度:需要区分高/低优先级任务

Vue 不需要的原因

  1. 响应式系统:Vue 的响应式系统自动追踪依赖,更新更精确
  2. 模板编译优化:编译时已确定依赖关系
  3. 批量更新:Vue 自动批量更新,不需要手动优化

对比

特性ReactVue
更新方式递归/可中断依赖收集
性能优化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>
  }
}
  1. 返回 React Element
  2. React Element 被转换为 Fiber 节点
  3. 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]
}

执行流程

  1. 首次渲染:创建 Hook,初始化状态
  2. 更新渲染:从链表取出 Hook,处理更新队列
  3. 返回状态和更新函数

46. React 是如何处理组件更新和渲染的?

更新触发

javascript
// setState
dispatchAction({
  action: newState,
  next: null
})

// 调度更新
scheduleUpdateOnFiber(fiber)

更新流程

触发更新 → 调度更新 → 协调阶段 → 提交阶段 → 渲染

协调阶段

  1. 生成新的 Fiber 树
  2. Diff 比较
  3. 标记副作用

提交阶段

  1. 执行 DOM 操作
  2. 执行生命周期
  3. 执行 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 字符串,发送给客户端。

优点

  1. SEO 友好
  2. 首屏加载快
  3. 适合内容型网站

实现

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>)

原因

  1. 列表顺序变化时:key 会变化,导致不必要的重新渲染
  2. 状态错乱:组件状态可能绑定到错误的元素

示例

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'
// ... 处理 children

52. 说说你在 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

三大原则

  1. 单一数据源:整个应用的 state 存储在一个 store 中
  2. State 是只读的:只能通过 dispatch action 来修改
  3. 使用纯函数修改: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 有什么区别,它们的共同思想?

共同思想

  1. 单一数据源
  2. 状态只读
  3. 同步更新

区别

特性ReduxVuex
修改方式ReducerMutation
异步处理MiddlewareAction
数据流单向单向
DevTools支持支持

代码对比

javascript
// Redux
dispatch({ type: 'INCREMENT' })

// Vuex
store.commit('INCREMENT')

57. React-Router 工作原理?react-router-dom 有哪些组件

工作原理

React Router 基于 history API:

  1. 监听 URL 变化
  2. 匹配路由配置
  3. 渲染对应组件

常用组件

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 和组件的映射关系。

常用组件

组件作用
BrowserRouterHTML5 history 路由
HashRouterhash 路由
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)

对比

特性BrowserRouterHashRouter
URL 格式/path#/path
服务器配置需要不需要
SEO友好不友好
兼容性HTML5更好

60. react 和 react-dom 是什么关系?

react

核心库,提供:

  • React.createElement
  • Component
  • Hooks
  • 虚拟 DOM 相关

react-dom

渲染库,提供:

  • render/hydrate
  • createPortal
  • findDOMNode

分离原因

  1. 支持多平台(react-native)
  2. 关注点分离
  3. 更好的 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?

原因

  1. 兼容性问题:部分浏览器不支持
  2. 精度问题:requestIdleCallback 的 FPS 不可控
  3. 性能问题:触发频率不稳定

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 />)

原理

  1. 服务器端:将组件渲染为 HTML 字符串
  2. 客户端: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) // true

67. 讲讲 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.memoJS 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({}) // false

React 内部实现

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

编译时

  1. JSX 转换:将 JSX 转换为小程序模板
  2. 样式转换:将 CSS 转换为 WXSS
  3. 配置生成:生成小程序配置文件

运行时

  1. 适配层:封装小程序 API
  2. 路由管理:模拟页面栈
  3. 状态管理:实现数据响应

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.xTaro 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. 加密通信开始                       |

详细步骤

  1. Client Hello:客户端发送支持的加密套件和随机数
  2. Server Hello:服务器选择加密套件,发送随机数
  3. Certificate:服务器发送证书
  4. Client Key Exchange:客户端验证证书,发送加密的预主密钥
  5. 生成会话密钥:双方用随机数和预主密钥生成会话密钥
  6. 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. 数组索引直接赋值
  2. 数组长度修改

解决方案

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

mounted

83. 请简述 == 的机制

比较规则

1. 类型相同

直接比较值

javascript
1 == 1 // true
'abc' == 'abc' // true

2. null 和 undefined

javascript
null == undefined // true
null == 0 // false
undefined == 0 // false

3. 数字和字符串

字符串转数字

javascript
'1' == 1 // true
'abc' == 1 // false (NaN)

4. 布尔值和其他

布尔值转数字

javascript
true == 1 // true
false == 0 // true

5. 对象和原始值

对象转原始值

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 编码:

位置 → [生成的行,生成的列,源文件索引,源文件行,源文件列,名称索引]

工作原理

  1. 编译时生成 .map 文件
  2. 浏览器加载时解析映射
  3. 报错时定位到源码位置

配置

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
  }
}

特点:

  • 无缓存,每次都执行
  • 可以执行异步操作
  • 适合副作用操作

对比

特性computedwatch
缓存
异步不支持支持
返回值必须可选
场景派生数据副作用

87. 什么是 DNS 劫持?

概念

DNS 劫持是指攻击者篡改 DNS 解析结果,将域名解析到错误的 IP 地址。

类型

1. 本地 DNS 劫持

修改本机 hosts 文件或 DNS 设置。

2. 中间人攻击

劫持 DNS 请求,返回伪造响应。

3. DNS 服务器劫持

攻击 DNS 服务器,篡改解析记录。

防范措施

  1. 使用 HTTPS
  2. 使用 DNS over HTTPS (DoH)
  3. 使用可信 DNS 服务器
  4. 定期检查 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认证信息
CookieCookie
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, 2

2. 最小延迟

浏览器有最小延迟限制(通常 4ms):

javascript
setTimeout(() => {}, 0) // 实际至少 4ms

3. 主线程阻塞

如果主线程有长任务,会延迟执行:

javascript
setTimeout(() => console.log('timeout'), 0)

// 长任务
for (let i = 0; i < 1e9; i++) {}

// timeout 会等待长任务完成

4. 页面不可见

页面在后台时,定时器会被节流:

javascript
// 页面不可见时,最小延迟变为 1000ms
document.hidden // true

94. 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

基于 VitePress 的本地知识库