大卖点: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),你想给它修剪枝叶(修改页面),你不会直接就去砍树,而是:
- 在纸上画一棵“草图”(Virtual DOM);
- 先在纸上改,试试看哪里剪比较好;
- 最后再对照草图,只剪真实树上必要的部分。
Virtual DOM 就是那张草图,React 就是那个聪明的园丁。
怎么样操作?
Virtual DOM 工作流程(三步走):
渲染阶段: React 用 JSX 生成一棵 Virtual DOM 树;
更新阶段: 组件数据(state/props)变化后,React 会重新生成一棵新的 Virtual DOM;
对比 & 更新阶段:
- React 会把新旧两棵 Virtual DOM 树进行“diff 比较” ;
- 只把有变化的部分更新到真实 DOM 上。
这种机制叫做 Reconciliation(协调)
React 自定义组件为什么要大写?小写了会怎么样?
自定义组件必须大写开头,这样 React 才能正确地判断它是一个组件,而不是一个普通的 HTML 标签。
如果你把自定义组件的名称写成小写,比如 <myComponent />,React 会把它当作普通的 HTML 标签来处理。这样会导致:React 尝试查找一个名为 <myComponent> 的 HTML 标签,而实际上没有这样的标签。这样会触发错误或者渲染异常,导致组件无法正常显示。
总结:
大写:表示这是一个自定义的 React 组件,React 会正确地识别它并渲染。
小写:React 会认为它是一个普通的 HTML 元素,导致无法正常渲染自定义组件。
Redux
是什么?
一个专门存储数据的大本营(仓库),大家都可以从里面拿数据,也可以往里面改数据,但必须要按照一定的规矩来操作。
为什么用它?
我们做前端页面时,通常会遇到这个问题:
- 组件 A 点了个按钮,数据变了;
- 组件 B 要跟着变,但它俩没有直接关系;
- 数据变来变去,组件多了之后,很难理清到底是谁改了谁。
这时候就需要一个统一的“数据中转站”来管理状态,Redux 就是干这个的。
怎么用?
store(仓库)
就是放所有数据的地方。
const store = createStore(reducer);
你可以想象成一个储物柜,里面存的是整个 app 的状态。
state(状态)
这就是我们要保存的数据,像这样:
{
count: 3,
user: { name: '小明' }
}
组件不直接修改 state,而是通过“触发动作”。
action(动作)
动作就像一张纸条,上面写着:“我要干什么”。
{
type: 'INCREMENT';
}
或者更复杂一点:
{ type: 'LOGIN_SUCCESS', payload: { name: '小明' } }
reducer(裁判)
Reducer 就像一个裁判,根据动作决定数据怎么变。
它的逻辑像这样:
function reducer(state, action) {
if (action.type === 'INCREMENT') {
return { ...state, count: state.count + 1 };
}
return state;
}
所有“纸条”(action)都要交给裁判(reducer)来裁决,你不能直接篡改数据。
你可以把 Redux 比喻成一个蛋糕店:
- 🧁 顾客(组件)说:我要一个草莓蛋糕! → 发出 Action
- 📋 店员(reducer)看单子:做一个草莓蛋糕 → 修改状态
- 🗃️ 仓库(store)记录现在有几个蛋糕
- 🧍♂️ 顾客(组件)看到新蛋糕做好了 → 自动更新页面展示
组件 -> 发出 Action(我要干嘛)
-> Reducer(裁判:OK,那就这么改)
-> Store(状态更新了)
-> 组件订阅到更新,自动刷新
store 是如何分发的?
在 Redux 中,store 是应用程序状态的唯一来源。状态只能通过“dispatch”来改变。Redux 中的“dispatch”指的是分发一个action,该 action 描述了要对应用状态进行的变化。分发流程如下:
- Store 包含了应用的整个状态。
- dispatch 是用来分发 action 的方法。当我们调用
store.dispatch(action)时,它将触发一次状态更新。 - Redux 使用 reducer 函数来计算新的状态。reducer 会接收当前状态和所分发的 action,并返回一个新的状态。
- 一旦状态被更新,Redux 会通知应用的所有组件重新渲染,确保视图与状态同步。
在实际的流程中,dispatch 是由用户交互、组件生命周期方法、或一些异步操作(如 thunk 中间件)触发的。
简而言之,dispatch 是将一个 action 传递给 Redux 的核心机制,而 store 通过 reducer 来根据 action 更新状态。
生命周期
React 生命周期是指组件从创建到销毁的整个过程,期间会经历一系列的钩子函数(生命周期方法)。
回答这个问题时,你可以根据面试官是否用的是 React 16 之前的 class 组件 还是 React 16.8+ 的函数组件(使用 Hooks) 来分别回答。下面给你两个版本的答法:
现代 React:函数组件 + Hooks
在函数组件中,React 生命周期主要通过
useEffect、useLayoutEffect、useRef等 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 在开发环境下启用了严格模式时,会对 useEffect 和 useLayoutEffect 的副作用进行“严格检查”,并在初次渲染和更新时各调用一次。这是为了帮助开发者发现副作用中的潜在问题(如副作用不清除、依赖项问题等)。
双调用机制:React 在开发模式下,所有的
effect会被调用两次:第一次是渲染时,第二次是在组件更新时。这是为了模拟实际的更新场景,确保副作用的清理和正确性。- 第一次调用:用于初始化副作用。
- 第二次调用:用于清理和重新运行副作用,确保更新过程中副作用不会遗漏。
注意:严格模式下的双调用仅在开发环境生效,生产环境不会执行此行为。
3. useId Hook
React 18 引入了 useId,它用于生成稳定且唯一的 ID。在并发渲染中,useId 生成的 ID 在每次渲染中不会变化,保证了在服务器渲染和客户端渲染中的一致性。
4. 自动批处理更新
React 18 还引入了自动批处理的更新机制,允许在事件处理程序、异步回调、setTimeout 等中进行多次状态更新,而无需手动调用 flushSync 来确保它们批处理。这提高了性能并减少了不必要的渲染。
5. useTransition 和 startTransition
React 18 提供了 useTransition 和 startTransition 来帮助管理 UI 的加载优先级。当你需要在响应性和渲染效率之间做出平衡时,可以使用这些 API。它们允许你标记一些更新为“低优先级”,让 React 优先处理用户交互。
这些功能使得 React 应用在渲染和性能优化上更具灵活性和响应能力,尤其是在面对大型和复杂的 UI 时。
从 React 16 升级到 18 的挑战
一、 createRoot 取代 ReactDOM.render
// 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 使用上的变化或问题
const store = createStore(reducer); // 老写法
搭配 react-redux@7 可能会有一些兼容问题。
✅ 推荐升级 Redux 工具链:
库 推荐版本 redux 4.2+ react-redux 8.x(React 18 支持更好) redux-toolkit 1.9+(更推荐用这个代替手写 reducer)
新版写法:
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: yourReducer,
});
配合 Provider 和 useSelector/useDispatch 更加稳定。
如果你遇到 Redux 相关的问题(如
useSelector不更新,或dispatch行为怪异):
- 检查是否升级了
react-redux@8;- 避免在老的
ReactDOM.render模式下继续写新代码;- 考虑迁移到
@reduxjs/toolkit,可减少样板代码;- 关闭开发环境下
StrictMode临时排查副作用 bug。
对比 React 和 Vue
1. JSX 与模板
React: 使用 JSX(JavaScript XML)语法,允许你在 JavaScript 中写 HTML 风格的代码,但实际上它是一个 JavaScript 对象的表示。JSX 的优势在于它将模板与逻辑紧密结合,方便开发者在同一地方处理视图和逻辑。
const element = <h1>Hello, {name}</h1>;Vue: 使用 模板语法,类似传统的 HTML,其中包含了 Vue 特有的指令(如
v-if,v-for)和绑定(如v-bind,v-model)。它使得 HTML、CSS 和 JavaScript 分开,易于理解,特别是对新手友好。<template> <h1>{{ message }}</h1> </template>
2. 数据绑定与响应式系统
- React: React 使用 单向数据流,数据从父组件流向子组件。状态管理是通过
useState、useReducer或外部库(如 Redux)来处理的。它的重新渲染是基于虚拟 DOM 和 diff 算法的。 - Vue: Vue 提供了 双向数据绑定,即通过
v-model实现视图与数据的同步。Vue 的响应式系统通过 Vue 实例的观察者模式来实现,自动追踪数据变化,并更新视图。
3. 状态管理
- React: 默认不提供内建的状态管理方案,通常使用
useState或useReducer进行局部状态管理,复杂的全局状态管理通常需要使用 Redux、MobX、Recoil 等库。 - Vue: Vue 提供了内建的 Vuex 库用于全局状态管理,Vuex 简单易用,且与 Vue 的响应式系统非常契合。Vue 3 还引入了 Composition API,使得状态管理更加灵活。
受控 与 非受控组件
在 React 中,受控组件是由 React 的 state 完全控制其值的表单元素,而非受控组件则是通过 DOM 引用(ref) 直接访问其值的表单元素。
| 类型 | 控制方式 | 获取值方式 | 场景举例 |
|---|---|---|---|
| 受控组件 | React state | state或onChange |
受表单状态驱动、统一数据流管理 |
| 非受控组件 | DOM 自身状态(Ref) | ref.current.value |
快速 demo、小型项目或性能优化场景 |
一般推荐使用受控组件,因为它更符合 React 的单向数据流理念,更容易做校验和状态管理。但非受控组件在一些场景下更简洁高效。
受控组件
const [value, setValue] = useState('');
<input value={value} onChange={(e) => setValue(e.target.value)} />;
非受控组件:
const inputRef = useRef(null);
<input ref={inputRef} />
<button onClick={() => console.log(inputRef.current.value)}>获取值</button>
单向数据流理念
数据只能从父组件传到子组件,不能反过来,整个 UI 状态的流向是单向的、可预测的。
例子
function Parent() {
const [message, setMessage] = useState('Hello');
return <Child text={message} />;
}
function Child({ text }) {
return <p>{text}</p>;
}
- 这里
Parent把message通过 props 传给Child。 - 如果
Child想修改message,它不能直接改,只能触发回调让 Parent 去改(比如传一个onChange方法下来)。
单向数据流 VS 双向数据绑定(比如 Vue)
| 概念 | React 单向数据流 | Vue 双向绑定(v-model) |
|---|---|---|
| 数据流方向 | 父 ➜ 子 | 父 ⇄ 子 |
| 状态修改方式 | 必须通过 setState 或父回调 | v-model 自动绑定 |
| 好处 | 数据流向清晰、容易调试 | 写法更简洁,但大型项目易混乱 |
useRef
useRef 是 React 提供的 Hook,用来获取 DOM 节点引用,或保存一个在组件生命周期内持续存在的可变值。改变 ref.current 不会引起组件重新渲染。
useRef 用来创建一个可以在组件生命周期中持续存在的可变对象,通常有两个核心用途:
获取 DOM 元素引用(最常见)
import { useRef, useEffect } from 'react';
function MyComponent() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 自动聚焦
}, []);
return <input ref={inputRef} />;
}
👉 inputRef.current 就是指向 <input> 的真实 DOM 节点。
保存变量,不会引发组件重新渲染
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:避免重复计算
const expensiveValue = useMemo(() => {
return heavyCalculation(input); // 假设这是一个非常慢的函数
}, [input]);
- 只有
input变化时才重新执行heavyCalculation; - 否则返回缓存的结果;
- 用于优化性能(比如复杂的排序、过滤、数学运算)。
useCallback:避免函数重新创建
const handleClick = useCallback(() => {
console.log('Clicked', count);
}, [count]);
- 每次渲染组件时,JS 默认会重新生成新函数(函数是引用类型);
- 如果这个函数被作为 prop 传给子组件,可能导致 子组件不必要的重新渲染;
- 用
useCallback缓存函数引用,除非依赖count发生变化。
注意事项
- 不要过度使用!它们本身也有开销(需要记录依赖和缓存);
- 只在性能真的有问题或组件频繁渲染时才用;
- 对于简单的函数或计算,不使用反而更快。
react router
是什么?
React Router 是 React 里用来实现“单页应用(SPA)中的页面切换”的工具。
说白了,它让你点不同链接时,不刷新页面,也能显示不同的组件,就像“换页”一样。
核心原理
监听地址变化,切换组件:
React Router 的工作原理是监听浏览器地址栏的变化(使用 History API 或 hash),然后根据配置好的路由表,找到匹配的组件并渲染。整个过程不会刷新页面,从而实现了单页应用中的“页面切换”。
- 浏览器的地址(URL)变了
- React Router 发现地址变了,就去看看哪条“路由规则”匹配这个地址
- 找到了,就把对应的 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 |
举个例子:
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 会:
- 看到路径是
/about - 找到
<Route path="/about"> - 渲染
<About />组件 - 不刷新页面!
常用 Hooks 对比速查表
useState 控制状态,useEffect 处理副作用,useRef 保存 DOM 或变量,useMemo 缓存计算结果,useCallback 缓存函数引用。它们配合使用,可以优化组件性能、控制渲染和提升用户体验。
| Hook | 作用 | 是否触发重新渲染 | 常见用途 |
|---|---|---|---|
useState |
定义组件状态 | ✅ 会 | 计数器、表单输入、组件状态控制等 |
useEffect |
副作用处理(生命周期) | ❌ 不直接触发 | 请求数据、事件监听、定时器等 |
useRef |
引用 DOM 或保存可变值 | ❌ 不会 | 获取 DOM 节点、保存 timer/上一次值等 |
useMemo |
计算值缓存 | ❌ 不会 | 优化性能,避免重复计算 |
useCallback |
缓存函数引用(防止子组件重渲染) | ❌ 不会 | 传递函数 props 给子组件时优化性能 |
useState
const [count, setCount] = useState(0);
useEffect
useEffect(() => {
console.log('组件挂载或 count 变化');
}, [count]);
useRef
const inputRef = useRef(null);
// inputRef.current 表示 DOM 节点
useMemo
const result = useMemo(() => expensiveCalculation(num), [num]);
// num 不变时,不重新计算
useCallback
const handleClick = useCallback(() => {
doSomething();
}, [value]);
// 函数不变,避免子组件重复渲染