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

156 阅读9分钟

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

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

 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。这种方式在列表静态不变时看似正常,但在列表发生增删改查或排序时会引发严重问题:

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

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

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

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

如果加上了key会怎么样

{
    todos.map((todo)=>{
        return <li key={todo.id}>{todo.title}</li>
    })
  }

image.png reat建立虚拟DOM映射表

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

当新增元素时:

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

JSX 中的表达式

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

// 变量
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
const element = <h1 className="title">Hello, {name}!</h1>;

编译后会变成:

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

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

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

Babel:JSX 编译的核心工具

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

1.安装依赖

# 核心依赖
pnpm install react react-dom

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

2.配置 Babel

创建 .babelrc 配置文件:

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

3.编译命令

在 package.json 中添加脚本:

{
  "scripts": {
    "build:jsx": "babel src --out-dir dist"  // 编译 src 目录到 dist
  }
}

4.执行编译

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 属性控制。

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 访问表单值。

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

五、什么是虚拟DOM ?

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

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

// 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. 状态变化触发重新渲染:当组件的 state 或 props 变化时,React 会重新调用组件函数,生成新的虚拟 DOM。

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

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

虚拟 DOM 一定更快吗?

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

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

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