使用index作为虚拟DOM的key导致的问题
之前遇到过一个设计者开的bug。说的是当用户在列表中添加一行记录时,界面上的CheckBox异常执行了一个Animation效果。当我收到这个bug时,我的第一反应是看看CheckBox对应的布尔值有没有变化。确认没有变化后,我觉得有必要去考察一下控件CSS的实现逻辑,所以把这个bug搁置一旁,因为不影响数据,算是低优先级的问题。直到我有机会处理剩余的bug时,我才再次尝试解决它。问题描述我用一个例子来描述我当时遇到的问题。假设我们用一个列表来实现某个业务功能。列表中每一行的最左边是一个CheckBox,右边是一些文本信息。该列表还支持添加行。单击按钮(下图中的添加到顶部),将在列表顶部添加一行。界面如下所示(暂时忽略Reverse按钮??)。我这里写代码的时候,逻辑上是用一个数组作为这个列表的数据源,然后用map函数返回JSX来创建这个列表。由于React要求每个从数据动态创建的虚拟DOM都有一个键,因此使用数组的索引作为键是完美的。所以我在这里写了这段代码:list.map((item,index)=>{return({/*处理点击操作*/}}/>{item.text}
);});那么这里到底发生了什么问题呢?即当新建一行时,下面某行的CheckBox执行一个动画,每次用户点击CheckBox都要执行一次动画,使CheckBox的布尔值发生变化。语言有点难描述,我录了个画面展示一下:看到了吗,每点击一次按钮,倒数第二个执行一个动画。(图片其实看不出来,我们产品里面的动画效果比较明显。。。)如果实在看不懂是什么问题,可以看看antd控件库的CheckBox组件的源码。..(开个玩笑,不过我真的看了antd代码证明了)大背景提示:介绍基本概念,懂的可以跳过。要想弄清楚为什么会出现这个问题,还是得说说前端开发的一些背景。在我学习前端开发的时候,已经到了前端技术逐渐统一稳定,“数据驱动”和“虚拟DOM技术”得到广泛认可和应用的时代。简单解释一下这两个概念:数据驱动:是指开发者致力于处理数据,前端库根据数据渲染DOM节点。这种说法是相对于开发者直接操作DOM节点和浏览器事件而言的。在本文的例子中,我们关注的是数据(CheckBox的布尔值),CSS动画的渲染交给控件库来实现。基于虚拟DOM的渲染:简单的说,基于虚拟DOM的渲染是使用JavaScript对象来表示DOM节点,通过比较对象之间的差异来判断真实DOM节点的变化。这最终形成了一种“缓冲”机制,以避免浏览器在渲染管线中产生不必要的计算开销。在React和Vue中,算法根据虚拟DOM的键(在相似的DOM节点中)来识别它是哪个节点。查找原因为了让虚拟DOM节点可识别,React和Vue都要求开发者为动态生成的DOM节点设置key,否则会报这样的警告:Warning:E??achchildinalistshouldhaveaunique"key"prop。为了便于实现,开发者首先会考虑使用数组的索引作为key,但是数组的索引能否准确的识别出唯一的虚拟DOM节点呢?让我们用本文的例子来比较数组索引和虚拟DOM节点之间的关系。在上图中按下按钮前后的过程中,React的虚拟DOM比对算法认为发生了如下变化:key为0的节点的CheckBox值没有变化。(算法错误,这里其实加了“electricblue”这一行)key为1的节点的CheckBox值由false变为true,需要执行一个表示值变化的CSS动画(算法错误,"oceanofnoise"没有改变)添加了一个带有key2的新节点。(算法坏了,“sometext”这一行没变。)从这个角度看,用index做key,算法就晕了,搞不清谁是谁了。这是添加或删除数组时会出现的问题。如果数组没有变化(比如本文的例子中,产品本身不需要添加记录),其实不会出现这个问题。另外,数组排序也会出现这个问题,大家可以想象一下。(图中的Reverse按钮是我用来测试排序的)问题解决这个问题很容易解决,只需要用一个能正确识别数组元素的字段作为虚拟DOM的key即可。有时候,由于业务方便,前端开发者往往可以得到一些后端API提供的唯一id。如果没有,他们可以自己创建一个随机数作为id。下图是使用正确按键后的点击效果(没有flash动画):antd控件中List的实现看了一下antdList控件是如何处理按键问题的。从源码可以看出,List首先选择rowKey,如果没有,会尝试使用数据源中的key字段,如果没有,则使用基于索引的字符串。所以提供官方API支持的rowKey是最好的选择。参考资料本文例子源码中提到了这个问题的Vue教程(我是看了这个教程才注意到的,感谢这个教程)。这篇文章也发表在我的博客上,这里是原文链接