react


大卖点:Virtual DOM

Virtual DOM 是 React 维护在内存中的 DOM 副本,当组件状态变化时,React 会重新生成一棵虚拟 DOM,然后与旧的进行对比,找出差异,并高效地更新到真实 DOM 上。这样可以提升性能,减少不必要的 DOM 操作。

为什么要用 Virtual DOM?

传统的 DOM 操作很慢,尤其是在复杂页面中。
而 Virtual DOM 的优势是:

优势 说明
性能更高 减少了直接操作 DOM 的次数
跨平台可能性 虚拟 DOM 不依赖于浏览器,可以渲染到不同平台(比如 React Native)
结构清晰 用组件来组织结构,易维护、易调试

是什么?

Virtual DOM(虚拟 DOM)是 React 提出的一种“用 JavaScript 对页面结构的抽象表示”。你可以把它想象成是:浏览器真实 DOM 的轻量版副本,存在于内存中。

e.g. 假设你家有一棵树(真实 DOM),你想给它修剪枝叶(修改页面),你不会直接就去砍树,而是:

  1. 在纸上画一棵“草图”(Virtual DOM);
  2. 先在纸上改,试试看哪里剪比较好;
  3. 最后再对照草图,只剪真实树上必要的部分

Virtual DOM 就是那张草图,React 就是那个聪明的园丁。

怎么样操作?

Virtual DOM 工作流程(三步走):

  1. 渲染阶段: React 用 JSX 生成一棵 Virtual DOM 树;

  2. 更新阶段: 组件数据(state/props)变化后,React 会重新生成一棵新的 Virtual DOM;

  3. 对比 & 更新阶段:

    • React 会把新旧两棵 Virtual DOM 树进行“diff 比较”
    • 只把有变化的部分更新到真实 DOM 上。

这种机制叫做 Reconciliation(协调)


React 自定义组件为什么要大写?小写了会怎么样?

自定义组件必须大写开头,这样 React 才能正确地判断它是一个组件,而不是一个普通的 HTML 标签。

如果你把自定义组件的名称写成小写,比如 <myComponent />,React 会把它当作普通的 HTML 标签来处理。这样会导致:React 尝试查找一个名为 <myComponent> 的 HTML 标签,而实际上没有这样的标签。这样会触发错误或者渲染异常,导致组件无法正常显示。

总结:
大写:表示这是一个自定义的 React 组件,React 会正确地识别它并渲染。
小写:React 会认为它是一个普通的 HTML 元素,导致无法正常渲染自定义组件。


Redux

是什么?

一个专门存储数据的大本营(仓库),大家都可以从里面拿数据,也可以往里面改数据,但必须要按照一定的规矩来操作。

为什么用它?

我们做前端页面时,通常会遇到这个问题:

  • 组件 A 点了个按钮,数据变了;
  • 组件 B 要跟着变,但它俩没有直接关系
  • 数据变来变去,组件多了之后,很难理清到底是谁改了谁。

这时候就需要一个统一的“数据中转站”来管理状态,Redux 就是干这个的

怎么用?

  1. store(仓库)

就是放所有数据的地方。

javascript
const store = createStore(reducer);

你可以想象成一个储物柜,里面存的是整个 app 的状态

  1. state(状态)

这就是我们要保存的数据,像这样:

javascript
{
  count: 3,
  user: { name: '小明' }
}

组件不直接修改 state,而是通过“触发动作”。

  1. action(动作)

动作就像一张纸条,上面写着:“我要干什么”。

javascript
{
  type: 'INCREMENT';
}

或者更复杂一点:

javascript
{ type: 'LOGIN_SUCCESS', payload: { name: '小明' } }
  1. reducer(裁判)

Reducer 就像一个裁判,根据动作决定数据怎么变

它的逻辑像这样:

javascript
function reducer(state, action) {
  if (action.type === 'INCREMENT') {
    return { ...state, count: state.count + 1 };
  }
  return state;
}

所有“纸条”(action)都要交给裁判(reducer)来裁决,你不能直接篡改数据

