前言
目前我搜索到关于react diff的文章有两个问题:
1.不少文章直接讲diff算法,甚至还会和Vue2/Vue3的diff算法做对比。这样的角度太学术了,缺少从业务实际角度对diff可能造成的问题的阐述。
2.从实际业务角度的文章又比较老旧,都是class组件时代的,目前大家都在使用hooks了,但是缺少对diff会对函数式组件造成影响的阐述。
Why(为什么要了解react的diff?)
不了解diff的机制,随便乱设key值,你会遇到以下问题:
1.页面更新时的性能差,甚至出现明显的页面闪动
2.子组件的useEffect钩子的行为与预期不符(希望重新初始化但没有,或者希望不要重新初始化但是有)
3.组件的自身的状态没有被清除,甚至被转移到其他组件
这些都是我在实际项目开发中真实遇到的问题,对业务造成了不同程度的影响。
What(我们要回答什么问题?)
1.为什么不能用index作为key,会导致什么问题?
2.如果用item的某一属性的值作为key有什么问题?
(1)在列表中未必是唯一的。
(2)即使列表中唯一,也未必能保证行为符合预期
3.用idx++等方式每次生成绝对唯一的key有什么问题?
这样每次都会重新执行生命周期,上次的状态不会保存,如果说组件用于渲染的数据是从后端获取,那么就可能造成页面闪动(因为先重新执行生命周期,这时渲染内容还没取到所以为空,然后取到后端数据,获取到渲染内容,经历了上次渲染有→第二次取到前无→第二次取到后有的历程,所以会有页面闪动)。
4.key在react+函数式组件下的语义是什么?
key是函数式组件的唯一标识,它决定了React在多次rerender列表的时候把函数组件的状态重置还是沿用上次的状态。
比如首次渲染中key为1的函数式组件在列表中,那么下次渲染列表中还存在key为1的函数式组件的话,React就会沿用这个函数式组件并且把上次渲染的状态传给他并进行一次组件rerender。
如果key都是新的,那么就会完全重新渲染函数式组件,重新执行新的生命周期。
5.如果说是受控组件是不是就没有这些问题了?
是的,纯受控函数式组件的key值是什么都不会出问题。只不过React可不会去识别你是不是受控组件,乱设key的话一样会触发警告。
6.所以key值到底如何设才是最佳实践呢?
不急,我们先通过一些例子验证下我们上面的结论,再来得出最佳实践。
Examples
用key作为index会对函数式组件造成的影响
代码见examples/0 media.giphy.com/media/C9v3i…
行为:
1.上次渲染的状态跑到下次渲染的同key组件中来
2.新的组件2的生命周期没有重新开始
3.上次渲染就已经存在的组件1重新执行了生命周期,状态丢失
分析:
(1)第一次,组件0的key为0。第二次,组件2的key为0。所以第一次渲染中的组件0的状态跑到了组件2中。
(2)第一次渲染中的组件1的key为1,第二次渲染中组件0的key为1,所以状态跑到了第二次渲染的组件0中,并且第二次渲染的组件0没有重新走生命周期。
(3)第二次渲染中组件1的key为2,是全新的key,所以第二次渲染中组件1重新走生命周期了。
同样的例子,用永远唯一key的行为
代码见examples/1
行为:
(1) 所有的状态都没有保留
(2)所有组件每次渲染都重新执行生命周期
用永远唯一的key会存在的页面闪动问题
代码见examples/2
用组件唯一标识来避免页面闪动
代码见代码见examples/3
这个例子中我给每个组件在列表项中都加上了id标识,并用id标识作为key,从而避免了页面闪动。
结论
用什么作为key是要分情况考虑的。
1.是受控组件,没有状态这么一说,那么组件的key是什么都无所谓,只会遇到警告,不会真正导致问题。
2.要请求接口来获取数据的组件,那么一定要有唯一标识作为key让React知道两次渲染的是同一个组件,这样才能避免闪动。
(1)列表长度永远固定,不会增删,那么可以用index作为key。
(2)列表长度不固定,那么就要用列表项中能标识组件的属性作为key,如果没有,你甚至要自己造一个。
3.有些重渲染希望不保留组件状态,有些重渲染希望保留组件状态。
这个比较复杂, 是我实际开发中遇到的case,我们的业务是要渲染富文本列表,列表项可能有音频、文字、视频、图片等。而音频组件是有状态的。这些东西组成了一道题的题干。
当我们执行切到下一题的时候,希望能不保留音频状态重新开始。而当点击其他按钮,希望能保留音频状态。
这种情况下显然是不能用idx++这种方案,不然每次任何重渲染列表的时候key都变了,音频组件的状态就全都没了。
我们需要要找一个切题才变的变量即questionId,但是不能直接用questionId作为key,因为每个列表项是要不同的嘛,所以就questionId+index作为组件的key。
这样就保证了每次都切题,所有组件都重新执行生命周期。而一道题内的重渲染会把同样的状态给到同key组件重渲染,不会触发dom操作,也不会丢失状态。