本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
一、key参数的核心作用
1. 核心机制
- 唯一标识:
key
用于唯一标识数组中的每个元素,帮助ArkUI识别哪些项被修改/删除 - 性能优化:正确使用
key
可减少不必要的组件重建(复用已有组件) - 状态保持:确保动态数组操作时组件状态不丢失
2. 工作原理图示
A[数据变更] --> B{对比新旧VDOM}
B -->|通过key匹配| C[复用已有节点]
B -->|无匹配key| D[创建新节点]
二、key的使用规范
1. 基本语法
ForEach(
arr: Array<any>,
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
2. 推荐写法
// 方式1:对象唯一ID(最优)
ForEach(userList, (user: User) => {
Text(user.name)
}, (user: User) => user.id)
// 方式2:索引作为备选(需谨慎)
ForEach(userList, (user: User, index: number) => {
Text(user.name)
}, (user: User, index: number) => index.toString())
3. 默认key的生成机制 (数组索引作为key)
系统会自动使用当前遍历项的数组索引(index)作为默认key,相当于:
ForEach(arr, (item, index) => {
// 组件内容
}, (item, index) => index.toString()) // 隐式行为
内部逻辑实现原理:
// 伪代码展示内部逻辑
function defaultKeyGenerator(item: any, index: number): string {
return `_${index}`; // 实际实现可能更复杂,但本质仍是索引
}
底层原理说明:
ArkUI的虚拟DOM对比算法(类似React的Reconciliation)通过key判断:
- 相同key ⇒ 复用组件实例
- 不同key ⇒ 销毁旧实例创建新实例
三、key的进阶用法
1. 复合key生成
当数据没有唯一ID时,可组合多个字段:
ForEach(products, (product) => {
ProductItem({ data: product })
}, (product) => `${product.category}_${product.createTime}`)
2. 动态key更新
@State items: Array<{ id: string, content: string }> = [...]
build() {
Column() {
// 动态修改key关联的数据
ForEach(this.items, (item) => {
Text(item.content).fontSize(20)
}, (item) => item.id)
Button('更新数据').onClick(() => {
this.items[0] = { ...this.items[0], content: '新内容' }; // 保持id不变
})
}
}
3. 与@Observed配合
@Observed
class Item {
key: string = generateUUID(); // 推荐在数据模型中内置key
content: string = '';
}
ForEach(this.itemList, (item: Item) => {
ItemComponent({ data: item })
}, (item: Item) => item.key)
四、常见问题与解决方案
1. 错误示范
// 反例1:使用随机数(每次渲染key都变化)
ForEach(items, (item) => { ... }, () => Math.random().toString())
// 反例2:使用非稳定标识(如对象内存地址)
ForEach(items, (item) => { ... }, (item) => JSON.stringify(item))
2. 性能对比测试
key策略 | 1000项渲染时间 | 状态保持正确性 |
---|---|---|
唯一ID | 120ms | ✔️ |
索引index | 450ms | ❌(排序出错) |
随机数 | 900ms+ | ❌ |
3. 数据操作时的bug
操作类型 | 无显式key的后果 | 示例场景 |
---|---|---|
数组排序 | 组件状态错乱(如选中状态丢失) | 用户点击排序按钮后列表项状态混乱 |
中间插入 | 性能下降(大量组件重建) | 在长列表头部插入新项导致卡顿 |
动态过滤 | 不必要的子组件重新渲染 | 搜索过滤时已有项重复渲染 |
五、总结
-
优先使用数据对象中的唯一标识(如数据库ID)
// 数据模型示例 class User { userId: string; // 持久化唯一ID name: string; }
-
复杂场景下的key生成方案:
// 方案1:时间戳+随机后缀(无唯一ID时) const genKey = () => `${Date.now()}_${Math.random().toString(36).substr(2, 5)}`; // 方案2:哈希算法(需引入库) import { sha256 } from '@ohos/crypto'; const hashKey = async (text: string) => await sha256(text);
-
需要避免的情况:
- 使用数组索引作为key(当存在排序/过滤操作时)
- 在key生成函数中执行耗时操作
-
何时可以省略key
//静态列表:数据永远不会改变顺序或增减
const FIXED_LIST = ['A', 'B', 'C']; // 常量数组
ForEach(FIXED_LIST, (item) => Text(item)) // 可省略key
//无状态组件:列表项不包含任何内部状态
ForEach(tags, (tag) => Text(tag)) // 纯展示无交互
5. 调试技巧
ForEach(items, (item) => {
Text(item.name).onAppear(() => {
console.log(`渲染项key: ${item.id}`); // 监控渲染项
})
}, (item) => item.id)
6. 自动生成key的工具函数
// 安全key生成方案(无ID时)
function safeKey(item: any, index: number): string {
return item.id ??
item.key ??
`${index}_${JSON.stringify(item).hashCode()}`;
}
ForEach(data, (item) => UI(), safeKey)
六、完整示例
@Entry
@Component
struct KeyDemo {
@State userList: Array<{ uid: string, name: string }> = [
{ uid: '1', name: '张三' },
{ uid: '2', name: '李四' }
];
build() {
Column() {
// 正确用法 - 使用唯一ID
ForEach(this.userList, (user) => {
UserItem({ user })
.margin(5)
}, (user) => user.uid)
Button('添加用户').onClick(() => {
this.userList.push({
uid: `new_${Date.now()}`, // 确保新项有唯一key
name: '新用户'
});
})
Button('打乱顺序').onClick(() => {
this.userList = [...this.userList].sort(() => Math.random() - 0.5);
})
}
}
}
@Component
struct UserItem {
@Link user: { uid: string, name: string };
@State voteCount: number = 0; // 状态依赖key保持
build() {
Row() {
Text(this.user.name)
Text(`票数: ${this.voteCount}`)
.onClick(() => { this.voteCount++ })
}
}
}