你可以把 Redux 比喻成一个蛋糕店:

  • 🧁 顾客(组件)说:我要一个草莓蛋糕! → 发出 Action
  • 📋 店员(reducer)看单子:做一个草莓蛋糕 → 修改状态
  • 🗃️ 仓库(store)记录现在有几个蛋糕
  • 🧍‍♂️ 顾客(组件)看到新蛋糕做好了 → 自动更新页面展示
none
组件 -> 发出 Action(我要干嘛)
      -> Reducer(裁判:OK,那就这么改)
          -> Store(状态更新了)
              -> 组件订阅到更新,自动刷新

store 是如何分发的?

在 Redux 中,store 是应用程序状态的唯一来源。状态只能通过“dispatch”来改变。Redux 中的“dispatch”指的是分发一个action,该 action 描述了要对应用状态进行的变化。分发流程如下:

  1. Store 包含了应用的整个状态。
  2. dispatch 是用来分发 action 的方法。当我们调用 store.dispatch(action) 时,它将触发一次状态更新。
  3. Redux 使用 reducer 函数来计算新的状态。reducer 会接收当前状态和所分发的 action,并返回一个新的状态。
  4. 一旦状态被更新,Redux 会通知应用的所有组件重新渲染,确保视图与状态同步。

在实际的流程中,dispatch 是由用户交互、组件生命周期方法、或一些异步操作(如 thunk 中间件)触发的。

简而言之,dispatch 是将一个 action 传递给 Redux 的核心机制,而 store 通过 reducer 来根据 action 更新状态。


生命周期

React 生命周期是指组件从创建到销毁的整个过程,期间会经历一系列的钩子函数(生命周期方法)。
回答这个问题时,你可以根据面试官是否用的是 React 16 之前的 class 组件 还是 React 16.8+ 的函数组件(使用 Hooks) 来分别回答。下面给你两个版本的答法:

现代 React:函数组件 + Hooks

在函数组件中,React 生命周期主要通过 useEffectuseLayoutEffectuseRef 等 Hook 来管理:

  • 组件挂载(Mount)useEffect(() => { ... }, []) 在组件首次渲染后执行,类似于 componentDidMount
  • 组件更新(Update) :当依赖项变化时,useEffect 会再次执行,类似于 componentDidUpdate
  • 组件卸载(Unmount)useEffect 的返回函数会在组件卸载时执行,类似于 componentWillUnmount

React 函数组件本身不提供精确的“生命周期方法”,而是通过 Hooks 更加灵活地控制副作用逻辑。

旧版 React:class 组件

在 class 组件中,React 生命周期可以分为三个阶段:

1. 挂载阶段(Mount)

  • constructor():初始化状态。
  • render():渲染组件。
  • componentDidMount():组件挂载后执行,常用于请求数据、订阅事件。

2. 更新阶段(Update)

  • shouldComponentUpdate():控制是否重新渲染。
  • render():重新渲染。
  • componentDidUpdate():更新后执行,常用于响应 props/state 改变。

3. 卸载阶段(Unmount)

  • componentWillUnmount():组件被移除时调用,常用于清理副作用(如清除定时器、取消订阅)。

从 React 16 开始,一些旧的生命周期方法(如 componentWillMount)被标记为不推荐使用。


React 18 的亮点

React 18 引入了一些新的功能和行为,尤其是在并发模式(Concurrent Mode)和严格模式(Strict Mode)方面。以下是一些关键点的简述:

1. 并发模式(Concurrent Mode)

并发模式是 React 18 的一项重大更新,旨在使应用更具响应性。它允许 React 在低优先级的更新时“暂停”渲染,以便可以先处理更高优先级的任务。这种机制是通过“调度”机制来完成的,React 会根据用户的交互和渲染优先级动态调整工作。

  • 可中断渲染:并发模式通过使渲染过程可以中断并重新开始,确保在忙碌的 UI 更新过程中仍然能够响应用户输入(如点击、滚动等)。
  • Suspense 配合并发:并发模式与 Suspense 结合使用时,能够延迟组件的渲染,直到数据准备就绪。

