key、JSX、Babel编译、受控组件与非受控组件、虚拟DOM考点解析

一、在react中,使用map遍历的时候为什么一定要带上key?

在 React 中使用 map 遍历生成元素时,key 是一个非常关键但容易被忽视的属性。很多开发者会疑惑:"明明不带 key 也能正常显示,为什么非要加呢?"。

jsx 复制代码
 const [todos,setTodos] = useState([
    {
        id:1,
        title:'吃饭',
    },
    {
        id:2,
        title:'睡觉',
    },
    {
        id:3,
        title:'打豆豆',
    }
  ])
  return (
    <>
      {
        todos.map((todo)=>{
            return <li>{todo.title}</li>
        })
      }
    </>
  )

结果是正常的,不带上key也能正常遍历出来,与带上key似乎没有什么差别,但当缺少 key 时,React 会默认使用数组索引作为隐式 key。这种方式在列表静态不变时看似正常,但在列表发生增删改查或排序时会引发严重问题:

js 复制代码
  useEffect(()=>{
        setTimeout(()=>{
            setTodos([     
                {
                    id:4,
                    title:'吃饭1',
                },
                ...todos,
            ])
        },1000)
    },[])
  return (
    <>
      {
        todos.map((todo)=>{
            return <li>{todo.title}</li>
        })
      }
    </>
  )

此时浏览器控制台会显示 所有 DOM 节点都被重新渲染(图中高亮的更新提示)。这是因为:

  • 无 key 时,React 用索引匹配新旧元素
  • 新增元素插入头部后,原有元素的索引全部错位
  • React 误判为 "所有元素都被修改",导致全量更新
text 复制代码
旧DOM顺序        新DOM顺序
---------       ---------
<li>吃饭</li>    <li>吃饭1</li>  // 内容被错误更新
<li>睡觉</li>    <li>吃饭</li>   // 应该保留原内容
<li>打豆豆</li>  <li>睡觉</li>   // 内容被污染
                <li>打豆豆</li> // 新增节点

这种 "错误复用" 不仅浪费性能(全量 DOM 重排重绘),还可能导致状态混乱(如输入框内容错位)。

如果加上了key会怎么样

jsx 复制代码
{
    todos.map((todo)=>{
        return <li key={todo.id}>{todo.title}</li>
    })
  }

reat建立虚拟DOM映射表

text 复制代码
Key映射关系:
1 → <li>吃饭</li>
2 → <li>睡觉</li> 
3 → <li>打豆豆</li>

当新增元素时:

text 复制代码
新增Key映射:
4 → <li>吃饭1</li>  // 仅插入此新节点
其他节点保持原位

从浏览器控制台可见,此时只有新增的节点被创建,原有节点无更新 ------ 这就是 key 带来的性能优化。

key 的本质作用

key 是 React 用于识别列表中元素唯一性的标识,它的核心作用是:

  • 帮助 React 区分不同元素,精准识别哪些元素被新增、删除或重新排序
  • 减少不必要的 DOM 操作,提升渲染性能
  • 避免因元素复用错误导致的状态混乱

下面是key的工作原理

graph TD A[新虚拟DOM] --> B{Key匹配?} B -->|是| C[复用现有DOM] B -->|否| D[创建新DOM] C --> E[属性更新] D --> F[插入DOM树]

总结:当使用 map 遍历生成元素却未指定 key 时,React 会默认将数组索引作为元素的标识来对比新旧虚拟 DOM。这种方式在列表发生新增、删除或重新排序时,会导致元素与索引的对应关系错位,进而引发 DOM 节点的错误复用------ 例如将原本属于 A 元素的 DOM 节点错误分配给 B 元素,导致节点内容被意外更新。这不仅会造成不必要的 DOM 重排与重绘,产生额外的性能开销,还可能引发表单输入值错位等状态混乱问题。

而当为元素指定唯一 key 后,React 能够通过 key 精准识别每个元素的身份,直接定位到需要新增、删除或更新的元素,从而只对变化的部分进行 DOM 操作,避免无意义的整体更新,既保证了渲染准确性,又提升了性能。

二、什么是JSX?

JSX(JavaScript XML)是 JavaScript 的语法扩展,允许在 JS 代码中嵌入 XML 风格的标签:

jsx 复制代码
// JSX 语法
const element = (
  <div className="container">
    <h1>Hello, React!</h1>
    <p>当前时间:{new Date().toLocaleTimeString()}</p>
  </div>
);

它既不是 HTML 也不是字符串,最终会被编译为普通的 JavaScript 函数调用。

JSX 的设计哲学

  • 声明式编程:描述 "UI 应该是什么样子",而非 "如何构建 UI"
  • 关注点分离:将 UI 结构与逻辑放在一起(组件),而非分离到 HTML 和 JS 文件
  • 直观性:相比纯 JS 创建元素,JSX 更接近视觉呈现的结构

