前端开发中,只要涉及到列表渲染,那么无论是Angular、React还是Vue框架,都会提示或要求每个列表项使用唯一的key,那很多开发者就会直接使用一个唯一的id或数组的index作为key的值,而并不知道使用key的原理。那么这篇博客就会讲解key的作用以及为什么最好不要使用index作为key的属性值。
key之所以那么重要,是因为React/Vue框架使用了虚拟DOM和Diff算法
高效率的更新视图。
虚拟DOM中key的作用
:(使用React来讲解,Vue同理)
1). 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用。
2). 详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,
随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(1).若虚拟DOM中内容没变, 直接使用之前的真实DOM
(2).若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM
b. 旧虚拟DOM中未找到与新虚拟DOM相同的key
根据数据创建新的真实DOM,随后渲染到到页面
需求:
有一个页面需要展示人员的信息,同时有个“添加”按钮,可以给列表添加一个人员,注意:是往列表前面追加一个人,即新添加的人展示在前面。
代码:
class Person extends React.Component{
state = {
persons:[
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
]
}
//触发“添加”按钮的回调函数
add = ()=>{
const {persons} = this.state
const p = {id:persons.length+1,name:'小王',age:20}
this.setState({persons:[p,...persons]})//新追加的小王p是处于persons的第一位(涉及到数组解构知识)
}
render(){
return (
<div>
<h2>展示人员信息</h2>
<button onClick={this.add}>添加一个小王</button>
<h3>使用id(数据的唯一标识)作为key</h3>
<ul>
{
this.state.persons.map((personObj)=>{
return <li key={personObj.id}>{personObj.name}---{personObj.age}</li>
})
}
</ul>
</div>
)
}
}
ReactDOM.render(<Person/>,document.getElementById('test'))
解释:我们应该知道React/Vue页面在数据发生改变时,是通过对比新、旧的虚拟DOM来更新页面展示的真实DOM的。
虚拟DOM就是一个普通的JS对象,下面我们就用一个对象来表示虚拟DOM
。
Diff算法的对话:(新、旧分别代表新旧的虚拟DOM)
新:你那里有个key为3的东西吗?
旧:没有这玩意。
新:好吧,那我就新建一个key为3,内容为“小王- - - 20”的真实DOM
。
新:那你有个key为1的东西吗?
旧:有啊。
新:那它的内容是“小张- - - 18”吗?
旧:是的。
新:那我就不用创建新的真实DOM了,复用
上次创建的那个真实DOM就可以。
新:那你有个key为2的东西吗?
旧:有啊。
新:那它的内容是“小李- - - 19”吗?
旧:是的。
新:那我也复用
上次创建的真实DOM就行。这次工作真轻松,只需新建一个真实DOM,如果没有key和Diff算法这玩意,还使用原生JS的更新视图,那我就需要重新创建三个真实DOM了
。
综上对话,key是使得Diff算法高效对比新旧虚拟DOM的差异进而高效更新视图的重要一环。
用index作为key可能会引发的问题:
1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
2. 如果结构中还包含输入类的DOM:
会产生错误DOM更新 ==> 界面有问题。
3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,
仅用于渲染列表用于展示,使用index作为key是没有问题的。
使用index作为key的属性值,在页面效果上没有任何差异,有差异的是效率
.
还是上面的代码:
class Person extends React.Component {
state = {
persons: [
{ id: 1, name: '小张', age: 18 },
{ id: 2, name: '小李', age: 19 },
],
}
add = () => {
const { persons } = this.state
const p = { id: persons.length + 1, name: '小王', age: 20 }
this.setState({ persons: [p, ...persons] })
}
render() {
return (
<div>
<h2>展示人员信息</h2>
<button onClick={this.add}>添加一个小王</button>
<h3>使用index(索引值)作为key</h3>
<ul>
{this.state.persons.map((personObj, index) => {
return (
<li key={index}>
{personObj.name}---{personObj.age}
</li>
)
})}
</ul>
</div>
)
}
}
ReactDOM.render(<Person />, document.getElementById('test'))
注意对比使用index作为key的属性值,生成的虚拟DOM的差别。
Diff算法的对话:
新:你那里有个key为0的东西吗?
旧:有啊。
新:那它的内容是“小王- - -20”吗?
旧:不是。
新:那内容改变了,我需要生成一个新的真实DOM替换上次展示在页面的真实DOM
.
新:你那里有个key为1的东西吗?
旧:有啊。
新:那它的内容是“小张- - -18”吗?
旧:不是。
新:那内容改变了,我又要创建一个新的真实DOM去替换旧的DOM
.
新:你那里有个key为2的东西吗?
旧:没有。
新:那我就新创建一个真实DOM咯,更新页面视图。害,这次我竟然要创建三个真实DOM,上次创建的真实DOM竟然一个都不能复用,累死我了
。
这就是使用index作为key的属性值的存在的副作用,但这种副作用不存在只向数组末尾追加数据的情况下。
比如向数组末尾追加数据展示:
Diff算法会发现key为0和1对应的真实DOM是可以复用的
,因此也是只需创建最后新加数据的一个真实DOM。
因此当新数据不是在末尾追加进数组,那么key就不要使用index降低效率了。