2. 严格模式(Strict Mode)下的双调用 effect

在 React 18 中,React 在开发环境下启用了严格模式时,会对 useEffectuseLayoutEffect 的副作用进行“严格检查”,并在初次渲染和更新时各调用一次。这是为了帮助开发者发现副作用中的潜在问题(如副作用不清除、依赖项问题等)。

  • 双调用机制:React 在开发模式下,所有的 effect 会被调用两次:第一次是渲染时,第二次是在组件更新时。这是为了模拟实际的更新场景,确保副作用的清理和正确性。

    • 第一次调用:用于初始化副作用。
    • 第二次调用:用于清理和重新运行副作用,确保更新过程中副作用不会遗漏。

    注意:严格模式下的双调用仅在开发环境生效,生产环境不会执行此行为。

3. useId Hook

React 18 引入了 useId,它用于生成稳定且唯一的 ID。在并发渲染中,useId 生成的 ID 在每次渲染中不会变化,保证了在服务器渲染和客户端渲染中的一致性。

4. 自动批处理更新

React 18 还引入了自动批处理的更新机制,允许在事件处理程序、异步回调、setTimeout 等中进行多次状态更新,而无需手动调用 flushSync 来确保它们批处理。这提高了性能并减少了不必要的渲染。

5. useTransition startTransition

React 18 提供了 useTransitionstartTransition 来帮助管理 UI 的加载优先级。当你需要在响应性和渲染效率之间做出平衡时,可以使用这些 API。它们允许你标记一些更新为“低优先级”,让 React 优先处理用户交互。

这些功能使得 React 应用在渲染和性能优化上更具灵活性和响应能力,尤其是在面对大型和复杂的 UI 时。


从 React 16 升级到 18 的挑战

一、 createRoot 取代 ReactDOM.render

javascript
// React 17 之前:
ReactDOM.render(<App />, document.getElementById('root'));

// React 18 之后:
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);

这影响了像 Redux、React Router 等依赖 DOM 渲染生命周期的库。

二、Redux 使用上的变化或问题

javascript
const store = createStore(reducer); // 老写法

搭配 react-redux@7 可能会有一些兼容问题。

✅ 推荐升级 Redux 工具链:

推荐版本
redux 4.2+
react-redux 8.x(React 18 支持更好)
redux-toolkit 1.9+(更推荐用这个代替手写 reducer)

新版写法:

javascript
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: yourReducer,
});

配合 ProvideruseSelector/useDispatch 更加稳定。

如果你遇到 Redux 相关的问题(如 useSelector 不更新,或 dispatch 行为怪异):

  1. 检查是否升级了 react-redux@8
  2. 避免在老的 ReactDOM.render 模式下继续写新代码;
  3. 考虑迁移到 @reduxjs/toolkit,可减少样板代码;
  4. 关闭开发环境下 StrictMode 临时排查副作用 bug。

对比 React 和 Vue

1. JSX 与模板

  • React: 使用 JSX(JavaScript XML)语法,允许你在 JavaScript 中写 HTML 风格的代码,但实际上它是一个 JavaScript 对象的表示。JSX 的优势在于它将模板与逻辑紧密结合,方便开发者在同一地方处理视图和逻辑。

    javascript
    const element = <h1>Hello, {name}</h1>;
  • Vue: 使用 模板语法,类似传统的 HTML,其中包含了 Vue 特有的指令(如 v-if, v-for)和绑定(如 v-bind, v-model)。它使得 HTML、CSS 和 JavaScript 分开,易于理解,特别是对新手友好。

    markup
    <template>
      <h1>{{ message }}</h1>
    </template>

2. 数据绑定与响应式系统

  • React: React 使用 单向数据流,数据从父组件流向子组件。状态管理是通过 useStateuseReducer 或外部库(如 Redux)来处理的。它的重新渲染是基于虚拟 DOM 和 diff 算法的。
  • Vue: Vue 提供了 双向数据绑定,即通过 v-model 实现视图与数据的同步。Vue 的响应式系统通过 Vue 实例的观察者模式来实现,自动追踪数据变化,并更新视图。

