React 面试考点解析:JSX、Babel 编译、Key 原理与受控/非受控组件

119 阅读7分钟

前言

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

e8cbd227216a8c8cf748bd4a1a27c181.png 编译过程:

pnpm i @babel/cli @babel/core -D

84a9d7a675a27165946c6b60130c6d74.png

  • @babel/core:Babel 核心库
  • @babel/cli:命令行工具,用于执行编译命令
  • -D:开发阶段的依赖 devDependencies

安装react核心运行库image.png-D和不加的区别如下: image.png

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 配置文件位置
  • .babelrc
  • babel.config.json
  • 或者在 package.json 中: image.png
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"
  ]
}

image.png 这样配置后:

  • @babel/preset-env:负责把 ES6+ 转成 ES5
  • @babel/preset-react:负责把 JSX 转成 React.createElement() 调用

5. 执行编译

./node_modules/.bin/babel 1.jsx -o 2.jsx

1851f47fcaab16509a470ea6df7153fb.png


三、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: '标题四'
  }
])

image.png

  • React 会依次比较每一项
  • 旧的前 3 项和新的前 3 项一致,直接复用
  • 新增的第 4 项会被创建并插入

✅ 性能尚可,没有问题。

场景二:在数组开头插入新项(性能大打折扣)
setTodos(prev => [
  {
    id: 4,
    title: '标题四'
  },
  ...prev
])

此时:

  • React 默认按顺序比较新旧列表
  • 原来第 1 项变成了新列表的第 2 项
  • 原来第 2 项变成了新列表的第 3 项……

React 无法识别这些项是否是“复用”的,只能全部重新创建并渲染,造成 不必要的性能浪费

lQLPJwnImMcHD1HNATDNAWiwhgRDh7LiBIwIWcJBiwpuAA_360_304.png

场景三:使用 index 作为 key
todos.map((todo, index) => (
  <li key={index}>{todo.title}</li> /*使用index作为key*/
))

image.png

此时,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>
))}

image.png 这样,即使数组顺序变化,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 管理,适合简单表单或性能优化