上节课我们对新版生命周期做了一个阶段性的总结。这节课我们不继续往下进行,我们先来插一个话题,关于我们之前提起过的,react
的diffing
算法。
回顾
理论上我们在开始新的一课的内容的时候都是要先对上一节课的内容来做一个简单的回顾,但是上一节其实没有什么可回顾的内容,简单来说新版生命周期和旧版生命周期之间核心部分都没用什么区别,而被废弃以及新增的钩子都不常用,只要了解就好,大家感兴趣的可以往深处去研究一下。接下来我们就开始介绍一下diffing
算法。
概述
我们在开篇的时候就说过react
使用了diffing
算法。而且我们也说了react
最大的优势就是,react
并不是每次更新都对页面上所有的真实DOM
都做了修改,而是,每个真实DOM
都对应了一个虚拟DOM
,然后每次更新时都会将两次的虚拟DOM
进行比对,修改的是不同的虚拟DOM
。
那么我们首先是不是应该验证一下这个算法到底是不是存在啊?然后我们之前在遍历数组渲染的时候也说了,要给每条虚拟DOM
都要加一个唯一的key
,是供diffing
算法使用的,那么这个key
到底是干什么用的?那么这节课我们就会就这两个方面来介绍一下。
验证diffing
算法
我们既然要验证diffing
算法,那么我们要怎么验证?是不是要更新页面,不更新页面我们也不会比对两次的虚拟DOM
啊,只要更新页面,那就要用到state
,那么我们来看这个案例
class Time extends React.Component { state = { date: new Date() }; componentDidMount() { setInterval( () => { this.setState({ date: new Date() }) }, 1000 ); } render() { return ( <div> <h2>Hello</h2> <input type="text" /> <span>Now is: {this.state.date.toTimeString()}</span> </div> ); } }
非常简短的一个小案例,我们先是定义组件。初始化了一个state
。然后我们先来看一下我们的render
方法吧,这个render
方法想要渲染一个什么样的页面?一个标题,一个输入框,以及一段文字,这个文字中是从state
中取到的当期日期和时间,然后转成了字符串形式来展示在页面上。
然后我们再来看,我们写了componentDidMount
钩子。在这个钩子里面我们开启了一个循环定时器,接收我们的执行器函数,和定时参数,在执行器函数中我们修改了state
,具体操作是取当前最新的Date
对象并赋给state
中的date
属性。而这个定时器的定时参数是1000ms
,这又是什么意思呢?是不是代表着每隔一秒就取最新的时间赋给state
?
那么我们的代码就很明确了,我们要在页面上展示一个标题,一个输入框,和一个时间字符串,而且这个时间字符串是每秒更新一次的。
像这样,实际效果中这边的时间是一直在更新的。那么我跟大家说,现在这个diffing
算法其实就正在起作用。我们再回到代码中,我们的state
是不是每秒都在变?state
一变,是不是就要驱动页面更新?是不是就要调render
方法?一旦重新调用了render
方法,就会获取到一堆新的虚拟DOM
,按照我们说的diffing
算法,这时候就要拿新的虚拟DOM
和之前原有的虚拟DOM
来做比对。
在完成比对之后发现<h2>
和<input>
还是原来的那写节点,所以在页面展示上并没有发生变化,唯一变化的是<span>
。diffing
算法也没有那么智能发现<span>
中是我们从state
中取的时间变了,但是diffing
算法可以发现,这个标签已经不是原来的那个标签了。它比对的最小单位是以标签为单位的。
所以diffing
算法并不是说发现了<span>
中前面几个词没有变只是后面时间在变,而是直接把整个<span>
都给替换掉了。而其他没有发生改变的标签就直接还是使用原来的,不做任何改变。
可能有人要说了,你说了这么半天,就一个劲儿说diffing
算法做了什么。但是你怎么证明diffing
算法真的存在,而且还真的就这么做了呢?
我们来看一下,页面上是不是有一个输入框?如果说diffing
算法不存在,那么每秒更新一次页面,新的页面是不是就会生成新的输入框?那么我们输入框的内容应该会在下一次更新之后被直接清空才对吧?那么我们来看一下我们输入之后在更新之后会不会被清空。
而实际上我们输入的并没有被清空,所以可以见得我们的输入框这个DOM
在更新的过程中并没有被更新。
所以说我们得到一个结论就是diffing
算法是存在的。那么我们再来想一个问题,如果我们在<span>
里面再来写一个<input>
,然后输入点东西。那么在更新之后,我们输入的东西会被清空吗?我们来看一下:
为什么这个也没有被清空呢?我们说了diffing
算法的最小比对单位是标签,而且也说了我们是span
标签被整体替换为什么这里的input
标签海水面没有被更新呢?我们说了diffing
算法比对的最小单位是标签,这里的input
标签在span
标签里面那也依然还是标签啊。这也是要参与比对的。
key
那么我们就这样来验证了我的所说的diffing
算法是否真的存在。那么我们真正的重点是在我们之前提到过的key
,这个key
我们曾经简单地介绍过是给diffing
算法用的,但是到底是干什么用的呢?这个才是我们这节课的重点也是核心部分。
我们来看一个案例:
class Person extends React.Component { state = { persons: [ { id: 1, name: "jingxun", age: 18 }, { id: 2, name: "jingxun1", age: 19 }, ] } render() { return ( <ul> {this.state.persons.map((p, idx) => <li key={idx}>{p.name},{p.age}</li>)} </ul> ); } }
首先我们定义了一个组件,初始化了state
,state
中persons
属性是一个数组,里面都是对象。然后我们在render
方法中渲染展示name
和age
。那么我们来看一下效果:
展示方面来说是没有任何问题的。那么我现在想做一个需求,在这边有一个按钮添加一个新的人比如叫jingxun2
,age
是 20,那么我们怎么处理?
class Person extends React.Component { state = { persons: [ { id: 1, name: "jingxun", age: 18 }, { id: 2, name: "jingxun1", age: 19 }, ] } render() { return ( <div> <h2>展示个人信息</h2> <button onClick={this.add}>Add new</button> <ul> {this.state.persons.map((p, idx) => <li key={idx}>{p.name},{p.age}</li>)} </ul> </div> ); } add = () => { const {persons} = this.state; const p = {id:persons.length+1,name:"jingxun2", age:20}; this.setState({persons:[p,...persons]}); } }
我们来看一下上面这段代码,展示情况很明显
但是我点击了按钮之后的效果是什么呢?我们先来看一下我们的onClick
事件回调中写的都是什么东西。首先获取原有的satte
中persons
属性,然后定义一个新的元素和persons
数组中的结构一致。而且id
我按照常规来说都是自增长的,所以我们就通过数组的长度来模拟自增长。然后更新state
,我们用展开运算符来拼接数组。那么当我们点击了按钮之后的效果究竟会怎么样呢?
成功添加了这是不是还挺好的?功能也实现了。但是这样做得话会有一个很严重的效率问题。怎么说呢?我们从两道面试官很爱问的面试题上来说。
key
的作用是什么
在大家面试中面试官其实是很喜欢问一些底层原理的东西的,比如react/vue
方面,面试官都会比较爱问这个key
的作用是什么呢?基本原理是什么呢?
简单地说key
是虚拟DOM
对象的标识,在更新显示时key
起着机器重要的作用。但是这么说实在也太笼统了,详细来解释吧:
当state
中数据发生变化的时候,react
会根据新数据生成新的虚拟DOM
,随后react
进行新虚拟DOM
和旧虚拟DOM
的diff
比对。这里diff
比对的规则如下:
- 旧虚拟
DOM
中找到了与新虚拟DOM
相同的key
- 如果虚拟
DOM
中内容没变,直接使用之前的真实DOM
- 如果虚拟
DOM
中的内容变了,则生成新的真实DOM
,随后替换掉原页面的真实DOM
- 旧虚拟
DOM
中未找到与新虚拟DOM
相同的key
- 根据数据创建新的真实
DOM
,随后渲染到页面上。
好我们现在知道了key
的作用是什么。我们来从代码层面来看一下整个流程。
还是上面那个案例,我们数据是不是在state
中?当我们初次挂载组件的时候在数据层面是不是就两条数据?这两条数据对应的是不是就有两个虚拟DOM
?
- 初次挂载数据
{ id: 1, name: "jingxun", age: 18 }
{ id: 2, name: "jingxun1", age: 19 }
- 初次挂载的虚拟
DOM
<li key=0>jingxun,19</li>
<li key=1>jingxun1,19</li>
当我们更新之后,更新后的数据是什么?是不是state
里面多了一条,那么对应的虚拟DOM
也应该有 3 个
- 更新后的数据
{ id: 3, name: "jingxun2", age: 20 }
{ id: 1, name: "jingxun", age: 18 }
{ id: 2, name: "jingxun1", age: 19 }
- 更新后的虚拟
DOM
<li key=0>jingxun2,20</li>
<li key=1>jingxun,18</li>
<li key=1>jingxun1,19</li>
那么我们来注意一个地方,我们在更新state
的时候是不是把我们新增的数据放在最前面了?而且我们的key
是不是用的index
?那么我们的新的虚拟DOM
是不是就想上面列表一样?那么在做diff
比对的时候会发生什么事?
按照比对规则,找到相同的key
做比较,没找到相同的key
的直接生成真实DOM
那么这么一对比,是不是所有虚拟DOM
都要更新?我们这个情况下一个旧的虚拟DOM
都没能成功复用下来啊。所以我们刚才说的那种方法虽然功能已经实现了,但是存在严重的效率问题。
key
与index
另外一个问题,就是我们之前说的,我们之前遍历列表,key
用的都是index
,我们也说了用index
会有问题,当然面试官也会问一句,为什么遍历列表时,key
最好不要用index
?那么为什么呢?
- 若对数据进行逆序添加或逆序删除等改变破坏顺序的操作,会产生没有必要的真实
DOM
- 如果结构中包含输入类的
DOM
会产生错误DOM
更新 - 如果没有上述所说的情况,仅作为一个展示的话用
index
作为key
是没有问题的
这些规则第一条看着是不是已经明白了,刚才那种导致一更新,在进行diff
比对时因为顺序乱了导致key
也乱了,使得本来应该复用的虚拟DOM
都没有被复用。这样的话就导致了产生了一些没有必要的真实DOM
,这样虽然页面没问题,但是会拖慢效率的。
可能有些人说,那我不管,能跑就行。我管效率干什么。那么好来看第二条,大家是不是觉得没明白?我们来改一下代码:
class Person extends React.Component { ... render() { return ( <div> <h2>展示个人信息</h2> <button onClick={this.add}>Add new</button> <ul> { this.state.persons.map( (p, idx) => <li key={idx}>{p.name},{p.age}<input type="text"/></li> ) } </ul> </div> ); } ... }
什么意思?是不是在没条数据列表后面都加了input
输入框?我们来看一下结果:
我们在每个input
框中输入对应的信息,那么来看一下我们点击了按钮之后会变成什么样子:
这是为什么?我们正常来说是不是应该新增的这一条的input
框里面是空的才对?但是现在不对啊。还是刚才的那一套
- 初次挂载数据
{ id: 1, name: "jingxun", age: 18 }
{ id: 2, name: "jingxun1", age: 19 }
- 初次挂载的虚拟
DOM
<li key=0>jingxun,19<input type="text"/></li>
<li key=1>jingxun1,19<input type="text"/></li>
当我们更新之后,更新后的数据是什么?是不是state
里面多了一条,那么对应的虚拟DOM
也应该有 3 个
- 更新后的数据
{ id: 3, name: "jingxun2", age: 20 }
{ id: 1, name: "jingxun", age: 18 }
{ id: 2, name: "jingxun1", age: 19 }
- 更新后的虚拟
DOM
<li key=0>jingxun2,20<input type="text"/></li>
<li key=1>jingxun,18<input type="text"/></li>
<li key=1>jingxun1,19<input type="text"/></li>
是不是这样?我们是不是将虚拟DOM
转成真实DOM
才能在输入框中输入值?那么虚拟DOM
中是不是根本没有value
属性啊?那么在进行diff
比对的时候,按照算法比对规则这些input
标签是不是根本就没有改变过?,所以在更新的时候是不是就生成一个新的input
标签就行了?那么这样的话页面渲染就出问题了啊。你说你能跑就行,这么明显的一个bug
你可是跑不了的啊。所以这就是使用index
作为key
可能会引发的问题。
如何来选择key
- 最好选择每条数据的唯一性标识
- 如果确定仅作为简单的展示,那么可以使用
index
总结
diffing
算法会来对比新旧虚拟DOM
diffing
算法通过key
来比对新旧真实DOM
- 尽量使用数据唯一标识来做
key
Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.
Link of this article:https://work.lynchow.com/article/diffing_of_react/