3. 状态管理

  • React: 默认不提供内建的状态管理方案,通常使用 useStateuseReducer 进行局部状态管理,复杂的全局状态管理通常需要使用 ReduxMobXRecoil 等库。
  • Vue: Vue 提供了内建的 Vuex 库用于全局状态管理,Vuex 简单易用,且与 Vue 的响应式系统非常契合。Vue 3 还引入了 Composition API,使得状态管理更加灵活。

受控 与 非受控组件

在 React 中,受控组件是由 React 的 state 完全控制其值的表单元素,而非受控组件则是通过 DOM 引用(ref) 直接访问其值的表单元素。

类型 控制方式 获取值方式 场景举例
受控组件 React state stateonChange 受表单状态驱动、统一数据流管理
非受控组件 DOM 自身状态(Ref) ref.current.value 快速 demo、小型项目或性能优化场景

一般推荐使用受控组件,因为它更符合 React 的单向数据流理念,更容易做校验和状态管理。但非受控组件在一些场景下更简洁高效。

受控组件

jsx
const [value, setValue] = useState('');

<input value={value} onChange={(e) => setValue(e.target.value)} />;

非受控组件:

jsx
const inputRef = useRef(null);

<input ref={inputRef} />
<button onClick={() => console.log(inputRef.current.value)}>获取值</button>

单向数据流理念

数据只能从父组件传到子组件,不能反过来,整个 UI 状态的流向是单向的、可预测的

例子

jsx
function Parent() {
  const [message, setMessage] = useState('Hello');

  return <Child text={message} />;
}

function Child({ text }) {
  return <p>{text}</p>;
}
  • 这里 Parentmessage 通过 props 传给 Child
  • 如果 Child 想修改 message它不能直接改,只能触发回调让 Parent 去改(比如传一个 onChange 方法下来)。

单向数据流 VS 双向数据绑定(比如 Vue)

概念 React 单向数据流 Vue 双向绑定(v-model)
数据流方向 父 ➜ 子 父 ⇄ 子
状态修改方式 必须通过 setState 或父回调 v-model 自动绑定
好处 数据流向清晰、容易调试 写法更简洁,但大型项目易混乱

useRef

useRef 是 React 提供的 Hook,用来获取 DOM 节点引用,或保存一个在组件生命周期内持续存在的可变值。改变 ref.current 不会引起组件重新渲染。

useRef 用来创建一个可以在组件生命周期中持续存在的可变对象,通常有两个核心用途:

获取 DOM 元素引用(最常见)

jsx
import { useRef, useEffect } from 'react';

function MyComponent() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // 自动聚焦
  }, []);

  return <input ref={inputRef} />;
}

👉 inputRef.current 就是指向 <input> 的真实 DOM 节点。

保存变量,不会引发组件重新渲染

jsx
function Counter() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current++;
    console.log('点击了', countRef.current);
  };

  return <button onClick={handleClick}>点我</button>;
}
  • 它不会因为 .current 变化而重新渲染组件
  • 适合用来存储像 定时器 ID、上一次的值、状态缓存 之类的东西

useMemo 和 useCallback 的区别

useMemo 是为了避免重复“算值”,useCallback 是为了避免重复“造函数”。

  • useMemo: 缓存值
  • useCallback: 缓存函数
Hook 作用 返回值 适用场景
useMemo 缓存计算结果 任何值(对象、数组、计算结果) 计算开销大的值,避免重复计算
useCallback 缓存函数引用 一个函数 避免组件重复渲染时函数重新创建,特别是传给子组件时

useMemo:避免重复计算

jsx
const expensiveValue = useMemo(() => {
  return heavyCalculation(input); // 假设这是一个非常慢的函数
}, [input]);
  • 只有 input 变化时才重新执行 heavyCalculation
  • 否则返回缓存的结果;
  • 用于优化性能(比如复杂的排序、过滤、数学运算)。

