Appearance
Key的作用与优化
学习目标
通过本章学习,你将深入理解:
- key属性的作用和重要性
- key的选择原则和最佳实践
- 使用index作为key的问题和场景
- key与React Diff算法的深层关系
- key对性能的具体影响
- 常见错误和完整解决方案
- 复杂场景下的key策略
- React 19中的key优化
第一部分:key的作用
1.1 为什么需要key
jsx
// ❌ 没有key的问题演示
function WithoutKeyProblem() {
const [items, setItems] = useState(['A', 'B', 'C']);
const addToStart = () => {
setItems(['New', ...items]);
};
return (
<div>
<button onClick={addToStart}>添加到开头</button>
<ul>
{items.map(item => (
<li>
<input type="checkbox" />
<span>{item}</span>
</li>
))}
</ul>
{/*
问题:
1. 控制台警告:Each child should have a unique "key"
2. 添加新项后,checkbox的选中状态会错位
3. 原因:React无法识别哪个元素是新的,哪个是旧的
*/}
</div>
);
}
// ✅ 使用key解决
function WithKeySolution() {
const [items, setItems] = useState([
{ id: 1, text: 'A' },
{ id: 2, text: 'B' },
{ id: 3, text: 'C' }
]);
const addToStart = () => {
setItems([
{ id: Date.now(), text: 'New' },
...items
]);
};
return (
<div>
<button onClick={addToStart}>添加到开头</button>
<ul>
{items.map(item => (
<li key={item.id}>
<input type="checkbox" />
<span>{item.text}</span>
</li>
))}
</ul>
{/*
有key后:
1. 没有警告
2. checkbox状态正确保持
3. React知道哪个是新元素,正确地只插入新元素
*/}
</div>
);
}1.2 key与Diff算法的关系
jsx
// Diff算法如何使用key
// 场景1:在列表开头插入元素
// 旧列表
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
// 新列表(在开头插入D)
<ul>
<li key="d">D</li>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
// 有key的情况:
// React通过key识别:
// 1. key="d"是新的 → 插入新元素
// 2. key="a","b","c"已存在 → 复用DOM
// 结果:只创建1个新DOM元素
// 无key的情况:
// React按位置比较:
// 1. 位置0: "A"变为"D" → 更新DOM
// 2. 位置1: "B"变为"A" → 更新DOM
// 3. 位置2: "C"变为"B" → 更新DOM
// 4. 位置3: 无变为"C" → 插入DOM
// 结果:更新3个DOM + 插入1个DOM(效率低)
function DiffAlgorithmDemo() {
const [items, setItems] = useState([
{ id: 'a', text: 'A' },
{ id: 'b', text: 'B' },
{ id: 'c', text: 'C' }
]);
const [operations, setOperations] = useState([]);
const addToStart = () => {
const newItem = { id: 'd', text: 'D' };
setItems([newItem, ...items]);
setOperations(['插入新元素D']);
};
const reverse = () => {
setItems([...items].reverse());
setOperations(['重新排列元素(无新增)']);
};
const deleteFirst = () => {
setItems(items.slice(1));
setOperations(['删除第一个元素']);
};
return (
<div>
<div>
<button onClick={addToStart}>开头插入</button>
<button onClick={reverse}>反转</button>
<button onClick={deleteFirst}>删除第一个</button>
</div>
<div>
<h4>操作记录:</h4>
<ul>
{operations.map((op, i) => (
<li key={i}>{op}</li>
))}
</ul>
</div>
<div>
<h4>列表:</h4>
<ul>
{items.map(item => (
<li key={item.id}>
{item.text}
<input type="text" defaultValue={item.text} />
</li>
))}
</ul>
</div>
</div>
);
}1.3 key的唯一性要求
jsx
function KeyUniqueness() {
const items = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 1, name: 'C' } // ⚠️ id重复!
];
return (
<div>
{/* ❌ 错误:key重复 */}
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
{/* Warning: Encountered two children with the same key */}
{/* ✅ 正确:确保key唯一 */}
<ul>
{items.map((item, index) => (
<li key={`${item.id}-${index}`}>{item.name}</li>
))}
</ul>
{/* 💡 key只需要在兄弟节点中唯一,不同列表可以有相同key */}
<div>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<ol>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ol>
{/* 两个不同的列表,可以有相同的key */}
</div>
</div>
);
}第二部分:key的选择
2.1 使用唯一ID(最佳实践)
jsx
function BestKeyPractice() {
// ✅ 最佳:数据库ID
const users = [
{ userId: 101, name: 'Alice' },
{ userId: 102, name: 'Bob' }
];
return (
<ul>
{users.map(user => (
<li key={user.userId}>{user.name}</li>
))}
</ul>
);
}
// ✅ 好:UUID
import { v4 as uuidv4 } from 'uuid';
function UUIDKey() {
const [items, setItems] = useState([
{ id: uuidv4(), text: 'Item 1' },
{ id: uuidv4(), text: 'Item 2' }
]);
const addItem = () => {
setItems([
...items,
{ id: uuidv4(), text: `Item ${items.length + 1}` }
]);
};
return (
<div>
<button onClick={addItem}>添加</button>
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
);
}
// ✅ 可以:时间戳(单线程添加)
function TimestampKey() {
const [items, setItems] = useState([]);
const addItem = () => {
setItems([
...items,
{ id: Date.now(), text: 'New Item' }
]);
};
return (
<div>
<button onClick={addItem}>添加</button>
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
);
}2.2 使用index作为key(谨慎)
jsx
// ✅ 可以使用index的场景
function SafeIndexUsage() {
// 条件1:列表是静态的(不会变化)
const staticItems = ['首页', '关于', '联系我们'];
// 条件2:列表不会重排序
// 条件3:列表不会过滤
// 条件4:列表项没有ID
return (
<nav>
{staticItems.map((item, index) => (
<a key={index} href={`/${item}`}>
{item}
</a>
))}
</nav>
);
}
// ❌ 不能使用index的场景
function UnsafeIndexUsage() {
const [items, setItems] = useState([
{ text: 'A' },
{ text: 'B' },
{ text: 'C' }
]);
const shuffle = () => {
setItems([...items].sort(() => Math.random() - 0.5));
};
const addToStart = () => {
setItems([{ text: 'New' }, ...items]);
};
const remove = (index) => {
setItems(items.filter((_, i) => i !== index));
};
return (
<div>
<button onClick={shuffle}>打乱</button>
<button onClick={addToStart}>添加到开头</button>
<ul>
{/* ❌ 不好:列表会重排序,使用index会导致问题 */}
{items.map((item, index) => (
<li key={index}>
<input type="text" defaultValue={item.text} />
<button onClick={() => remove(index)}>删除</button>
</li>
))}
</ul>
{/*
问题:
- 打乱顺序后,输入框的值和items对不上
- 删除元素后,可能删错
*/}
</div>
);
}
// ✅ 正确:使用稳定的ID
function StableKeyUsage() {
const [items, setItems] = useState([
{ id: 1, text: 'A' },
{ id: 2, text: 'B' },
{ id: 3, text: 'C' }
]);
const shuffle = () => {
setItems([...items].sort(() => Math.random() - 0.5));
};
const addToStart = () => {
setItems([{ id: Date.now(), text: 'New' }, ...items]);
};
const remove = (id) => {
setItems(items.filter(item => item.id !== id));
};
return (
<div>
<button onClick={shuffle}>打乱</button>
<button onClick={addToStart}>添加到开头</button>
<ul>
{items.map(item => (
<li key={item.id}>
<input type="text" defaultValue={item.text} />
<button onClick={() => remove(item.id)}>删除</button>
</li>
))}
</ul>
{/*
优点:
- 无论怎么打乱,每个元素的key都稳定
- 输入框的值总是跟随正确的item
- 删除操作准确无误
*/}
</div>
);
}2.3 组合key
jsx
// 多级列表的key组合
function CompositeKeyExample() {
const categories = [
{
id: 1,
name: '分类1',
subcategories: [
{
id: 1, // ⚠️ 注意:子分类的id可能与其他分类的子分类重复
name: '子分类1-1',
items: [
{ id: 1, name: '项目1-1-1' }, // ⚠️ 项目id也可能重复
{ id: 2, name: '项目1-1-2' }
]
},
{
id: 2,
name: '子分类1-2',
items: [
{ id: 1, name: '项目1-2-1' }, // id=1,与上面重复
{ id: 2, name: '项目1-2-2' }
]
}
]
},
{
id: 2,
name: '分类2',
subcategories: [
{
id: 1, // id=1,与分类1的子分类重复
name: '子分类2-1',
items: [
{ id: 1, name: '项目2-1-1' }
]
}
]
}
];
return (
<div className="multi-level-list">
{categories.map(category => (
// 第一层:category.id是唯一的
<div key={category.id} className="category">
<h2>{category.name}</h2>
{category.subcategories.map(sub => (
// 第二层:组合category.id和sub.id确保唯一
<div key={`${category.id}-${sub.id}`} className="subcategory">
<h3>{sub.name}</h3>
<ul>
{sub.items.map(item => (
// 第三层:组合所有层级的id确保唯一
<li key={`${category.id}-${sub.id}-${item.id}`}>
{item.name}
</li>
))}
</ul>
</div>
))}
</div>
))}
</div>
);
}
// 更简洁的组合key方法
function CompositeKeyHelper() {
// 创建组合key的辅助函数
const createKey = (...parts) => parts.join('-');
return (
<div>
{categories.map(cat => (
<div key={cat.id}>
{cat.subcategories.map(sub => (
<div key={createKey(cat.id, sub.id)}>
{sub.items.map(item => (
<li key={createKey(cat.id, sub.id, item.id)}>
{item.name}
</li>
))}
</div>
))}
</div>
))}
</div>
);
}2.4 key的生成策略
jsx
// 策略1:使用数据的自然ID
function NaturalIDKey({ products }) {
return (
<ul>
{products.map(product => (
<li key={product.sku}> {/* 使用SKU作为key */}
{product.name}
</li>
))}
</ul>
);
}
// 策略2:组合多个字段
function CompositeFieldKey({ orders }) {
return (
<ul>
{orders.map(order => (
<li key={`${order.userId}-${order.timestamp}`}>
订单 #{order.orderId}
</li>
))}
</ul>
);
}
// 策略3:内容哈希
import { hashCode } from './utils';
function ContentHashKey({ items }) {
return (
<ul>
{items.map(item => (
<li key={hashCode(JSON.stringify(item))}>
{item.content}
</li>
))}
</ul>
);
}
// 策略4:自动生成ID
function AutoGeneratedKey() {
const [items, setItems] = useState([]);
const nextId = useRef(1);
const addItem = (text) => {
setItems([
...items,
{ id: nextId.current++, text }
]);
};
return (
<div>
<button onClick={() => addItem('New Item')}>添加</button>
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
);
}第三部分:key的性能影响
3.1 正确的key提升性能
jsx
function PerformanceComparison() {
const [items, setItems] = useState(
Array.from({ length: 1000 }, (_, i) => ({
id: i,
text: `Item ${i}`,
value: Math.random()
}))
);
const deleteFirst = () => {
setItems(items.slice(1));
};
const addToStart = () => {
setItems([
{ id: Date.now(), text: 'New', value: Math.random() },
...items
]);
};
const shuffle = () => {
setItems([...items].sort(() => Math.random() - 0.5));
};
return (
<div>
<div>
<button onClick={deleteFirst}>删除第一项</button>
<button onClick={addToStart}>开头添加</button>
<button onClick={shuffle}>打乱顺序</button>
</div>
<ul>
{items.map(item => (
<li key={item.id}>
<input type="text" defaultValue={item.text} />
<span>{item.value.toFixed(3)}</span>
</li>
))}
</ul>
{/*
性能分析:
使用item.id作为key:
- 删除第一项:React识别删除了id=0的项,其他999项保持不变
- 开头添加:React只需插入1个新元素
- 打乱顺序:React知道元素只是移动位置,复用所有DOM
使用index作为key:
- 删除第一项:所有index都变了,React认为所有项都变了
- 需要更新999个DOM元素
- 性能差距:10-100倍
*/}
</div>
);
}3.2 错误的key降低性能
jsx
// ❌ 最差:使用随机数
function WorstKeyPractice() {
const items = ['A', 'B', 'C'];
return (
<ul>
{items.map(item => (
<li key={Math.random()}> {/* ❌ 每次渲染都是新key */}
<input type="text" defaultValue={item} />
</li>
))}
</ul>
);
// 后果:
// - 每次渲染,React都认为是全新的元素
// - 完全销毁重建所有DOM
// - 输入框的值会丢失
// - 极差的性能
}
// ❌ 很差:使用不稳定的值
function BadKeyPractice() {
const items = ['A', 'B', 'C'];
return (
<ul>
{items.map(item => (
<li key={Date.now() + item}> {/* ❌ 时间戳不稳定 */}
{item}
</li>
))}
</ul>
);
}
// ❌ 差:使用对象作为key
function ObjectAsKey() {
const items = [
{ data: { id: 1 }, name: 'A' },
{ data: { id: 2 }, name: 'B' }
];
return (
<ul>
{items.map(item => (
<li key={item.data}> {/* ❌ 对象会转为字符串"[object Object]" */}
{item.name}
</li>
))}
</ul>
);
}第四部分:复杂场景的key处理
4.1 动态列表的key
jsx
function DynamicListKey() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
// 过滤后的todos
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);
return (
<div>
<div>
<button onClick={() => setFilter('all')}>全部</button>
<button onClick={() => setFilter('active')}>进行中</button>
<button onClick={() => setFilter('completed')}>已完成</button>
</div>
<ul>
{filteredTodos.map(todo => (
// ✅ 使用todo.id作为key,即使过滤改变,key仍然稳定
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
</li>
))}
</ul>
</div>
);
}4.2 分页列表的key
jsx
function PaginatedListKey({ items, pageSize }) {
const [currentPage, setCurrentPage] = useState(1);
const currentItems = useMemo(() => {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
return items.slice(start, end);
}, [items, currentPage, pageSize]);
return (
<div>
<ul>
{currentItems.map(item => (
// ✅ 使用item.id,而不是页内索引
<li key={item.id}>
{item.name}
</li>
))}
</ul>
<div>
<button onClick={() => setCurrentPage(p => p - 1)}>上一页</button>
<span>第 {currentPage} 页</span>
<button onClick={() => setCurrentPage(p => p + 1)}>下一页</button>
</div>
</div>
);
}第五部分:最佳实践
5.1 key选择决策树
jsx
// 选择key的决策流程
function KeyDecisionTree() {
const decisionFlow = `
数据有唯一ID吗?
├─ 是 → 使用数据ID作为key ✅
│ 例如:key={item.id}
│
└─ 否 → 列表会重排序、过滤或改变吗?
├─ 是 → 生成稳定的唯一ID ✅
│ 例如:key={uuidv4()} 或 key={Date.now()}
│
└─ 否 → 列表是否静态不变?
├─ 是 → 可以使用index ⚠️
│ 例如:key={index}
│
└─ 否 → 组合多个字段生成唯一key ✅
例如:key={`${item.type}-${item.name}`}
`;
console.log(decisionFlow);
}5.2 key的常见错误
jsx
// ❌ 错误1:使用不稳定的key
function MistakeUnstableKey() {
const items = ['A', 'B', 'C'];
return (
<ul>
{items.map(item => (
<li key={Math.random()}>{item}</li> // 每次都变
))}
</ul>
);
}
// ❌ 错误2:key重复
function MistakeDuplicateKey() {
const items = [
{ id: 1, name: 'A' },
{ id: 1, name: 'B' }, // id重复
{ id: 2, name: 'C' }
];
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// ❌ 错误3:用index但列表会变化
function MistakeIndexWithChanges() {
const [items, setItems] = useState(['A', 'B', 'C']);
return (
<ul>
{items.map((item, index) => (
<li key={index}> // 列表会删除、添加、排序
<input defaultValue={item} />
</li>
))}
</ul>
);
}
// ❌ 错误4:key包含对象
function MistakeObjectInKey() {
const items = [
{ data: { id: 1 }, name: 'A' }
];
return (
<ul>
{items.map(item => (
<li key={item.data}>{item.name}</li> // 对象转字符串
))}
</ul>
);
}
// ✅ 正确的修复
function CorrectKeyUsage() {
const [items, setItems] = useState([
{ id: uuidv4(), name: 'A' },
{ id: uuidv4(), name: 'B' },
{ id: uuidv4(), name: 'C' }
]);
return (
<ul>
{items.map(item => (
<li key={item.id}>
<input defaultValue={item.name} />
</li>
))}
</ul>
);
}第六部分:实战案例
6.1 Todo列表的key
jsx
function TodoListWithKey() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习React', completed: false, priority: 'high' },
{ id: 2, text: '写代码', completed: false, priority: 'medium' },
{ id: 3, text: '看文档', completed: true, priority: 'low' }
]);
const [filter, setFilter] = useState('all');
const [sortBy, setSortBy] = useState('default');
// 过滤
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(t => !t.completed);
case 'completed':
return todos.filter(t => t.completed);
default:
return todos;
}
}, [todos, filter]);
// 排序
const sortedTodos = useMemo(() => {
const result = [...filteredTodos];
switch (sortBy) {
case 'priority':
const order = { high: 0, medium: 1, low: 2 };
return result.sort((a, b) => order[a.priority] - order[b.priority]);
case 'name':
return result.sort((a, b) => a.text.localeCompare(b.text));
default:
return result;
}
}, [filteredTodos, sortBy]);
const addTodo = (text) => {
setTodos([
...todos,
{
id: Date.now(), // ✅ 使用时间戳作为唯一ID
text,
completed: false,
priority: 'medium'
}
]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<div className="controls">
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="all">全部</option>
<option value="active">进行中</option>
<option value="completed">已完成</option>
</select>
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="default">默认排序</option>
<option value="priority">按优先级</option>
<option value="name">按名称</option>
</select>
</div>
<ul className="todo-list">
{sortedTodos.map(todo => (
// ✅ 关键:使用todo.id作为key
// 无论怎么过滤、排序,每个todo的key都不变
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<span className={`priority-${todo.priority}`}>
{todo.priority}
</span>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}6.2 可拖拽列表的key
jsx
function DraggableListWithKey() {
const [items, setItems] = useState([
{ id: 'item-1', text: 'Item 1', order: 0 },
{ id: 'item-2', text: 'Item 2', order: 1 },
{ id: 'item-3', text: 'Item 3', order: 2 }
]);
const [draggedId, setDraggedId] = useState(null);
const handleDragStart = (id) => {
setDraggedId(id);
};
const handleDrop = (dropId) => {
if (!draggedId) return;
const dragIndex = items.findIndex(item => item.id === draggedId);
const dropIndex = items.findIndex(item => item.id === dropId);
const newItems = [...items];
const [dragged] = newItems.splice(dragIndex, 1);
newItems.splice(dropIndex, 0, dragged);
setItems(newItems);
setDraggedId(null);
};
return (
<ul>
{items.map(item => (
// ✅ 使用稳定的item.id作为key
// 拖拽改变顺序时,React知道元素只是移动,不是新建
<li
key={item.id}
draggable
onDragStart={() => handleDragStart(item.id)}
onDragOver={e => e.preventDefault()}
onDrop={() => handleDrop(item.id)}
style={{
opacity: draggedId === item.id ? 0.5 : 1,
cursor: 'move'
}}
>
{item.text}
</li>
))}
</ul>
);
}练习题
基础练习
- 理解key的作用,创建有key和无key的列表对比
- 对比使用index和id作为key的差异
- 修复key相关的警告
- 实现一个使用组合key的嵌套列表
进阶练习
- 实现一个可拖拽排序的列表,体验key的重要性
- 优化大列表的key选择策略
- 处理动态列表的key(支持增删改查)
- 实现一个性能对比工具,展示不同key策略的性能差异
高级练习
- 分析key对React Diff算法的具体影响
- 实现一个智能key生成器
- 优化复杂嵌套列表的key策略
- 创建一个key问题诊断工具
通过本章学习,你已经深入理解了key的作用、选择和优化策略。正确使用key是React列表渲染的核心,也是性能优化的关键。记住这个黄金法则:永远使用稳定且唯一的ID作为key,避免使用index(除非列表完全静态)!继续学习,成为React列表渲染专家!