react 面试总结
1. 为什么在 React 的 class 组件中能使用函数组件,在函数组件中也能使用 class 组件?
因为 React 组件的本质是“可复用的 UI 单元”,无论是 class 组件还是函数组件,最终都会被 React 内部转换为可渲染的元素(React Element)。
React 对组件的类型没有限制,只要组件返回合法的 React 元素(虚拟 DOM),就能在任何组件中嵌套使用。这种设计保证了组件的灵活性和组合性,符合 React “组件化”的核心思想。
2. 函数组件和类组件的区别
维度 | 函数组件(Functional Component) | 类组件(Class Component) |
---|---|---|
定义方式 | 以函数形式定义,接收 props 并返回 React 元素 | 继承 React.Component ,通过 render() 方法返回元素 |
状态管理 | 依赖 useState 、useReducer 等 Hooks | 依赖 this.state 和 this.setState() |
生命周期 | 依赖 useEffect 等 Hooks 模拟 | 通过 componentDidMount 等生命周期方法直接定义 |
this 绑定 | 无 this ,直接使用参数和局部变量 | 需要处理 this 绑定问题(如箭头函数、bind ) |
性能 | 轻量,无类实例开销,React 优化更友好 | 有类实例开销,性能略逊于函数组件(差异微小) |
适用场景 | 简单 UI 逻辑、依赖 Hooks 管理状态和副作用 | 复杂逻辑(早期),现逐渐被函数组件 + Hooks 替代 |
3. setState(类组件)
- 类组件中 setState
执行了什么动作?
setState
是类组件更新状态的唯一方式,执行以下动作:
- 将新状态与旧状态合并(浅合并,只合并第一层属性)。
- 触发 React 的更新机制,标记组件为“需要重新渲染”。
- 调度组件的重新渲染(
render
方法),并更新 DOM。
- 状态存放在哪里?
状态存储在类组件的实例(this
)上,即 this.state
中。
- 为什么更改状态后组件会重新渲染?
setState
会触发 React 的“协调(Reconciliation)”过程:
- 调用
setState
后,React 会标记组件为“脏组件”(需要更新)。 - 进入更新阶段,重新执行
render
方法生成新的虚拟 DOM。 - 通过“Diff 算法”对比新旧虚拟 DOM,计算出最小更新量,最终更新真实 DOM。
- 核心原理是什么?
- 状态更新:合并新状态到
this.state
。 - 调度更新:React 内部维护一个更新队列,批量处理多个
setState
调用,避免频繁渲染。 - 重新渲染:通过虚拟 DOM 对比,高效更新视图。
- setState
是同步还是异步执行?
既可能是异步,也可能是同步,取决于调用场景:
- 异步:在 React 合成事件(如
onClick
)、生命周期方法中,setState
会批量处理,更新和渲染是异步的。 - 同步:在原生事件(如
addEventListener
)、setTimeout
等异步操作中,setState
会同步更新状态并触发渲染。
(本质是 React 能否“捕获”更新,若能则批量异步,否则同步)
- 可以使用 await setState
吗?
不可以。setState
没有返回值(返回 undefined
),而 await
需要等待一个 Promise,因此使用 await setState(...)
无效。
- setState
中可以传递函数作为参数吗?
可以。函数格式为 setState((prevState, props) => newState)
,作用是:
- 确保基于最新的状态(
prevState
)计算新状态,避免因批量更新导致的状态覆盖问题。 - 示例:javascript
this.setState(prevState => ({ count: prevState.count + 1 }));
4. useState(函数组件)
- useState
执行了什么动作?
useState
是函数组件中管理状态的 Hook,执行以下动作:
- 初始化时,创建状态变量并设置初始值。
- 返回一个数组
[state, setState]
,其中state
是当前状态值,setState
是更新状态的函数。 - 调用
setState
时,更新状态并触发组件重新渲染。
- 状态存放在哪里?
状态存储在 React 内部的“ Hooks 链表”中,与函数组件的 Fiber 节点(虚拟 DOM 单元)关联,而非函数作用域内(避免函数调用时被重置)。
- 为什么更改状态后组件会重新渲染?
setState
(useState
返回的更新函数)会触发函数组件的重新执行:
- 调用
setState
后,React 标记组件为“需要更新”。 - 重新执行函数组件,
useState
会读取最新的状态值。 - 生成新的虚拟 DOM,通过 Diff 算法更新真实 DOM。
- 核心原理是什么?
- 状态存储:React 通过“ Hooks 链表”和 Fiber 节点保存状态,确保函数组件重新执行时能读取到最新状态。
- 更新触发:
setState
会调度组件重新渲染,函数组件再次执行时,useState
从链表中读取最新状态。
- useState
是同步还是异步执行?
与 setState
类似:
- 异步:在 React 合成事件、
useEffect
等 React 可控场景中,批量处理更新,异步触发渲染。 - 同步:在原生事件、
setTimeout
等场景中,同步更新状态并触发渲染。
- 可以使用 await useState
吗?
不可以。useState
返回的是 [state, setState]
数组,而非 Promise,await
无法等待。
- useState
中可以传递函数作为参数吗?
可以,有两种场景:
- 初始化函数:
useState(initFn)
,initFn
仅在组件首次渲染时执行,返回初始状态。适用于复杂计算的初始值,避免每次渲染重复计算。javascriptconst [count, setCount] = useState(() => 100 + 200); // 仅首次执行
- 更新函数:
setState(prevState => newState)
,与setState
函数参数作用一致,确保基于最新状态更新。javascriptsetCount(prev => prev + 1);
- 手写简化版 useState
原理:用数组模拟 Hooks 链表,记录状态和更新函数,通过索引匹配每次调用。
let hookIndex = 0;
const hooks = []; // 存储所有状态的数组
function useState(initialValue) {
// 初始化:如果是首次调用,计算初始值
if (hooks[hookIndex] === undefined) {
// 处理初始化函数的情况
hooks[hookIndex] = typeof initialValue === 'function'
? initialValue()
: initialValue;
}
const currentIndex = hookIndex; // 保存当前索引(闭包)
// 定义更新函数
const setState = (newValue) => {
// 处理更新函数的情况
if (typeof newValue === 'function') {
hooks[currentIndex] = newValue(hooks[currentIndex]);
} else {
hooks[currentIndex] = newValue;
}
// 模拟 React 重新渲染组件
render();
};
const state = hooks[hookIndex];
hookIndex++; // 索引自增,确保下次调用匹配下一个状态
return [state, setState];
}
// 模拟组件渲染
function render() {
hookIndex = 0; // 每次渲染重置索引
console.log('组件重新渲染');
// 执行组件函数...
}
5. useEffect
- useEffect
接受的参数是同步还是异步执行?
- 第一个参数(副作用函数):异步执行,在组件渲染完成后(DOM 更新后)执行,不会阻塞浏览器渲染。
- 第二个参数(依赖数组):同步解析,用于判断副作用是否需要重新执行。
- 原理是什么?手写简化版 useEffect
原理:
- React 在组件渲染后,对比依赖数组与上一次的依赖,若有变化则执行副作用函数。
- 支持返回清理函数,在组件卸载或依赖变化前执行,用于取消订阅、清除定时器等。
手写简化版:
let hookIndex = 0;
const hooks = [];
function useEffect(effect, deps) {
const currentIndex = hookIndex;
// 首次调用或依赖变化时执行
const hasChanged = () => {
if (!hooks[currentIndex]) return true; // 首次调用
// 对比新旧依赖
const oldDeps = hooks[currentIndex].deps;
return deps.some((dep, i) => dep !== oldDeps[i]);
};
if (hasChanged()) {
// 执行清理函数(如果有)
if (hooks[currentIndex]?.cleanup) {
hooks[currentIndex].cleanup();
}
// 执行副作用函数,保存清理函数
const cleanup = effect();
hooks[currentIndex] = {deps, cleanup};
}
hookIndex++;
}
// 模拟组件卸载时执行所有清理函数
function unmount() {
hooks.forEach(hook => {
if (hook?.cleanup) hook.cleanup();
});
}
使用示例:
function Component() {
useEffect(() => {
console.log('副作用执行');
return () => console.log('清理函数执行');
}, [/* 依赖 */]);
}
6. antd 的 message 方法无需挂载即可直接使用的原理
antd 的 message
这类“函数式组件”之所以能直接调用(无需手动挂载),核心是通过全局动态创建 DOM 实现的,具体原理:
预先定义全局容器:
当引入message
时,antd 会在页面中自动创建一个全局唯一的容器(如<div class="ant-message-container">
),固定定位在页面顶层(通常是 body 下),用于承载所有弹窗。函数调用即动态生成 DOM:
调用message.success()
时,内部会:- 创建弹窗 DOM 元素(包含图标、文本等);
- 将元素插入全局容器;
- 通过 CSS 动画控制显示/隐藏;
- 自动管理生命周期(定时关闭、点击关闭等)。
脱离组件树的独立渲染:
这类组件不依赖 React 组件树的挂载流程,而是直接操作 DOM,因此无需在 JSX 中声明,直接通过函数调用即可触发。
简言之:message
本质是对“动态 DOM 操作”的封装,通过预创建全局容器和自动化的 DOM 管理,实现了“函数调用即展示”的便捷性。
7. 封装 antd 模态弹窗需考虑的问题及示例
需考虑的核心问题:
- 状态控制:通过
visible
或类似状态控制弹窗显示/隐藏。 - 内容定制:支持传入标题、内容、底部按钮等自定义元素。
- 交互反馈:确定/取消按钮需返回 Promise,便于处理异步逻辑(如表单提交)。
- 生命周期:提供打开/关闭回调,支持动画过渡。
- 灵活性:允许自定义样式、尺寸、遮罩层等属性。
示例:封装带 Promise 回调的模态弹窗
import {Modal, Button} from 'antd';
import {useState} from 'react';
// 封装弹窗组件
const MyModal = ({
title,
content,
visible,
onCancel,
confirmText = '确定',
cancelText = '取消'
}) => {
// 用于控制按钮加载状态
const [loading, setLoading] = useState(false);
// 确定按钮点击事件(返回Promise)
const handleConfirm = () => {
return new Promise((resolve) => {
setLoading(true);
// 调用外部传入的确认逻辑(用户自定义)
resolve(); // 通知外部处理完成
}).finally(() => {
setLoading(false);
});
};
return (
<Modal
title={title}
open={visible}
onCancel={onCancel}
footer={[
<Button key="cancel" onClick={onCancel}>
{cancelText}
</Button>,
<Button
key="confirm"
type="primary"
loading={loading}
onClick={async () => {
// 等待用户处理完成后关闭弹窗
await handleConfirm();
onCancel(); // 关闭弹窗
}}
>
{confirmText}
</Button>
]}
>
{content}
</Modal>
);
};
// 使用示例
const ParentComponent = () => {
const [visible, setVisible] = useState(false);
const handleOpen = () => setVisible(true);
const handleClose = () => setVisible(false);
return (
<>
<Button onClick={handleOpen}>打开弹窗</Button>
<MyModal
visible={visible}
onCancel={handleClose}
title="自定义弹窗"
content={<p>请确认是否执行此操作?</p>}
confirmText="执行"
/>
</>
);
};
关键设计:
- Promise 回调:
handleConfirm
返回 Promise,支持外部传入异步逻辑(如接口请求),完成后自动关闭弹窗。 - 加载状态:通过
loading
状态防止重复点击,提升用户体验。 - 高度定制:标题、内容、按钮文本等均可通过 props 自定义。
8. 封装 npm 脚手架(my-cli)的关键配置与注意事项
1. 配置 npx my-cli create-app my-app
命令
package.json 配置:
通过bin
字段指定命令入口文件,使my-cli
成为可执行命令:json{ "name": "my-cli", "version": "1.0.0", "bin": { "my-cli": "./bin/index.js" // 命令入口文件 } }
入口文件(bin/index.js):
使用commander
解析命令参数,处理create-app
指令:javascript#!/usr/bin/env node const { program } = require('commander'); // 定义 create-app 命令 program .command('create-app <app-name>') // 接收 app 名称参数 .description('创建新应用') .action((appName) => { // 执行创建逻辑(如拉取模板、初始化项目) console.log(`正在创建应用:${appName}`); }); program.parse(process.argv); // 解析命令行参数
发布与测试:
本地测试可通过npm link
将my-cli
链接为全局命令,发布后用户即可通过npx my-cli
调用。
2. 保证执行指定代码,避免执行无关逻辑
- 入口文件隔离:确保
bin/index.js
只处理命令解析,核心逻辑拆分到其他模块,通过导入方式调用。 - 条件判断:在非命令执行场景(如被其他包导入时),通过
require.main === module
判断是否为入口文件,避免自动执行:javascript// 仅在直接执行该文件时才运行命令逻辑 if (require.main === module) { program.parse(process.argv); }
- 清理依赖:通过
.npmignore
排除测试文件、文档等无关内容,减少包体积并避免意外执行。
3. npm 包封装的其他注意事项
- 版本管理:遵循语义化版本(Major.Minor.Patch),如
1.0.0
。 - 权限控制:入口文件需添加
#!/usr/bin/env node
声明,确保可执行权限。 - 错误处理:捕获命令执行中的异常,提供友好错误提示。
- 文档说明:在 README 中说明命令用法、参数含义及示例。
- 依赖管理:区分
dependencies
(运行时依赖)和devDependencies
(开发时依赖)。 - 兼容性:指定支持的 Node.js 版本(如
engines: { "node": ">=14" }
)。
9. webpack 和 vite 的区别与共性
共性:
- 均为前端构建工具,用于模块打包、代码转换(如 TS→JS、SCSS→CSS)、优化输出等。
- 支持插件生态,可扩展功能(如压缩、热更新)。
- 支持开发环境与生产环境配置分离。
区别:
维度 | webpack | vite |
---|---|---|
构建原理 | 基于“打包”:将所有模块递归解析为依赖图,打包成单文件。 | 基于“原生 ESM”:开发时不打包,直接通过浏览器 ESM 加载模块。 |
开发环境性能 | 冷启动慢(需全量打包),热更新随项目增大变慢。 | 冷启动快(无需打包),热更新快(只更新修改的模块)。 |
生产环境处理 | 成熟的优化策略(代码分割、tree-shaking 等)。 | 生产环境仍需打包(使用 rollup),优化策略更简洁。 |
适用场景 | 大型复杂项目(支持多种模块规范、复杂依赖)。 | 中小型项目、Vue/React 等现代框架项目(依赖 ESM 支持)。 |
配置复杂度 | 配置项多,较复杂(需手动配置 loader、plugin)。 | 配置简洁,内置常用功能(如 TS、CSS 支持)。 |
热更新机制 | 基于 HMR 插件,需手动配置模块更新逻辑。 | 原生支持 HMR,自动处理模块依赖更新。 |
简言之:webpack 是“全能型打包工具”,适合复杂项目;vite 是“现代轻量工具”,利用 ESM 提升开发效率,更适合现代前端项目。
- 现在一个vue3的项目中有一个vue的单文件组件,内部使用了onMount函数,然后呢在内部有一个usePageData的hook,hook中也使用了onMount这个函数,请问这样做OK吗? 在 Vue 3 中,这种做法是完全可行且合理的。
原因如下:
onMounted 的设计特性
onMounted
是 Vue 3 提供的生命周期 Hook,其作用是注册一个回调函数,在组件挂载完成后执行。
无论是在组件内部直接使用,还是在自定义 Hook(如usePageData
)中使用,onMounted
都会被正确关联到当前组件的生命周期。自定义 Hook 的本质
自定义 Hook(如usePageData
)本质上是逻辑复用的函数,它可以调用 Vue 提供的各种 Hook(包括onMounted
、ref
、watch
等)。
当组件调用usePageData
时,Hook 内部的onMounted
会像在组件内直接使用一样,被 Vue 收集并在组件挂载后执行。执行顺序
若组件和其内部调用的 Hook 中都有onMounted
,则它们的回调会按注册顺序依次执行(先执行组件内的,还是 Hook 内的,取决于调用usePageData
的时机)。
示例代码(正确示范)
<!-- 组件 -->
<template>
<div>{{ data }}</div>
</template>
<script setup>
import {onMounted, ref} from 'vue';
import {usePageData} from './usePageData';
// 组件内的 onMounted
onMounted(() => {
console.log('组件挂载完成');
});
// 调用自定义 Hook(内部也有 onMounted)
const {data} = usePageData();
</script>
// usePageData.js(自定义 Hook)
import {onMounted, ref} from 'vue';
export function usePageData() {
const data = ref(null);
// Hook 内的 onMounted
onMounted(() => {
console.log('Hook 中:组件挂载完成,开始请求数据');
// 模拟请求数据
data.value = '从接口获取的数据';
});
return {data};
}
输出结果
组件挂载完成
Hook 中:组件挂载完成,开始请求数据
总结
- 在组件和自定义 Hook 中同时使用
onMounted
是完全合法的。 - Vue 会自动将所有
onMounted
回调关联到当前组件的生命周期,确保它们在正确时机执行。 - 这种方式是 Vue 3 中逻辑复用的常见做法(通过自定义 Hook 拆分组件逻辑)。