useCallback:避免函数重新创建

jsx
const handleClick = useCallback(() => {
  console.log('Clicked', count);
}, [count]);
  • 每次渲染组件时,JS 默认会重新生成新函数(函数是引用类型);
  • 如果这个函数被作为 prop 传给子组件,可能导致 子组件不必要的重新渲染
  • useCallback 缓存函数引用,除非依赖 count 发生变化。

注意事项

  • 不要过度使用!它们本身也有开销(需要记录依赖和缓存);
  • 只在性能真的有问题或组件频繁渲染时才用;
  • 对于简单的函数或计算,不使用反而更快。

react router

是什么?

React Router 是 React 里用来实现“单页应用(SPA)中的页面切换”的工具。

说白了,它让你点不同链接时,不刷新页面,也能显示不同的组件,就像“换页”一样。

核心原理

监听地址变化,切换组件:

React Router 的工作原理是监听浏览器地址栏的变化(使用 History API 或 hash),然后根据配置好的路由表,找到匹配的组件并渲染。整个过程不会刷新页面,从而实现了单页应用中的“页面切换”。

  1. 浏览器的地址(URL)变了
  2. React Router 发现地址变了,就去看看哪条“路由规则”匹配这个地址
  3. 找到了,就把对应的 React 组件渲染出来

这个过程都发生在浏览器里,不会重新向服务器发请求,不刷新页面。

❓1. 为什么地址变了页面不会刷新?

答:因为用了 HTML5 的 History API(如 pushState),只是改变地址栏,不会触发页面重载

❓2. 和传统网页跳转有什么区别?

答:传统跳转会刷新整个页面,请求新 HTML;而 React Router 只是组件切换,页面仍然是同一个,不刷新。

❓3. 地址变了,它是怎么知道的?

答:React Router 会监听浏览器的地址变化(比如点击 <Link to="/about" /> 触发了 history.pushState),它内部注册了一个监听器,每当路径改变就触发重新渲染。

两种模式(React Router Dom)

React Router 有两种工作模式:

模式 特点 地址栏变化方式
BrowserRouter 用的是 HTML5 的history.pushState,地址栏会像正常网址一样变 比如/about
HashRouter 用的是#后面的 hash,适合旧浏览器或静态文件托管 比如/#/about

举个例子:

jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='/' element={<Home />} />
        <Route path='/about' element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

如果用户访问 /about,React Router 会:

  1. 看到路径是 /about
  2. 找到 <Route path="/about">
  3. 渲染 <About /> 组件
  4. 不刷新页面!

常用 Hooks 对比速查表

useState 控制状态,useEffect 处理副作用,useRef 保存 DOM 或变量,useMemo 缓存计算结果,useCallback 缓存函数引用。它们配合使用,可以优化组件性能、控制渲染和提升用户体验。

Hook 作用 是否触发重新渲染 常见用途
useState 定义组件状态 ✅ 会 计数器、表单输入、组件状态控制等
useEffect 副作用处理(生命周期) ❌ 不直接触发 请求数据、事件监听、定时器等
useRef 引用 DOM 或保存可变值 ❌ 不会 获取 DOM 节点、保存 timer/上一次值等
useMemo 计算值缓存 ❌ 不会 优化性能,避免重复计算
useCallback 缓存函数引用(防止子组件重渲染) ❌ 不会 传递函数 props 给子组件时优化性能

useState

jsx
const [count, setCount] = useState(0);

useEffect

jsx
useEffect(() => {
  console.log('组件挂载或 count 变化');
}, [count]);

useRef

jsx
const inputRef = useRef(null);
// inputRef.current 表示 DOM 节点

useMemo

jsx
const result = useMemo(() => expensiveCalculation(num), [num]);
// num 不变时,不重新计算

useCallback

jsx
const handleClick = useCallback(() => {
  doSomething();
}, [value]);
// 函数不变,避免子组件重复渲染

文章作者: Citrus
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Citrus
晚上使用黑夜模式阅读能够减轻视觉疲劳。