前言
React 作为现代前端开发中最重要的框架之一,其基础知识在面试中被频繁考察。本文将围绕 JSX 语法、Babel 编译原理、Key 的作用与最佳实践 以及 受控组件与非受控组件的区别 四个核心考点,系统性地梳理 React 的基础知识,帮助你构建扎实的技术基础,提升面试应答能力。
一、JSX:React 中的语法糖
1. 什么是 JSX?
JSX(JavaScript XML)是一种类似 HTML 的语法扩展,允许我们在 JavaScript 中直接书写类似 HTML 的结构。它并非标准的 JavaScript 语法,不能独立运行,需要通过编译工具转换为标准的 JavaScript 代码。
const element = <h1>Hello, JSX!</h1>;
2. JSX 的运行原理
JSX 最终会被 Babel 等工具编译为 React.createElement() 调用:
const element = React.createElement('h1', null, 'Hello, JSX!');
这一步是构建 React 应用的核心环节,JSX 依赖于 React 环境和构建工具(如 Vite、Webpack) ,不能直接在浏览器中运行。
二、Babel:让 JavaScript 更强大
1. Babel 的使命
Babel 是一个 JavaScript 编译器,其核心功能是将高版本的 JavaScript 语法(如 ES6、ES7、ES8) 转换为向后兼容的 ES5 代码,以便在旧浏览器或环境中运行。
💖Slogan:Make JavaScript Great Again!
2. Babel 的作用
- 将
let/const转换为var - 将箭头函数
() => {}转换为function(){} - 将
class类语法转换为构造函数 - 支持 JSX 转换为
React.createElement() - 支持异步语法
async/await转换为Promise
3. 安装与使用
进入后端开发环境:
npm init -y
编译过程:
pnpm i @babel/cli @babel/core -D
@babel/core:Babel 核心库@babel/cli:命令行工具,用于执行编译命令-D:开发阶段的依赖 devDependencies
安装react核心运行库:
加
-D和不加的区别如下:
4. Babel 插件机制
Babel 的核心非常轻量,它本身并不做任何语法转换,所有的转换能力都来自插件(plugins) 。
可以按需引入插件,比如:
@babel/plugin-transform-arrow-functions:把箭头函数转成普通函数@babel/plugin-transform-classes:把 class 转成 ES5 构造函数@babel/plugin-transform-template-literals:处理模板字符串
但手动一个一个加插件太麻烦,于是就有了 预设(preset) ,它是一个插件集合。
4.1 常见的两个预设说明
1. @babel/preset-env
- 作用:根据希望支持的浏览器环境,自动决定需要哪些 Babel 插件,将 ES6+ 的代码转换为 ES5。
2. @babel/preset-react
- 作用:处理 React 相关语法,主要是JSX。
- 它会自动引入处理 JSX、React 组件、React 开发工具提示等所需的插件。
- 使用场景:React 项目中使用 JSX 语法时必须配置
4.2 常见的 Babel 配置文件位置
.babelrcbabel.config.json- 或者在
package.json中:
4.3 安装方式
你需要安装它作为开发依赖:
npm install --save-dev @babel/preset-react
或者使用 pnpm:
pnpm add -D @babel/preset-react
完整的 React+Babel 配置示例
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
这样配置后:
@babel/preset-env:负责把 ES6+ 转成 ES5@babel/preset-react:负责把 JSX 转成React.createElement()调用
5. 执行编译
./node_modules/.bin/babel 1.jsx -o 2.jsx
三、React 列表渲染中的 Key:为何必须唯一?
1. 为什么需要 Key?
在 React 中,当我们使用 map 渲染列表时,必须为每个列表项指定一个 key 属性。这是 React 实现 高效更新和渲染组件 的关键机制之一。
{todos.map(todo => (
<Todo key={todo.id} text={todo.text} />
))}
2、React 如何更新列表?
React 为了提升性能,采用了 虚拟 DOM Diff 算法,在更新列表时,会比较 旧的虚拟 DOM 和 新的虚拟 DOM,找出最小的差异进行更新。
而 key 就是 React 判断 哪些元素是“新”的、哪些是“旧的”、哪些可以“复用” 的依据。
3. 不设置 key 或使用 index 作为 key 的问题
场景一:在数组末尾插入新项(性能尚可)
setTodos(prev => [
...prev,
{
id: 4,
title: '标题四'
}
])
- React 会依次比较每一项
- 旧的前 3 项和新的前 3 项一致,直接复用
- 新增的第 4 项会被创建并插入
✅ 性能尚可,没有问题。
场景二:在数组开头插入新项(性能大打折扣)
setTodos(prev => [
{
id: 4,
title: '标题四'
},
...prev
])
此时:
- React 默认按顺序比较新旧列表
- 原来第 1 项变成了新列表的第 2 项
- 原来第 2 项变成了新列表的第 3 项……
React 无法识别这些项是否是“复用”的,只能全部重新创建并渲染,造成 不必要的性能浪费。
场景三:使用 index 作为 key
todos.map((todo, index) => (
<li key={index}>{todo.title}</li> /*使用index作为key*/
))
此时,React 将按顺序重新渲染列表。由于我们使用了 index 作为 key,会导致以下情况:
- 原来的第 1 项(
index: 0)现在变成了第 2 项,它的key变成了1 - 原来的第 2 项(
index: 1)现在变成了第 3 项,它的key变成了2 - 原来的第 3 项(
index: 2)现在变成了第 4 项,它的key变成了3 - 新插入的项(
index: 3)占据了第 1 个位置,它的key是0
问题:
- React 会认为原来的每一项都发生了变化,因为它们的
key发生了改变。 - React 必须销毁所有旧的 DOM 节点并重新创建新的节点,而不是复用现有的节点。
- 性能浪费:即使内容没有改变,React 也会重新渲染整个列表。
4. 正确做法:使用唯一标识符(如 id)
{todos.map(todo => (
<div key={todo.id}>{todo.text}</div>
))}
这样,即使数组顺序变化,React 也能根据
id 正确识别元素,仅更新需要变化的部分,提高性能和稳定性。
四、受控组件 vs 非受控组件:表单状态管理的核心区别
React 中有两种主要方式来处理表单数据:受控组件(Controlled Components) 和 非受控组件(Uncontrolled Components) 。
1. 受控组件(Controlled Components)
- 状态由 React 管理,通过
useState控制输入框的值 - 所有输入变化都会触发
onChange并更新状态 - 更适合需要实时验证、联动状态、表单提交等复杂逻辑的场景
- 这意味着 React 完全控制了输入框的内容。
function ControlledInput({onSubmit}) {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
}
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(value);
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={handleChange}
/>
<button type="submit">提交</button>
</form>
)
}
关键点:
状态驱动输入框的值:输入框的值总是与 React 的状态同步。
实时响应:每次输入都会触发
onChange事件,更新状态并重新渲染输入框。
应用场景:
需要实时验证:例如表单验证、字符长度限制等。
联动逻辑:多个输入框之间有联动关系,如一个输入框的内容会影响另一个输入框的状态。
复杂表单:表单提交前需要进行复杂的处理或校验。
优点:
数据流清晰:所有数据都通过状态管理,易于追踪和调试。
实时反馈:可以立即根据用户的输入做出反应(如显示错误提示)。
缺点:
性能开销:频繁的
setState调用会导致组件频繁重新渲染,影响性能。可采用防抖节流。
2. 非受控组件(Uncontrolled Components)
- 输入框的值是由 DOM 自身管理的,而不是 React 的状态,通过
useRef直接访问 DOM 元素,获取输入框的值 - React 不直接管理输入框的值,只有在需要时(如表单提交)才去读取它的值。
- 更接近传统 HTML 表单行为
import { useRef } from 'react';
function UncontrolledInput() {
// 使用 useRef 获取输入框的引用
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// 通过 ref 获取输入框的值
console.log(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
{/* 输入框的值由 DOM 管理 */}
<input type="text" ref={inputRef} />
<button type="submit">提交</button>
</form>
);
}
优点:
性能更优:减少了状态更新和重新渲染的次数,适合简单的表单场景。
代码简洁:不需要编写额外的状态管理和事件处理逻辑。
缺点:
数据流不直观:由于数据存储在 DOM 中,难以追踪和调试。
不适合复杂交互:如果需要实时验证或联动逻辑,使用非受控组件会变得复杂。
3. 选择建议
| 场景 | 推荐组件类型 |
|---|---|
| 实时验证、联动逻辑、表单重置、动态控制 | 受控组件 |
| 性能优化、简单表单、快速获取初始值 | 非受控组件 |
五、总结:React 考点一览
| 考点 | 说明 |
|---|---|
| JSX | 本质是语法糖,需通过 Babel 编译为 React.createElement() |
| Babel | 编译器,将 ES6+ 语法转为 ES5,支持 JSX、async/await 等高级语法 |
| Key | 列表渲染必备,帮助 React 高效 Diff DOM,避免不必要的渲染 |
| Key 最佳实践 | 不使用 index,应使用唯一标识符(如 id) |
| 受控组件 | 表单状态由 React 管理,适合复杂交互 |
| 非受控组件 | 表单状态由 DOM 管理,适合简单表单或性能优化 |