起因
React 的 key 相信大家都很了解,也不用我多说。但是平时对于key的使用可能没有那么严格,有可能不给 key ,有可能给 index ,其实一般来说不会出什么问题,顶多就是性能上会有一些损失,但是在某些特定的情况下使用不当也可能会导致 bug ,比如下面这种情况。
key导致的bug
在一个后台管理系统中,左侧是一个菜单可以选择不同的选项,右侧对应了不同的视频。但是左侧菜单切换的时候,右侧视频的封面图虽然重新加载了,点击播放后视频的内容并不是新的,还是上一个选项卡的视频,看起来就像没有重新渲染一样。而排查了代码之后发现我确实没有给 table 去加 key 值,而加上这个 key 值之后也确实是好了。我们来看看 video 那段代码。
<video controls={true} poster={poster}>
<source src={videoUrl} />
</video>
那么问题来了:
- 如果我不加 key,react 不是应该把各个list理解为完全不一样而重新渲染吗?
- video 里面的 src 确实已经变了,但是播放的视频还是老视频
一顿搜索之后,我先找到了第二个问题的答案:
video 用这种 source 的写法时,如果只改变 src 的话,video 是不会重新去加载视频的,正确的做法应该是将 dom 卸载掉再重新加载,这样才能正确拉取到对应的视频。
虽然我感觉这很不符合常识,但结果就是这样的,在不加 key 的情况下,去掉 source 直接在 video 中使用 src 也能解决这个 bug。像下面这样
<video controls={
true} poster={
poster} src={
videoUrl} />
那么我们回到第一个问题,我没有给key值,React居然不会把他们都认为是不一样的dom去重新渲染?
diff算法
key 值有什么用呢?在 react 的 diff 过程中,如果遇见 key 相同的两个结点,react 会认为这是两个相同的结点,在下一次渲染中会复用这个结点,减少渲染的内容,从而提升性能。那,react 的 diff 算法又是什么呢?
传统的diff算法
我没有仔细去研究过完全找出两颗树的改动之处最小的时间复杂度是多少,根据网上的信息来看,目前最小的时间复杂度也到了 O ( n 3 ) O(n^3) O(n3),而React目前的diff算法时间复杂度为 O ( n ) O(n) O(n),他们两个都不在一个量级上,所以 react 的 diff 势必是丢掉了一些东西的。
React的diff算法
为了降低算法复杂度,React 针对前端开发的习惯做了一些限制:
- React 只会对同级元素 diff,如果一个 dom 结点跨越了层级,那么它是永远不可能被复用的。
- 如果 dom 结点的类型发生了改变,这个结点以及其后代都会被销毁然后重新渲染
- react 可以通过 key 值来判断哪些结点属于同一个结点
所以通过这三条我们可以总结出要想让我们的页面少一些不必要的渲染,我们可以: - 不要轻易改变 dom 层级
- 不要轻易改变 dom 类型
- 利用好 key
diff实现
说了这么多,那么我们就一起去看看 react 到底是怎么实现 diff 的,而 key 在这其中又发挥了怎样的作用
入口函数
diff的入口函数叫 r e c o n c i l e C h i l d F i b e r s reconcileChildFibers reconcileChildFibers ,在这之前经历了一系列的调度操作,最后来到了diff环节,那在 r e c o n c i l e C h i l d F i b e r s reconcileChildFibers reconcileChildFibers中 react 又干了什么呢
// returnFiber是我们最后将要渲染的Fiber树
// currentFirstChild是当前的第一个子child
// newChild是将要挂载的所有的子元素
// lanes用于优先级判断
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
// 首先判断newChild是否是Fragment
var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
// 如果是Fragment则把里面的内容拿出来
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// 判断newChild是不是对象
var isObject = typeof newChild === 'object' && newChild !== null;
// 如果是对象则根据不同的类型去处理
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
case REACT_PORTAL_TYPE:
return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes));
}
}
// 判断是否是文本类型
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes));
}
// 判断是否是数组
if (isArray$1(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
// 判断是否可迭代
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes);
}
// 后面都是一些错误处理
if (isObject) {
throwOnInvalidObjectType(returnFiber, newChild);
}
{
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
// If the new child is undefined, and the return fiber is a composite
// component, throw an error. If Fiber return types are disabled,
// we already threw above.
switch (returnFiber.tag) {
case ClassComponent:
{
{
var instance = returnFiber.stateNode;
if (instance.render._isMockFunction) {
// We allow auto-mocks to proceed as if they're returning null.
break