前端面试-react专项
大约 29 分钟约 8728 字
前端面试-react专项
1、react 生命周期函数
初始化阶段:
getDefaultProps:获取实例的默认属性
getInitialState:获取每个实例的初始化状态
componentWillMount:组件即将被装载、渲染到页面上
render:组件在这里生成虚拟的 DOM 节点
componentDidMount:组件真正在被装载之后
运行中状态:
componentWillReceiveProps:组件将要接收到属性的时候调用
shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止 render 调用,后面的函数不会被继续执行了)
componentWillUpdate:组件即将更新不能修改属性和状态
render:组件重新描绘 componentDidUpdate:组件已经更新
销毁阶段:
componentWillUnmount:组件即将销毁生命周期详细说明与实际应用:
// React 16.3 之前的生命周期(Legacy)
class MyComponent extends React.Component {
// 1. 挂载阶段
constructor(props) {
super(props);
this.state = { count: 0 };
console.log('1. constructor: 初始化 state 和绑定方法');
}
// 已废弃:componentWillMount
// 替代:constructor 或 componentDidMount
componentWillMount() {
console.log('2. componentWillMount: 组件即将挂载(已废弃)');
}
render() {
console.log('3. render: 生成虚拟 DOM');
return <div>{this.state.count}</div>;
}
componentDidMount() {
console.log('4. componentDidMount: 组件已挂载');
// 常见用途:
// - 发起网络请求
// - 订阅事件
// - 操作 DOM
// - 初始化第三方库
fetch('/api/data').then(res => res.json()).then(data => {
this.setState({ data });
});
window.addEventListener('resize', this.handleResize);
}
// 2. 更新阶段
componentWillReceiveProps(nextProps) {
console.log('5. componentWillReceiveProps: 接收新 props(已废弃)');
// 已被 getDerivedStateFromProps 替代
}
shouldComponentUpdate(nextProps, nextState) {
console.log('6. shouldComponentUpdate: 是否需要更新');
// 返回 false 可以阻止不必要的渲染
// 常用于性能优化
return nextState.count !== this.state.count;
}
componentDidUpdate(prevProps, prevState) {
console.log('7. componentDidUpdate: 组件已更新');
// 常见用途:
// - 根据 props 变化发起网络请求
// - 操作更新后的 DOM
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}
// 3. 卸载阶段
componentWillUnmount() {
console.log('8. componentWillUnmount: 组件即将卸载');
// 常见用途:
// - 清除定时器
// - 取消网络请求
// - 移除事件监听
window.removeEventListener('resize', this.handleResize);
}
// 4. 错误处理
componentDidCatch(error, info) {
console.log('9. componentDidCatch: 捕获子组件错误');
this.setState({ hasError: true });
}
}React 16.3+ 新增的生命周期:
// 新增的静态方法 getDerivedStateFromProps
// 用于替代 componentWillReceiveProps
static getDerivedStateFromProps(props, state) {
// 返回新的 state 对象,或者 null 表示不更新
if (props.id !== state.prevId) {
return {
prevId: props.id,
data: null // 重置数据
};
}
return null;
}
// 新增的 getSnapshotBeforeUpdate
// 在 DOM 更新之前调用,返回值作为 componentDidUpdate 的第三个参数
getSnapshotBeforeUpdate(prevProps, prevState) {
// 常用于获取 DOM 更新前的信息(如滚动位置)
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
this.listRef.current.scrollTop =
this.listRef.current.scrollHeight - snapshot;
}
}被废弃的生命周期及替代方案:
废弃的生命周期 → 替代方案
componentWillMount → constructor 或 componentDidMount
componentWillReceiveProps → getDerivedStateFromProps
componentWillUpdate → getSnapshotBeforeUpdate
废弃原因:
- 这些生命周期经常被误用,导致 bug
- 在 Fiber 架构下,这些方法可能被多次调用
- 新的异步渲染模式需要可中断的生命周期2、React类组件(Class component)和函数式组件(Functional component)之间有何不同
类组件不仅允许使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态。
当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 '无状态组件',可以使用一个纯函数来创建这样的组件。这种组件也被称为哑组件或展示组件。深度对比:
// 类组件
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prev => ({ count: prev.count + 1 }));
}
componentDidMount() {
document.title = `点击了 ${this.state.count} 次`;
}
componentDidUpdate() {
document.title = `点击了 ${this.state.count} 次`;
}
render() {
return (
<div>
<h1>你好,{this.props.name}</h1>
<p>计数: {this.state.count}</p>
<button onClick={this.handleClick}>点击</button>
</div>
);
}
}
// 函数式组件(推荐)
function Welcome({ name }) {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
document.title = `点击了 ${count} 次`;
}, [count]);
return (
<div>
<h1>你好,{name}</h1>
<p>计数: {count}</p>
<button onClick={() => setCount(c => c + 1)}>点击</button>
</div>
);
}对比维度:
┌──────────────┬────────────────────┬────────────────────┐
│ 特性 │ 类组件 │ 函数式组件 │
├──────────────┼────────────────────┼────────────────────┤
│ 状态管理 │ this.state │ useState │
│ 生命周期 │ 生命周期方法 │ useEffect │
│ this 关键字 │ 需要理解 this │ 不需要 this │
│ 代码量 │ 较多(样板代码) │ 较少(简洁) │
│ 性能优化 │ shouldComponentUpdate │ React.memo │
│ 逻辑复用 │ HOC / Render Props │ Custom Hooks │
│ 调试 │ 较难 │ 较易 │
│ 可测试性 │ 需要实例化 │ 纯函数,易于测试 │
│ 未来方向 │ 不会被移除但不再推荐 │ 官方推荐 │
└──────────────┴────────────────────┴────────────────────┘
选择建议:
- 新项目:优先使用函数式组件 + Hooks
- 旧项目:逐步迁移到函数式组件
- 需要错误边界(Error Boundary):仍需类组件
- 需要继承:使用类组件3、React状态(state)和属性(props)之间有何不同
State 是一种数据结构,用于组件挂载时所需数据的默认值。State 可能会随着时间的推移而发生突变,但多数时候是作为用户事件行为的结果。
Props则是组件的配置。props 由父组件传递给子组件,并且就子组件而言,props 是不可变的。组件不能改变自身的 props,但是可以把其子组件的 props 放在一起(统一管理)。Props也不仅仅是数据--回调函数也可以通过 props 传递。详细对比和代码示例:
// State 和 Props 的区别
function ParentComponent() {
// State: 组件内部管理的数据
const [user, setUser] = useState({ name: '张三', age: 25 });
const [theme, setTheme] = useState('dark');
// Props 传递给子组件
return (
<div>
{/* name 和 age 是 Props(从父传给子) */}
<ChildComponent name={user.name} age={user.age} />
{/* onChange 是 Props(回调函数) */}
<ChildComponent
name={user.name}
onNameChange={(newName) => setUser({...user, name: newName})}
/>
{/* 主题是 State(父组件管理) */}
<ThemeSwitcher theme={theme} onThemeChange={setTheme} />
</div>
);
}
function ChildComponent({ name, age, onNameChange }) {
// name 和 age 是 Props — 只读,不能修改
// 只能通过调用 onNameChange 来通知父组件修改
return (
<div>
<p>姓名: {name}</p>
<p>年龄: {age}</p>
<input
value={name}
onChange={(e) => onNameChange(e.target.value)}
/>
</div>
);
}State vs Props 核心区别:
State:
- 组件内部管理的数据
- 可变(通过 setState 或 useState 修改)
- 由组件自身初始化和管理
- 改变会触发组件重新渲染
Props:
- 从父组件传递给子组件的数据
- 只读(子组件不能直接修改)
- 由父组件控制
- 改变也会触发子组件重新渲染
单向数据流:
- Props 从父组件流向子组件(单向)
- 子组件通过回调函数通知父组件修改数据
- 父组件修改 State 后,新的 Props 会传递给子组件4、什么是高阶组件
高阶组件是一个以组件为参数并返回一个新组件的函数。最常见的就是 Redux 的 connect 函数。除了简单分享工具库和简单的组合,HOC 最好的方式是共享 React 组件之间的行为。如果发现不同的地方写了大量代码来做同一件事时,就可以用 HOC。高阶组件的详细实现:
// 1. 基础高阶组件:属性代理
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const isLoggedIn = useAuth();
if (!isLoggedIn) {
return <Navigate to="/login" />;
}
return <WrappedComponent {...props} />;
};
}
// 使用
const ProtectedPage = withAuth(DashboardPage);
// 2. 反向继承高阶组件
function withLogging(WrappedComponent) {
return class extends WrappedComponent {
componentDidMount() {
console.log(`${WrappedComponent.name} 已挂载`);
super.componentDidMount?.();
}
render() {
return super.render();
}
};
}
// 3. 实际应用:Redux connect 就是一个 HOC
import { connect } from 'react-redux';
const mapStateToProps = (state) => ({
user: state.user,
isLoading: state.loading,
});
const mapDispatchToProps = {
login,
logout,
};
// connect 返回一个 HOC
const EnhancedComponent = connect(mapStateToProps, mapDispatchToProps)(MyComponent);
// 4. 实际应用:权限控制 HOC
function withPermission(requiredPermission) {
return function(WrappedComponent) {
return function PermissionGuard(props) {
const { permissions } = useAuth();
const hasPermission = permissions.includes(requiredPermission);
if (!hasPermission) {
return <ForbiddenPage />;
}
return <WrappedComponent {...props} />;
};
};
}
// 使用
const AdminPanel = withPermission('admin')(AdminDashboard);
const EditArticle = withPermission('article:edit')(ArticleEditor);HOC 的注意事项:
HOC 的缺点:
1. 命名冲突:多个 HOC 可能传递同名的 props
2. 调试困难:组件嵌套层级深,DevTools 中难以追踪
3. 不透明性:难以理解 props 的来源
4. 复用性限制:无法在条件语句中使用 Hooks
HOC 的替代方案(Hooks 时代):
1. Custom Hooks — 替代 HOC 的逻辑复用
2. Render Props — 替代 HOC 的组件增强
3. Context + Provider — 替代跨层级传递 props
// 用 Custom Hook 替代 HOC
function useAuth() {
const [user, setUser] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
// 验证 token
setIsLoggedIn(true);
}
}, []);
return { user, isLoggedIn };
}
// 在组件中直接使用
function DashboardPage() {
const { isLoggedIn } = useAuth();
if (!isLoggedIn) return <Navigate to="/login" />;
return <div>Dashboard</div>;
}5、为什么建议传递给 setState 的参数是一个 callback 而不是一个对象
因为 this.props 和 this.state 的更新可能是异步的,不能依赖它们的值去计算下一个 state。详细解释和代码示例:
// 错误写法:基于当前 state 计算新 state
this.setState({ count: this.state.count + 1 });
// 如果连续调用多次 setState,可能产生不正确的结果
// 因为 setState 是异步的,this.state.count 可能还没更新
// 正确写法:使用函数式更新
this.setState(prevState => ({ count: prevState.count + 1 }));
// prevState 保证是最新值,即使连续调用多次也正确
// 连续调用示例
handleClick() {
// 错误:连续调用可能只 +1 而不是 +3
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 正确:每次都基于最新的 state 更新,结果 +3
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
}
// setState 的第二个参数(回调函数)
this.setState(
{ count: this.state.count + 1 },
() => {
// 在 state 更新完成后执行
console.log('count 已更新为:', this.state.count);
// 适合在更新后操作 DOM 或发请求
}
);
// Hooks 中的类似模式
const [count, setCount] = useState(0);
// 错误
setCount(count + 1);
setCount(count + 1);
// 结果:count + 1(因为闭包中的 count 是旧值)
// 正确
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 结果:count + 2(每次都基于最新值)6、(在构造函数中)调用 super(props) 的目的是什么
在 super() 被调用之前,子类是不能使用 this 的,在 ES2015 中,子类必须在 constructor 中调用 super()。传递 props 给 super() 的原因则是便于在子类中能在 constructor 访问 this.props。详细解释:
// 正确写法
class MyButton extends React.Component {
constructor(props) {
super(props); // 必须调用
// 调用 super(props) 后才能使用 this.props
console.log(this.props.name); // 正确
}
}
// 如果不传 props
class MyButton extends React.Component {
constructor(props) {
super(); // 内部仍然会设置 props,但在 constructor 中不可用
console.log(this.props); // undefined
console.log(props); // 正确
}
render() {
// render 中 this.props 是可用的
// React 在外部会自动设置 props
console.log(this.props.name); // 正确
}
}
// React 官方解释:
// 在构造函数中调用 super(props) 的原因是为了让 constructor 中
// 能够通过 this.props 访问传入的 props
// 如果不传 props,虽然 render 和其他方法中 this.props 仍然可用,
// 但 constructor 中 this.props 会是 undefined7、React事件处理
React中的事件处理程序将传递SyntheticEvent实例,该实例是React跨浏览器本机事件的跨浏览器包装器。这些综合事件具有与您惯用的本机事件相同的界面,除了它们在所有浏览器中的工作方式相同。
React实际上并未将事件附加到子节点本身。React将使用单个事件侦听器在顶层侦听所有事件。React 事件机制详解:
// React 合成事件(SyntheticEvent)
function Button() {
const handleClick = (event) => {
// event 是 SyntheticEvent,不是原生 Event
console.log(event.type); // 'click'
console.log(event.target); // 触发事件的 DOM 元素
console.log(event.currentTarget); // 绑定事件的 DOM 元素
// 阻止默认行为
event.preventDefault();
// 阻止事件冒泡
event.stopPropagation();
// 如果需要原生事件
console.log(event.nativeEvent); // 原生 Event 对象
};
return <button onClick={handleClick}>点击我</button>;
}
// React 17+ 事件委托变化
// React 16:所有事件委托到 document
// React 17:所有事件委托到 root 节点
// React 18:继续委托到 root 节点(createRoot)
// 事件冒泡和捕获
function App() {
return (
<div onClick={() => console.log('div clicked')}>
{/* 捕获阶段 */}
<button
onClickCapture={() => console.log('button capture')}
onClick={() => console.log('button bubble')}
>
点击
</button>
</div>
);
// 输出顺序:
// 1. button capture(捕获阶段)
// 2. button bubble(冒泡阶段)
// 3. div clicked(冒泡阶段)
}
// 常见事件类型
// onClick — 点击
// onChange — 表单值变化
// onSubmit — 表单提交
// onKeyDown / onKeyUp — 键盘事件
// onFocus / onBlur — 焦点事件
// onMouseEnter / onMouseLeave — 鼠标进入/离开
// onScroll — 滚动事件
// onWheel — 滚轮事件
// onDragStart / onDrag / onDragEnd — 拖拽事件React 事件与原生事件的关键区别:
1. 事件委托方式不同
- React 16:委托到 document
- React 17+:委托到 root 容器节点
- 这解决了微前端场景下多个 React 版本共存的事件冲突
2. 事件池(Event Pooling)
- React 16 中 SyntheticEvent 会被复用
- 事件回调执行后,事件对象的属性会被清空
- 需要异步访问事件时,调用 event.persist()
- React 17+ 移除了事件池机制
3. 阻止默认行为
- React 中必须显式调用 event.preventDefault()
- 不能通过返回 false 来阻止默认行为(与原生 DOM 不同)8、React如何创建refs
Refs 是使用 React.createRef() 方法创建的,并通过 ref 属性添加到 React 元素上。Refs 的多种创建方式:
// 1. createRef(类组件)
class MyInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentDidMount() {
this.inputRef.current.focus();
}
render() {
return <input ref={this.inputRef} />;
}
}
// 2. useRef(函数组件)
function MyInput() {
const inputRef = React.useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
// 3. 回调 Ref
function CustomInput() {
const inputRef = React.useCallback((node) => {
if (node) {
node.focus();
}
}, []);
return <input ref={inputRef} />;
}
// 4. Ref 转发(forwardRef)
const FancyInput = React.forwardRef((props, ref) => {
return <input ref={ref} className="fancy" />;
});
function Parent() {
const inputRef = React.useRef(null);
return <FancyInput ref={inputRef} />;
}
// 5. useImperativeHandle(自定义暴露的方法)
const CustomInput = React.forwardRef((props, ref) => {
const inputRef = React.useRef();
React.useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => inputRef.current.value = '',
getValue: () => inputRef.current.value,
}));
return <input ref={inputRef} />;
});
function Parent() {
const customRef = React.useRef();
useEffect(() => {
customRef.current.focus(); // 聚焦
customRef.current.clear(); // 清空
}, []);
return <CustomInput ref={customRef} />;
}9、什么是JSX
JSX即JavaScript XML。一种在React组件内部构建标签的类XML语法。JSX为react.js开发的一套语法糖,也是react.js的使用基础。React在不使用JSX的情况下一样可以工作,然而使用JSX可以提高组件的可读性,因此推荐使用JSX。JSX 的本质和规则:
// JSX 本质上是 React.createElement 的语法糖
// JSX 写法
const element = <h1 className="greeting">Hello, {name}!</h1>;
// 编译后等价于
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, ',
name,
'!'
);
// 进一步展开
const element = {
type: 'h1',
props: {
className: 'greeting',
children: ['Hello, ', name, '!']
}
};
// JSX 的规则
// 1. 必须有且只有一个根元素
// 错误:return <div>a</div><div>b</div>
// 正确:return <><div>a</div><div>b</div></>
// 2. 使用 className 代替 class
// 错误:<div class="container">
// 正确:<div className="container">
// 3. 使用 htmlFor 代替 for
// 错误:<label for="input">
// 正确:<label htmlFor="input">
// 4. 事件使用驼峰命名
// 错误:<button onclick={handleClick}>
// 正确:<button onClick={handleClick}>
// 5. 使用花括号 {} 嵌入 JavaScript 表达式
const greeting = <h1>Hello, {name}!</h1>;
const style = <div style={{ color: 'red', fontSize: '16px' }}>Styled</div>;
const condition = <div>{isLoggedIn ? '欢迎' : '请登录'}</div>;
// 6. JSX 中的注释
const element = (
<div>
{/* 这是 JSX 注释 */}
<span>内容</span>
</div>
);10、为什么不直接更新state状态
如果直接更新state状态,那么它将不会重新渲染组件,而是使用 setState() 方法。它计划对组件状态对象的更新。状态改变时,组件通过重新渲染做出响应。详细解释:
// React 的状态管理机制
// 1. 直接修改 state 不会触发重新渲染
// 错误
this.state.count = 1; // 不会触发渲染
// 2. 必须通过 setState 修改
// 正确
this.setState({ count: 1 }); // 触发重新渲染
// 3. React 的批量更新机制
// React 会将多次 setState 合并为一次更新
handleClick() {
this.setState({ count: 1 });
this.setState({ name: '张三' });
// 只触发一次渲染,而不是两次
}
// 4. React 的状态是不可变的(Immutable)
// 错误:直接修改数组或对象
this.state.list.push(1); // 不会触发渲染
this.state.user.name = '李四'; // 不会触发渲染
// 正确:创建新的引用
this.setState({ list: [...this.state.list, 1] });
this.setState({ user: { ...this.state.user, name: '李四' } });
// Hooks 中的不可变更新
const [list, setList] = useState([]);
const [user, setUser] = useState({ name: '张三', age: 25 });
// 正确
setList([...list, 1]);
setList(prev => [...prev, 1]);
setUser(prev => ({ ...prev, name: '李四' }));
// 数组操作
setList(prev => prev.filter(item => item.id !== id)); // 删除
setList(prev => prev.map(item => // 更新
item.id === id ? { ...item, done: true } : item
));11、React中的这三个点(...)是做什么的
扩展传值符号,是把对象或数组里的每一项展开,是属于ES6的语法。展开运算符在 React 中的常见用法:
// 1. 展开 Props
function Button(props) {
return <button {...props} />; // 展开所有 props
}
// 2. 合并对象
const defaultConfig = { color: 'blue', size: 'medium' };
const userConfig = { color: 'red', disabled: true };
const finalConfig = { ...defaultConfig, ...userConfig };
// { color: 'red', size: 'medium', disabled: true }
// 3. 展开数组
const list1 = [1, 2, 3];
const list2 = [4, 5, 6];
const combined = [...list1, ...list2]; // [1, 2, 3, 4, 5, 6]
// 4. 复制对象(浅拷贝)
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
copy.a = 10; // 不影响 original
copy.b.c = 20; // 影响 original(浅拷贝的局限)
// 5. 解构剩余参数
const { type, ...restProps } = props;
// type = props.type
// restProps = 除 type 外的其他 props
// 6. 在 setState 中更新部分状态
this.setState(prev => ({
...prev,
name: '新名字',
age: prev.age + 1
}));
// 7. 数组去重
const unique = [...new Set([1, 2, 2, 3, 3, 4])]; // [1, 2, 3, 4]12、简单介绍下react hooks 产生的背景及 hooks 的优点
hooks是针对在使用react时存在以下问题而产生的:
组件之间复用状态逻辑很难,在hooks之前,实现组件复用,一般采用高阶组件和 Render Props,它们本质是将复用逻辑提升到父组件中,很容易产生很多包装组件,带来嵌套地域。
组件逻辑变得越来越复杂,尤其是生命周期函数中常常包含一些不相关的逻辑,完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
复杂的class组件,使用class组件,需要理解 JavaScript 中 this 的工作方式,不能忘记绑定事件处理器等操作,代码复杂且冗余。除此之外,class组件也会让一些react优化措施失效。Hooks 解决的核心问题:
问题 1:组件间逻辑复用困难
- 传统方案:HOC、Render Props → 嵌套地狱
- Hooks 方案:Custom Hooks → 扁平化复用
问题 2:生命周期函数中的逻辑混乱
- componentDidMount 中同时包含:网络请求、事件订阅、DOM 操作
- componentDidUpdate 中又包含:条件判断、数据同步
- Hooks 方案:每个 useEffect 独立处理一个副作用
问题 3:Class 组件的复杂性
- this 绑定问题
- 代码量多(样板代码)
- 热重载不可靠
- Tree Shaking 不友好
- 难以做编译优化
Hooks 的优势:
1. 逻辑复用更自然(Custom Hooks)
2. 关注点分离(每个 Hook 处理一个关注点)
3. 没有 this 绑定问题
4. 代码更简洁
5. 更容易做编译优化(React Compiler)
6. 更好的类型推导(TypeScript)13、React hooks 怎么模拟生命周期
1、模拟componentDidMount
useEffect(()=>{console.log('第一次渲染时调用')},[])
2、模拟componentDidUpdate
没有第二个参数代表监听所有的属性更新
useEffect(()=>{console.log('任意属性该改变')})
同时监听多个属性的变化需要将属性作为数组传入第二个参数。
useEffect(()=>{console.log('n变了')},[n,m])
3、模拟componentWillUnmount
useEffect(()=>{
const timer = setTimeout(()=>{...},1000)
return()=>{
console.log('组件销毁')
clearTimerout(timer)
}
})完整的生命周期模拟对照表:
// 1. constructor → useState
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
// 2. componentDidMount
useEffect(() => {
console.log('组件挂载完成(等同于 componentDidMount)');
fetchData().then(data => setUser(data));
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => {
// 清理函数(等同于 componentWillUnmount)
clearInterval(timer);
};
}, []); // 空依赖数组 = 只执行一次
// 3. componentDidUpdate
useEffect(() => {
console.log('组件更新完成(等同于 componentDidUpdate)');
// 每次 render 后都会执行
});
// 4. 带条件的 componentDidUpdate
useEffect(() => {
console.log('userId 变化时执行');
fetchUser(userId);
}, [userId]); // 依赖变化时执行
// 5. componentWillUnmount
useEffect(() => {
return () => {
console.log('组件即将卸载(等同于 componentWillUnmount)');
// 清理:定时器、事件监听、取消请求
};
}, []);
// 6. shouldComponentUpdate → React.memo
const MemoizedChild = React.memo(function Child({ data }) {
return <div>{data}</div>;
});
// 7. useMemo(模拟 shouldComponentUpdate 的计算优化)
const expensiveValue = useMemo(() => {
return computeExpensiveValue(input);
}, [input]);
// 8. useCallback(缓存函数引用,避免子组件不必要渲染)
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);14、React 中的 useState 是什么?
useState是react hooks中最常用且用法最简单的一个hook。 useState(0) 返回一个元组,其中第一个参数 count是计数器的当前状态,setCounter 提供更新计数器状态的方法。useState 详细用法:
// 基础用法
const [state, setState] = useState(initialValue);
// state: 当前状态值
// setState: 更新状态的函数
// initialValue: 初始值(只在首次渲染时使用)
// 1. 基本类型
const [count, setCount] = useState(0);
const [name, setName] = useState('张三');
const [isLoading, setIsLoading] = useState(false);
// 2. 对象类型(注意不可变更新)
const [user, setUser] = useState({ name: '张三', age: 25 });
// 更新
setUser(prev => ({ ...prev, age: 26 })); // 更新 age,保留其他字段
// 3. 数组类型
const [items, setItems] = useState([]);
// 添加
setItems(prev => [...prev, newItem]);
// 删除
setItems(prev => prev.filter(item => item.id !== id));
// 更新
setItems(prev => prev.map(item =>
item.id === id ? { ...item, done: true } : item
));
// 4. 惰性初始化(传入函数,只在首次渲染时执行)
const [data, setData] = useState(() => {
// 适用于初始化计算成本高的场景
return expensiveCompute();
});
// 5. 函数式更新(基于旧值计算新值)
setCount(prev => prev + 1); // 推荐:不依赖闭包中的旧值
// 而不是
setCount(count + 1); // 可能在闭包中获取到旧值
// 6. 多个状态 vs 单个状态对象
// 方案 A:多个 useState(推荐,更新粒度更细)
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
// 方案 B:单个 useState + 对象(适合需要原子更新的场景)
const [form, setForm] = useState({ name: '', email: '', age: 0 });15、当调用setState时,React render 是如何工作的
虚拟 DOM 渲染:当render方法被调用时,它返回一个新的组件的虚拟 DOM 结构。当调用setState()时,render会被再次调用,因为默认情况下shouldComponentUpdate总是返回true,所以默认情况下React 是没有优化的。
原生 DOM 渲染:React 只会在虚拟DOM中修改真实DOM节点,而且修改的次数非常少——这是很棒的React特性,它优化了真实DOM的变化,使React变得更快。React 渲染机制的详细流程:
setState 调用后的完整流程:
1. 调用 setState
↓
2. React 将更新排入队列(调度)
- React 16+ 使用 Fiber 调度器
- 支持优先级调度(高优先级任务先执行)
- 批量更新(多个 setState 合并)
↓
3. 协调(Reconciliation)
- 生成新的 Virtual DOM 树
- 与旧的 Virtual DOM 树进行 Diff 比较
- Diff 算法:O(n) 复杂度
- 同层比较,不跨层级
↓
4. 提交(Commit)
- 将最小变更应用到真实 DOM
- 只更新变化的部分
↓
5. 浏览器重绘(Paint)
- 真实 DOM 更新后,浏览器执行重绘
Diff 算法的三个策略:
1. 跨层级移动节点 → 直接重建(不比较)
2. 不同类型的组件 → 直接重建(不比较)
3. 通过 key 标识节点 → 同层比较16、React 中 key 的重要性是什么?
key 用于识别唯一的 Virtual DOM 元素及其驱动 UI 的相应数据。它们通过回收 DOM 中当前所有的元素来帮助 React 优化渲染。这些 key 必须是唯一的数字或字符串,React 只是重新排序元素而不是重新渲染它们。这可以提高应用程序的性能。key 的深度解析:
// 没有 key 的问题
const list = [{id: 1, name: 'A'}, {id: 2, name: 'B'}, {id: 3, name: 'C'}];
const reversedList = [...list].reverse();
// 没有 key:React 无法识别哪个元素是同一个
// 会导致所有元素重新渲染
{list.map(item => <li>{item.name}</li>)}
// 正确:使用唯一且稳定的 key
{list.map(item => <li key={item.id}>{item.name}</li>)}
// key 的选择原则:
// 1. 唯一性:在兄弟元素中唯一
// 2. 稳定性:不随渲染次数变化
// 3. 不要使用 index 作为 key(列表会变化时)为什么不要用 index 作为 key:
场景:在列表头部插入元素
原始列表:[{id:1, name:'A'}, {id:2, name:'B'}, {id:3, name:'C'}]
key=index: [key=0:A, key=1:B, key=2:C]
插入后:[{id:0, name:'X'}, {id:1, name:'A'}, {id:2, name:'B'}, {id:3, name:'C'}]
key=index: [key=0:X, key=1:A, key=2:B, key=3:C]
React 对比:
key=0: X ≠ A → 重新渲染(实际应该新增)
key=1: A ≠ B → 重新渲染(实际应该不变)
key=2: B ≠ C → 重新渲染(实际应该不变)
key=3: C (新增)
结果:4次重新渲染,但实际上只有1个新元素
使用 id 作为 key:
key=0: X (新增)
key=1: A = A (不变)
key=2: B = B (不变)
key=3: C = C (不变)
结果:只有1次新增渲染,效率大幅提升
使用 index 作为 key 的副作用:
1. 不必要的重新渲染
2. 受控组件的状态错乱(如 input 中的值)
3. 动画和过渡效果异常17、什么是Redux?
Redux 是当今最热门的前端开发库之一。它是 JavaScript 程序的可预测状态容器,用于整个应用的状态管理。使用 Redux 开发的应用易于测试,可以在不同环境中运行,并显示一致的行为。Redux 核心概念和代码示例:
// Redux 三大原则:
// 1. 单一数据源(Single Source of Truth)
// 2. State 只读(State is Read-Only)
// 3. 使用纯函数修改 State(Changes are made with pure functions)
// 1. Action:描述发生了什么
const increment = { type: 'counter/increment' };
const addTodo = (text) => ({ type: 'todo/add', payload: { text } });
// 2. Reducer:根据 Action 计算新的 State
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/increment':
return { value: state.value + 1 };
case 'counter/decrement':
return { value: state.value - 1 };
default:
return state;
}
}
function todoReducer(state = [], action) {
switch (action.type) {
case 'todo/add':
return [...state, {
id: Date.now(),
text: action.payload.text,
done: false
}];
case 'todo/toggle':
return state.map(todo =>
todo.id === action.payload.id
? { ...todo, done: !todo.done }
: todo
);
default:
return state;
}
}
// 3. Store:全局状态容器
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: {
counter: counterReducer,
todos: todoReducer
}
});
// 4. 在 React 中使用
import { Provider, useSelector, useDispatch } from 'react-redux';
function App() {
return (
<Provider store={store}>
<Counter />
<TodoList />
</Provider>
);
}
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>计数: {count}</p>
<button onClick={() => dispatch({ type: 'counter/increment' })}>
+1
</button>
</div>
);
}18、列出 Redux 的组件
1. Action – 这是一个用来描述发生了什么事情的对象。
2. Reducer – 这是一个确定状态将如何变化的地方。
3. Store – 整个程序的状态/对象树保存在Store中。
4. View – 只显示 Store 提供的数据。Redux 数据流详解:
Redux 数据流(单向数据流):
┌─────────────────────────────────────────────┐
│ View (React) │
│ 用户交互 → dispatch(Action) │
└──────────────────┬──────────────────────────┘
│ Action
▼
┌─────────────────────────────────────────────┐
│ Store │
│ 接收 Action → 调用 Reducer → 更新 State │
│ 通知所有订阅者 → View 重新渲染 │
└──────────────────┬──────────────────────────┘
│ Previous State + Action
▼
┌─────────────────────────────────────────────┐
│ Reducer │
│ 纯函数:(state, action) → newState │
│ 不可变性:返回新的 state,不修改旧的 state │
└─────────────────────────────────────────────┘
Redux Toolkit(RTK)现代化方案:
- configureStore — 创建 Store
- createSlice — 简化 Reducer 和 Action 创建
- createAsyncThunk — 处理异步操作
- createEntityAdapter — 规范化实体数据19、Redux 有哪些优点?
Redux 的优点如下:
结果的可预测性 - 由于总是存在一个真实来源,即 store,因此不存在如何将当前状态与动作和应用的其他部分同步的问题。
可维护性 - 代码变得更容易维护,具有可预测的结果和严格的结构。
服务器端渲染 - 你只需将服务器上创建的 store 传到客户端即可。这对初始渲染非常有用,并且可以优化应用性能,从而提供更好的用户体验。
开发人员工具 - 从操作到状态更改,开发人员可以实时跟踪应用中发生的所有事情。
社区和生态系统 - Redux 背后有一个巨大的社区,这使得它更加迷人。
易于测试 - Redux 的代码主要是小巧、纯粹和独立的功能。这使代码可测试且独立。
组织 - Redux 准确地说明了代码的组织方式,这使得代码在团队使用时更加一致和简单。20、常用的hooks
useState:定义state的数据,参数是初始化的数据,返回值两个值1. 初始化值,2. 修改的方法
useEffect:副作用函数
useMemo:用来计算数据,返回一个结果,监听数据的变化,具有缓存性
useCallback:当父组件向子组件传递函数的时候,缓存组件
useRef:相当于createRef的使用,创建组件的属性信息
useContext:相当在函数组件中获取context状态数的内容信息
useReducer:useReducer是用来弥补useState的补不足,可以把数据进行集中式的管理常用 Hooks 完整示例:
// 1. useState — 状态管理
const [count, setCount] = useState(0);
// 2. useEffect — 副作用处理
useEffect(() => {
// 组件挂载/更新后执行
document.title = `计数: ${count}`;
return () => {
// 清理函数(卸载前执行)
};
}, [count]); // 依赖数组
// 3. useMemo — 计算缓存
const expensiveResult = useMemo(() => {
return computeExpensiveValue(count);
}, [count]); // 只在 count 变化时重新计算
// 4. useCallback — 函数缓存
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 函数引用稳定,不会在每次渲染时重新创建
// 5. useRef — 引用管理
const inputRef = useRef(null);
const timerRef = useRef(null);
const previousValue = useRef(initialValue);
// 6. useContext — 上下文消费
const theme = useContext(ThemeContext);
// 7. useReducer — 复杂状态管理
const [state, dispatch] = useReducer(reducer, initialState);
// 8. useLayoutEffect — 同步副作用(DOM 更新后、浏览器绘制前)
useLayoutEffect(() => {
// 测量 DOM 元素尺寸
const rect = ref.current.getBoundingClientRect();
setWidth(rect.width);
}, []);
// 9. useId — 生成唯一 ID
const id = useId();
// 10. useTransition — 低优先级状态更新
const [isPending, startTransition] = useTransition();
startTransition(() => {
setSearchQuery(input); // 低优先级更新
});
// 11. useDeferredValue — 延迟更新
const deferredQuery = useDeferredValue(searchQuery);
// 12. useSyncExternalStore — 订阅外部存储
const state = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot
);21、为什么浏览器无法阅读JSX?
浏览器只能读取JavaScript对象,而不能读取普通JavaScript对象中的JSX。因此,要使浏览器能够读取 JSX,首先,我们需要使用Babel之类的JSX转换器将JSX文件转换为JavaScript对象,然后将其传递给浏览器。JSX 编译过程:
// JSX 代码
function Greeting({ name }) {
return <h1 className="title">Hello, {name}!</h1>;
}
// Babel 编译后
function Greeting({ name }) {
return React.createElement(
'h1',
{ className: 'title' },
'Hello, ',
name,
'!'
);
}
// 进一步编译为 JS 对象(虚拟 DOM)
{
type: 'h1',
key: null,
ref: null,
props: {
className: 'title',
children: ['Hello, ', name, '!']
},
_owner: null
}
// 构建工具配置
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react']
}
}
}
]
}
};22、什么是高阶成分(HOC)?
高阶组件是重用组件逻辑的高级方法。基本上,这是从React的组成性质衍生的模式。HOC是自定义组件,在其中包裹了另一个组件。他们可以接受任何动态提供的子组件,但不会修改或复制其输入组件中的任何行为。您可以说HOC是"纯"组件。HOC 的常见应用模式:
// 1. 属性注入
function withTheme(WrappedComponent) {
return function ThemeComponent(props) {
const theme = useContext(ThemeContext);
return <WrappedComponent {...props} theme={theme} />;
};
}
// 2. 条件渲染
function withLoading(WrappedComponent) {
return function LoadingComponent({ isLoading, ...props }) {
if (isLoading) return <Spinner />;
return <WrappedComponent {...props} />;
};
}
// 3. 日志记录
function withLogger(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
console.log(`${WrappedComponent.name} mounted`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// 组合使用
const EnhancedPage = withLoading(withTheme(withLogger(Dashboard)));23、React的严格模式如何使用,有什么用处?
StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。StrictMode 的详细作用:
import { StrictMode } from 'react';
function App() {
return (
<StrictMode>
<MyComponent />
</StrictMode>
);
}
// StrictMode 检测的问题:
// 1. 识别不安全的生命周期方法
// 2. 关于过时字符串 ref API 的警告
// 3. 关于过时 findDOMNode 方法的警告
// 4. 检测意外的副作用
// 5. 检测过时的 context API
// 6. 确保可复用的状态只被初始化一次
// React 18 StrictMode 的双重渲染
// 在开发模式下,StrictMode 会让组件渲染两次
// 这有助于发现副作用问题
// 注意:只在开发模式下生效,不影响生产构建24、React中什么是受控组件和非控组件?
详细对比:
// 受控组件:表单值由 React state 控制
function ControlledForm() {
const [value, setValue] = useState('');
return (
<form>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p>输入值: {value}</p>
</form>
);
}
// 特点:
// - 表单值由 state 控制
// - 每次输入都会触发 onChange
// - 可以实时验证输入
// - 可以控制输入格式
// 非受控组件:表单值由 DOM 自身管理
function UncontrolledForm() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('输入值:', inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input
ref={inputRef}
defaultValue="默认值"
/>
<button type="submit">提交</button>
</form>
);
}
// 特点:
// - 表单值由 DOM 管理
// - 需要使用 ref 访问值
// - 代码更简洁
// - 适合一次性提交的表单选择建议:
受控组件适合:
- 需要实时验证
- 需要动态禁用/启用
- 需要格式化输入
- 多个表单项互相依赖
非受控组件适合:
- 一次性提交的简单表单
- 与非 React 库集成
- 不需要实时验证这组题真正考什么
- 面试官通常想知道你是否真正理解浏览器、框架和工程化之间的联系。
- 高频追问往往从概念定义延伸到性能、兼容性和线上诊断。
- 如果能结合真实页面问题回答,可信度会明显提高。
- React 面试经常从生命周期延伸到 Hooks、状态管理、性能优化和 Fiber 架构。
60 秒答题模板
- 先说这个概念解决什么问题。
- 再说它在浏览器或框架里的工作机制。
- 最后补一个线上场景或优化案例。
容易失分的点
- 只背 API 名称,不理解执行链路。
- 只说框架,不说浏览器原理。
- 回答性能题时没有指标和验证手段。
- 不了解 React 16-18 版本的变化和新增特性。
- 不了解 Fiber 架构和并发模式。
刷题建议
- 把浏览器、框架、工程化和性能题分开复习,避免知识点混在一起。
- 每道题尽量补一个页面真实案例,比如登录流程、首屏优化或状态同步。
- 前端题常考对比题,复习时要准备两到三个维度的横向比较。
- 重点掌握 Hooks 与 Class 组件的对比和迁移思路。
高频追问
- 这个概念在 React、Vue、原生浏览器里分别怎么体现?
- 如果线上出现白屏、性能抖动或状态错乱,你会怎么定位?
- 这个方案的可维护性和性能代价是什么?
- React 18 的并发模式对现有代码有什么影响?
- useState 和 useReducer 如何选择?
复习重点
- 把每道题的关键词整理成自己的知识树,而不是只背原句。
- 对容易混淆的概念要做横向比较,例如机制差异、适用边界和性能代价。
- 复习时优先补"为什么",其次才是"怎么用"和"记住什么术语"。
- 重点掌握:生命周期、Hooks、状态管理、性能优化。
面试作答提醒
- 先说结论与应用场景,再解释机制。
- 讲性能题时尽量带上监控指标。
- 框架题要注意区分版本特性和通用原理。
- React 面试回答要体现出实际项目经验和问题解决能力。
