一、在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的工作原理
总结:当使用 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>
);
注意:
{}
中只能放表达式(有返回值的代码),不能放语句(如if
、for
)。
JSX能被直接运行吗? 并不能,JSX 是开发时 方便书写 的语法糖,但浏览器 只能运行标准的 JavaScript,所以必须经过编译转换才能执行。
三、JSX 的编译过程与 Babel 配置
JSX 不能被浏览器直接执行,必须经过编译转换。这个过程主要由 Babel 完成。
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 实现更新的过程可分为三步:
具体拆解:
-
状态变化触发重新渲染 :当组件的
state
或props
变化时,React 会重新调用组件函数,生成新的虚拟 DOM。 -
Diff 算法对比差异:React 会对比新旧两个虚拟 DOM 树,找出需要更新的部分(这个过程称为 "协调")。
- 对比规则:先按
type
(标签类型)和key
匹配节点,再对比props
和子节点 - 优化策略:只做同级对比(不跨层级比较),大幅减少计算量
- 更新真实 DOM:React 只将差异部分同步到真实 DOM,避免全量替换。
虚拟 DOM 一定更快吗?
虚拟 DOM 的优势在于 减少不必要的真实 DOM 操作,但并非在所有场景下都比直接操作 DOM 快:
- 对于简单的单次更新(如修改一个文本),直接操作 DOM 可能更快(省去虚拟 DOM 的计算开销)
- 对于复杂组件或频繁更新,虚拟 DOM 的批量处理和最小更新策略能显著提升性能
React 的目标不是 "比原生 DOM 快",而是通过虚拟 DOM 提供 更一致的开发体验和可预测的更新机制,同时在大多数场景下保证良好性能。