diffing算法
虚拟dom
我们知道,react里面操作的都是虚拟dom,最后经过render渲染为真正的dom,那么为什么要提出虚拟dom这个概念呢?其实就是将逻辑和视图区分开,react的虚拟dom,就相当于mvc的c,将数据逻辑和真正的dom区分开,我们知道,对于前端来说dom操作是非常昂贵的,性能消耗最大的就是dom操作。而virtual dom减少了对dom的操作,,不仅避免了资源浪费,而且页面的构建也得到了很大的提升。
为什么要diff
前面我们说了,应避免过多的操作dom,那么diff就是解决了这种问题。我们知道,react在状态发生变化的时候,会批量更新dom,生成新的UI,但是难道state的某一个值发生变化就要导致整个dom重新渲染吗?这明显是不科学的,为了解决这种问题,react提出了diff算法,通过对比新旧的两个虚拟dom树来检测那些dom是真的需要重新如安然,哪些dom是没有变化的。
diff算法
传统的diff算法是通过循环递归的方式对节点一次进行对比,需要O(n ^3)的时间复杂度。为什么?我们从最简单的说,把一棵树转换为另外一棵树,其实就是把一颗n个节点的树挨个儿去另一棵n个节点的树中查找,整个过程是n*n,那么如果发现有不同的地方,我们就需要需改,对一棵树的增删改的算法复杂度是n,那么整个过程就是n*n*n
react将这种对比策略做了一个优化,将复杂度降到了O(n),那么react是怎么实现的呢?react的diff会预设三个限制
- 只进行同层级比较
- 新旧节点的type不同,直接删除旧节点,创建新节点,比如组件不同,元素的类型不同,原来是ul,里面是li,后来改成了div+p,这个时候就会删除旧dom,创建新的dom.
- 通过key来复用节点
在上面三个限制的基础上,对tree,conponent,element的处理方式又做了优化
- dom节点不一致直接删除旧节点,创建新节点
- 组件类型不一致直接删除组件下所有节点,创建新节点
- 同一层的dom元素,以每个元素对应的key为标识,提供三种操作方式,删除新建和移动
diff的最小颗粒度
diff的最小颗粒度是标签,我们举例看一下
js
class Hello extends React.Component {
state={
time:new Date()
}
componentDidMount(){
this.timer=setInterval(()=>{
this.setState({
time:new Date()
})
},5000)
}
render() {
console.log("i am render")
return (
<ul>
<li>备注:<input type="text"/></li>
<li><span>{this.state.time.toTimeString()}</span></li>
</ul>
)
}
}
我们看到,每隔5秒,span标签就会从新刷新一次,而其他元素没有变化
key
前面我们说了,同一层级的元素如果发生了变化,可以删除 插入和移动,前提是必须有key作为标识,如果没有key,比如下面代码
js
class Hello extends React.Component {
state = {
heros: [
{
id: 1,
name: "张三"
},
{
id: 2,
name: "李四"
}
]
}
render() {
console.log("i am render")
return (
<ul>
{this.state.heros.map((hero,index)=>{
return <li>{hero.name}</li>
})}
</ul>
)
}
}
这个时候浏览器提示了一个warm ,意思是每一个child都应该有唯一key。
那么这个key应该怎么选取呢
- 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
- 如果确定只是简单的展示数据,用index也是可以的。 但是不推荐。
为什么不推荐index作为key,官方文档说,如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。
我们来举个例子对比一下,再分析问题所在。这段代码里,我点击添加按钮的时候,改变了原来数组对象的顺序,在数组的前头添加了一个对象,我们点击看一下效果
js
class Hello extends React.Component {
state = {
heros: [
{
id: 1,
name: "张三"
},
{
id: 2,
name: "李四"
}
]
}
addHero=()=>{
this.setState({
heros: [
{
id: 3,
name: "王五"
},
...this.state.heros
]
})
}
render() {
console.log("i am render")
return (
<ul>
<button onClick={this.addHero}>点击添加王五</button>
<h1>index as key</h1>
{this.state.heros.map((hero,index)=>{
return <li key={index}>{hero.name}<input type="text" /></li>
})}
<h1>age as key</h1>
{this.state.heros.map((hero,index)=>{
return <li key={hero.id}>{hero.name}<input type="text" /></li>
})}
</ul>
)
}
}
我们发现了什么,以index做为key,input框并没有跟着往下移动,id作为key的往下移动了,我们发现问题了,那么为什么index作为key,input框不往下移动呢?就是因为数据的顺序发生变化了,王五被塞进了数组对象的头部,但是diff的时候,由于元素的key是index,还是从0开始的,并没有发生变化,所以input框并不会移动。