JSX 与 HTML 的关键区别

虽然 JSX 看起来像 HTML,但存在多处语法差异:

特性 JSX 语法 HTML 语法 原因
类名 className class class 是 JavaScript 保留字
事件处理 onClick(驼峰式) onclick(全小写) 遵循 JS 变量命名规范
内联样式 style={{ color: 'red' }} style="color: red" JSX 中样式是对象
自闭合标签 必须闭合(<img /> 可省略(<img> 符合 XML 规范,避免歧义
注释 {/* 注释内容 */} <!-- 注释内容 --> 嵌入在 JS 环境中的注释

JSX 中的表达式

使用 {} 可以在 JSX 中嵌入任意 JavaScript 表达式:

jsx 复制代码
// 变量
const name = "React";
const user = { name: "Alice", age: 25 };

// 表达式嵌入
const profile = (
  <div>
    <h1>姓名:{name}</h1>
    <p>年龄:{user.age > 18 ? "成年" : "未成年"}</p>
    <p>爱好:{["阅读", "编程"].join("、")}</p>
  </div>
);

注意:{} 中只能放表达式(有返回值的代码),不能放语句(如 iffor)。

JSX能被直接运行吗? 并不能,JSX 是开发时 方便书写 的语法糖,但浏览器 只能运行标准的 JavaScript,所以必须经过编译转换才能执行。

三、JSX 的编译过程与 Babel 配置

JSX 不能被浏览器直接执行,必须经过编译转换。这个过程主要由 Babel 完成。

graph LR A[JSX代码] --> B[Babel编译] --> C[React.createElement调用] --> D[虚拟DOM对象] --> E[真实DOM]

JSX 的编译目标是将标签转换为 React.createElement 调用(或新转换中的 jsx 函数)。以这段代码为例:

jsx 复制代码
// 原始 JSX
const element = <h1 className="title">Hello, {name}!</h1>;

编译后会变成:

js 复制代码
// 传统编译结果
const element = React.createElement(
  'h1',          // 标签类型
  { className: 'title' },  // 属性对象
  'Hello, ',     // 子节点1
  name,          // 子节点2(表达式)
  '!'            // 子节点3
);

从 React 17 开始,引入了新的 JSX 转换,无需显式导入 React:

js 复制代码
// 新编译结果(自动导入运行时)
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx(
  'h1',
  { className: 'title', children: `Hello, ${name}!` }
);

Babel:JSX 编译的核心工具

Babel 是一个 JavaScript 编译器,负责将 JSX 转换为浏览器可执行的代码。以下是完整的配置与使用流程:

1.安装依赖

bash 复制代码
# 核心依赖
pnpm install react react-dom

# Babel 相关开发依赖
pnpm install --save-dev @babel/core @babel/cli @babel/preset-react

2.配置 Babel

创建 .babelrc 配置文件:

json 复制代码
{
  "presets": [
    [
      "@babel/preset-react",
      {
        "runtime": "automatic"  // 使用新的 JSX 运行时(推荐)
      }
    ]
  ]
}

3.编译命令

package.json 中添加脚本:

json 复制代码
{
  "scripts": {
    "build:jsx": "babel src --out-dir dist"  // 编译 src 目录到 dist
  }
}

4.执行编译

bash 复制代码
pnpm run build:jsx

新旧 JSX 转换的对比

React 17 引入的新 JSX 转换(runtime: "automatic")带来了显著改进:

特性 旧转换(runtime: "classic" 新转换(runtime: "automatic"
React 导入 必须手动导入 import React from 'react' 自动导入必要的运行时函数
打包体积 更大(包含冗余的 React.createElement 更小(仅导入所需函数)
兼容性 支持所有 React 版本 需 React 17+
自定义工厂 不支持 支持自定义 JSX 工厂函数

四、受控组件与非受控组件

在 React 中处理表单元素时,有两种核心模式:受控组件和非受控组件。它们的区别在于谁来管理表单数据

受控组件

表单数据由 React 组件的状态(useState)管理,表单元素的值通过 value 属性控制。

jsx 复制代码
function ControlledInput() {
  const [value, setValue] = useState('') // 响应式状态
  const [error, setError] = useState('')
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log(value, '//////');
  }
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="controlled-input">受控组件</label>
      <input 
        type="text" 
        value={value}
        onChange={(e) => setValue(e.target.value)}
        required
      />
      {error && <p>{error}</p>}
      <input type="submit" value="提交" />
    </form>
  )
}

非受控组件

表单数据由 DOM 自身管理,通过 ref 访问表单值。

jsx 复制代码
function UncontrolledRef() {
  const inputRef = useRef(null) // 非响应式状态
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log(inputRef.current.value);
  }
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="uncontrolled-input">非受控组件</label>
      <input
        type="text"
        id='uncontrolled-input'
        ref={inputRef}
      />
      <input type="submit" value="提交" />
    </form>
  )
}

两种模式的对比

特性 受控组件 非受控组件
数据存储 React 状态(useState DOM 元素自身
取值方式 直接从状态读取 通过 ref.current.value 读取
实时验证 容易实现(状态变化时验证) 较难(需监听 DOM 事件)
初始值 value(受控) defaultValue(仅初始化)
重渲染 输入时会触发 输入时不会触发
代码量 较多(需编写 onChange 较少(无需状态管理)
应用场景 需要实时验证、复杂表单交互、表单数据需要实时处理等交互性强的场景 简单表单、表单数据不需要实时处理、表单数据不需要实时验证等交互性不强的场景

五、什么是虚拟DOM ?

虚拟 DOM 是 JavaScript 对象,用于描述真实 DOM 的结构和属性。它是真实 DOM 的 "轻量副本",不依赖浏览器环境,仅存在于内存中。

例如,一段 JSX 对应的虚拟 DOM 如下:

jsx 复制代码
// JSX 结构
const element = <li className="active">吃饭</li>;

// 对应的虚拟 DOM 对象
const vdom = {
  type: 'li',          // 标签类型
  props: {             // 属性集合
    className: 'active',
    children: '吃饭'   // 子节点
  },
  key: null            // 若指定了 key 会包含在这里
};

可以看出,虚拟 DOM 用简单的键值对描述了真实 DOM 的所有信息,但比真实 DOM 更 "轻量"(没有浏览器相关的复杂属性和方法)。

为什么需要虚拟 DOM?

真实 DOM 操作是前端性能瓶颈之一 ------ 每次 DOM 更新都可能触发重排(重新计算布局)和重绘(重新绘制像素),代价高昂。

虚拟 DOM 的核心价值在于 减少真实 DOM 操作

  • 批量更新:先在内存中计算所有变化,再一次性同步到真实 DOM
  • 最小操作:通过对比新旧虚拟 DOM,只更新变化的部分(而非全量替换)
  • 跨平台兼容:虚拟 DOM 与平台无关,让 React 能同时支持浏览器、移动端(React Native)等环境

虚拟 DOM 的工作流程

React 利用虚拟 DOM 实现更新的过程可分为三步:

graph LR A[组件状态变化] --> B[生成新虚拟DOM] B --> C(Diff算法对比新旧虚拟DOM) C --> D[计算出最小更新范围] D --> E[同步变化到真实DOM]

具体拆解:

  1. 状态变化触发重新渲染 :当组件的 stateprops 变化时,React 会重新调用组件函数,生成新的虚拟 DOM。

  2. Diff 算法对比差异:React 会对比新旧两个虚拟 DOM 树,找出需要更新的部分(这个过程称为 "协调")。

  • 对比规则:先按 type(标签类型)和 key 匹配节点,再对比 props 和子节点
  • 优化策略:只做同级对比(不跨层级比较),大幅减少计算量
  1. 更新真实 DOM:React 只将差异部分同步到真实 DOM,避免全量替换。

虚拟 DOM 一定更快吗?

虚拟 DOM 的优势在于 减少不必要的真实 DOM 操作,但并非在所有场景下都比直接操作 DOM 快:

  • 对于简单的单次更新(如修改一个文本),直接操作 DOM 可能更快(省去虚拟 DOM 的计算开销)
  • 对于复杂组件或频繁更新,虚拟 DOM 的批量处理和最小更新策略能显著提升性能

React 的目标不是 "比原生 DOM 快",而是通过虚拟 DOM 提供 更一致的开发体验和可预测的更新机制,同时在大多数场景下保证良好性能。

相关推荐
ai小鬼头9 分钟前
创业小公司如何低预算打造网站?熊哥的实用建站指南
前端·后端
洋流16 分钟前
0基础进大厂,React框架基础篇:创建你的第一个React框架项目——梦开始的地方
react.js
阿星做前端16 分钟前
聊聊前端请求拦截那些事
前端·javascript·面试
阿凤2121 分钟前
在UniApp中防止页面上下拖动的方法
前端·uni-app
拾光拾趣录29 分钟前
DocumentFragment:高性能DOM操作
前端·dom
归于尽1 小时前
从JS到TS:我们放弃了自由,却赢得了整个世界
前端·typescript
palpitation971 小时前
Fitten Code使用体验
前端
byteroycai1 小时前
用 Tauri + FFmpeg + Whisper.cpp 从零打造本地字幕生成器
前端
用户1512905452201 小时前
C 语言教程
前端·后端
UestcXiye1 小时前
Rust Web 全栈开发(十):编写服务器端 Web 应用
前端·后端·mysql·rust·actix