原本觉得用 index 做 key 是低级错误,但在小程序这个特殊的运行环境里,这恰恰是赤石的开始。
在 Web 端写虚拟列表,我习惯这么写:
一、 插槽命名重复警告
<view v-for="item in visibleList" :key="item.id">
<slot name="item" :data="item" />
</view>
当你满心欢喜地在 UniApp 微信小程序里跑起来时,你会发现一滑动列表控制台就开始疯狂输出:
More than one slot named "list-item-0" are found...
我明明每个 ID 都是唯一的,为什么它说我 slot 重名?
二、 微信小程序插槽特性
这是因为微信小程序原生根本不支持 Vue 那种动态、灵活的作用域插槽。UniApp 为了实现这个功能,在编译阶段做了一个骚操作:
-
静态预留:编译器会在
.wxml里预先生成一堆具名插槽,名字就叫list-item-0,list-item-1... 你设定的buffer有多大,它就预留多少个。 -
Key 的蝴蝶效应:
- 如果你用
index做 Key:滚动时,Vue 的 Diff 算法发现 Key 没变(永远是 0, 1, 2...),它认为 DOM 结构是稳定的。它只会更新插槽里的数据内容,而不会去动插槽节点本身。 - 如果你用
item.id做 Key:滚动时,旧 ID 消失,新 ID 出现。Vue 会试图销毁旧的插槽节点,并创建一个新的插槽节点。
- 如果你用
三、 双线程
为什么销毁再创建会报错?
因为小程序是 逻辑层(JS) 和 渲染层(WXML) 双线程异步通信的。
- 逻辑层:快如闪电,一瞬间删掉了 ID 1,创建了 ID 11。
- 渲染层:慢半拍。当逻辑层告诉渲染层“我要挂载 ID 11 对应的
list-item-0”时,渲染层还没来得及把旧的 ID 1 给删掉。
结果就是 渲染层在同一时间,看到了两个都叫 list-item-0 的插槽,于是就报错了。
四、 我认命了
在 Web 端,用 index 做 Key 会导致节点复用异常、组件状态错乱;但在小程序虚拟列表里,不用 index 就会疯狂报错警告。
最后我还是选择了使用 :key="index"。
总结
在写小程序时,要时刻提醒自己:它不是浏览器。 很多 Web 端的特性在小程序双线程架构和静态编译的各种限制下,可能就是导致项目崩溃的原因。