[铁人赛 Day07] 为何不该使用 index 当作 Key 值 ?——React render 更新机制解释

前言

你可能听过以下这个错误案例(或者说 anti-pattern 的案例):在一个会不断新增、排序、删除的列表上,使用 index 作为 key 值。但你知道这麽做,究竟会造成什麽问题吗?

todos.map((todo, index) => (
		// 只要有给 key 值就不会报错了,那问题究竟在哪?
    <Todo {...todo} key={index} />
  ));
}

此篇文章会介绍 React 的 reconciliation algorithm,也就是在每一次的 render 之间,React 如何使用最有效率的方式去更新?如何去比较状态更新之间的差异?写 code 的时候要怎麽避免效能的浪费呢?

演算法基於两个规则

使用 React 的时候,你可以把 render() 想像成一个建立出 React element tree 的方法,当 props 或者 state 更新之後,render() 会 return 一个新的 element tree。在这个过程中,React 面临的议题是要有效率的去更新画面,达到目前的 element tree 的样子。

React 使用了启发式演算法(heuristic algorithm) ,这套演算法是基於两个假定的规则,而後来也在实践中验证了这套规则的可行性,是适用於大多数的 use cases 的。

规则一:在 render 更新的过程中,两个不同 type 的元素会制造出相异的 element tree.

规则二:开发者会设定 key 值来暗示哪些子元素在 renders 之间是维持稳定不会改变的。

规则一的运作方式:不同的 type

比较两个 element tree 的时候,React 会先去比较他们的根元素,依照 type 的相同或不同,接下来的行为也会有所不同。

何谓「有不同的 type」?例如 <a><img> 不同、<Article> 与 <Comment> 不同, <Button> 与 <div> 不同。

当两个根元素有不同的 type 的时候,React 会把旧的 element tree 销毁,然後建立新的。在销毁旧的 element tree 的时候,连带旧的 DOM nodes 也会被销毁,Component instances 内会接受到 componentWillUnmount() ,建立起新的 element tree 的时候,新的 DOM node 也会被插入原先的 DOM 中,Component instances 内则会接受到 componentDidMount() ,同时,跟旧的 element tree 相关的 state 也会一起遗失。

销毁的时候,所有在该元件底下的 root 都会一起 unmounted,例如:

// 旧的 element tree
<div><Counter /></div>
// 新的 element tree
<span><Counter /></span>

//// 在这个更新过程中,旧 <Counter> 也会一起被销毁,并且 remount 一个新的。

那相同的 type 会发生什麽事情?

如果两个根元素有相同的 type 呢?React 会去查看两者的属性,相同的会被留起来,而只会更新那些不同的属性。例如:

// 旧的 element tree
<div className="before" title="stuff" />
// 新的 element tree
<div className="after" title="stuff" />

//// 在这个更新过程中,在底下的 DOM node 上,React 只会去更改 className 

那如果是 style 呢?

// 旧的 element tree
<div style={{color: 'red', fontWeight: 'bold'}} />
// 新的 element tree
<div style={{color: 'green', fontWeight: 'bold'}} />

//// 更新过程中,React 只会去更新 coloer,而不会去更新 fontWeight

处理完 DOM node 之後,接着是处理针对 children 的 recurses(递回)。

DOM node 之後,处理 recurses

当元件更新的时候, instance 会维持一样,所以 state 可以被在 renders 之间保留起来。

但为了要与新的 element 一致,React 要更新底下元件 instance 的 props,在这个过程中 componentWillReceiveProps()componentWillUpdate()componentDidUpdate() 会在 instance 中被呼叫。接着,render() 方法会被呼叫,辨别差异的演算法(diff algorithm)会在前一个结果、以及後一个结果之间 recurses(递回)。

听起来可能有点抽象,让我们直接看案例吧。

// 从原本的 list
<ul>
	<li>first</li>
	<li>second</li>
</ul>

// 新增一个内容,变成新的 list
<ul>
	<li>first</li>
	<li>second</li>
	<li>third</li>
</ul>

React 会去对照,然後发现两者的 <li>first</li><li>second</li> trees 对起来了,只有第三个 <li>second</li> 没对到,然後就会去嵌入 <li>second</li> 这个 tree。

然而如果你加入新的 li 的方法,是从最前方加入...

// 从原本的 list
<ul>
	<li>first</li>
	<li>second</li>
</ul>

// 新增一个内容,变成新的 list
<ul>
	// 如果是加在前面呢?
	<li>third</li>
	<li>first</li>
	<li>second</li>
</ul>

React 就会不知道可以保留 <li>first</li><li>second</li> ,而是需要去 mutate 每一个 child,这种方式会很没效率,所以才有接下来的 Key 值。

规则二的运作方式:用 key 值来做辨识

当这些 children 带有 key 值的时候,React 就会用 key 值作为辨识,来辨别是否有新的 child 被加入或删除,举例来说:

// 从原本的 list
<ul>
	<li key="2015">Duke</li>
	<li key="2016">Villanova</li>
</ul>

// 新增一个内容,变成新的 list
<ul>
	// 并且是把新的元素加在前面
	<li key="2014">Connecticut</li>
	<li key="2015">Duke</li>
	<li key="2016">Villanova</li>
</ul>

在有 key 值的情况下,React 会知道拥有 2015 跟 2016 的 key 值的元素移动了,而 2014 key 值的元素是新的。所以独特的 key 值会是重要的,而这个「独特」只需要在他的同一层元素(siblings)中是不重复的就可以了,不需要是全域性的独特。

因为上述类型的 component instances 的更新根据是 key,如果你使用 index 作为 key,而 list 物件又是会被删减、被改变顺序的,那就代表你的 key 值会不断的改变,造成 uncontrolled inputs 这类东西的元件状态被搞混、并且以预期之外的方式更新。

也不要使用不稳定的 key 值,例如 Math.random() ,容易让你的 component instances 跟 DOM nodes 非必要的被重复建立。

https://reactjs.org/docs/reconciliation.html

https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318


<<:  [DAY14]Label进阶使用-Affinity and anti-affinity

>>:  简单的先做 VS 技术难题先做

爬虫怎麽爬 从零开始的爬虫自学 DAY17 python爬虫所需套件

前言 各位早安,书接上回我们简单介绍过 html 基本架构了,从今天开始要正式开始实作爬虫程序的部分...

DAY 21 新增查询与删除团购讯息

管理讯息的功能有 新增团购讯息 删除团购讯息 查询团购讯息 手动新增团购者 手动删除团购者 新增团购...

#30 No-code 之旅 — 恭喜完赛!

最後一天!礼拜五快乐!恭喜大家完赛!恭喜自己XD 今天来回头看看我们这三十天学了哪些事,还有讲一下未...

二元树左到右查找 - DAY 16

前序检查(preorder) 中序检查(inorder) 後序检查(postorder) 後序检查来...

【Day14】数据展示元件 - Card

元件介绍 Card 是一个可以显示单个主题内容及操作的元件,通常这个主题内容包含图片、标题、描述或是...