|
|
@ -6,3 +6,249 @@ |
|
|
|
|
|
|
|
理论上我们在开始新的一课的内容的时候都是要先对上一节课的内容来做一个简单的回顾,但是上一节其实没有什么可回顾的内容,简单来说新版生命周期和旧版生命周期之间核心部分都没用什么区别,而被废弃以及新增的钩子都不常用,只要了解就好,大家感兴趣的可以往深处去研究一下。接下来我们就开始介绍一下`diffing`算法。 |
|
|
|
|
|
|
|
## 概述 |
|
|
|
|
|
|
|
我们在开篇的时候就说过`react`使用了`diffing`算法。而且我们也说了`react`最大的优势就是,`react`并不是每次更新都对页面上所有的真实`DOM`都做了修改,而是,每个真实`DOM`都对应了一个虚拟`DOM`,然后每次更新时都会将两次的虚拟`DOM`进行比对,修改的是不同的虚拟`DOM`。 |
|
|
|
|
|
|
|
那么我们首先是不是应该验证一下这个算法到底是不是存在啊?然后我们之前在遍历数组渲染的时候也说了,要给每条虚拟`DOM`都要加一个唯一的`key`,是供`diffing`算法使用的,那么这个`key`到底是干什么用的?那么这节课我们就会就这两个方面来介绍一下。 |
|
|
|
|
|
|
|
## 验证`diffing`算法 |
|
|
|
|
|
|
|
我们既然要验证`diffing`算法,那么我们要怎么验证?是不是要更新页面,不更新页面我们也不会比对两次的虚拟`DOM`啊,只要更新页面,那就要用到`state`,那么我们来看这个案例 |
|
|
|
|
|
|
|
```jsx |
|
|
|
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`? |
|
|
|
|
|
|
|
那么我们的代码就很明确了,我们要在页面上展示一个标题,一个输入框,和一个时间字符串,而且这个时间字符串是每秒更新一次的。 |
|
|
|
|
|
|
|
![image-20220105133634223](https://file.lynchow.com/image-20220105133634223.png) |
|
|
|
|
|
|
|
像这样,实际效果中这边的时间是一直在更新的。那么我跟大家说,现在这个`diffing`算法其实就正在起作用。我们再回到代码中,我们的`state`是不是每秒都在变?`state`一变,是不是就要驱动页面更新?是不是就要调`render`方法?一旦重新调用了`render`方法,就会获取到一堆新的虚拟`DOM`,按照我们说的`diffing`算法,这时候就要拿新的虚拟`DOM`和之前原有的虚拟`DOM`来做比对。 |
|
|
|
|
|
|
|
在完成比对之后发现`<h2>`和`<input>`还是原来的那写节点,所以在页面展示上并没有发生变化,唯一变化的是`<span>`。`diffing`算法也没有那么智能发现`<span>`中是我们从`state`中取的时间变了,但是`diffing`算法可以发现,这个标签已经不是原来的那个标签了。它比对的最小单位是以标签为单位的。 |
|
|
|
|
|
|
|
所以`diffing`算法并不是说发现了`<span>`中前面几个词没有变只是后面时间在变,而是直接把整个`<span>`都给替换掉了。而其他没有发生改变的标签就直接还是使用原来的,不做任何改变。 |
|
|
|
|
|
|
|
可能有人要说了,你说了这么半天,就一个劲儿说`diffing`算法做了什么。但是你怎么证明`diffing`算法真的存在,而且还真的就这么做了呢? |
|
|
|
|
|
|
|
我们来看一下,页面上是不是有一个输入框?如果说`diffing`算法不存在,那么每秒更新一次页面,新的页面是不是就会生成新的输入框?那么我们输入框的内容应该会在下一次更新之后被直接清空才对吧?那么我们来看一下我们输入之后在更新之后会不会被清空。 |
|
|
|
|
|
|
|
![image-20220105135858594](https://file.lynchow.com/image-20220105135858594.png) |
|
|
|
|
|
|
|
而实际上我们输入的并没有被清空,所以可以见得我们的输入框这个`DOM`在更新的过程中并没有被更新。 |
|
|
|
|
|
|
|
所以说我们得到一个结论就是`diffing`算法是存在的。那么我们再来想一个问题,如果我们在`<span>`里面再来写一个`<input>`,然后输入点东西。那么在更新之后,我们输入的东西会被清空吗?我们来看一下: |
|
|
|
|
|
|
|
![image-20220105140658606](https://file.lynchow.com/image-20220105140658606.png) |
|
|
|
|
|
|
|
为什么这个也没有被清空呢?我们说了`diffing`算法的最小比对单位是标签,而且也说了我们是`span`标签被整体替换为什么这里的`input`标签海水面没有被更新呢?我们说了`diffing`算法比对的最小单位是标签,这里的`input`标签在`span`标签里面那也依然还是标签啊。这也是要参与比对的。 |
|
|
|
|
|
|
|
## `key` |
|
|
|
|
|
|
|
那么我们就这样来验证了我的所说的`diffing`算法是否真的存在。那么我们真正的重点是在我们之前提到过的`key`,这个`key`我们曾经简单地介绍过是给`diffing`算法用的,但是到底是干什么用的呢?这个才是我们这节课的重点也是核心部分。 |
|
|
|
|
|
|
|
我们来看一个案例: |
|
|
|
|
|
|
|
```jsx |
|
|
|
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`。那么我们来看一下效果: |
|
|
|
|
|
|
|
![image-20220105150730963](https://file.lynchow.com/image-20220105150730963.png) |
|
|
|
|
|
|
|
展示方面来说是没有任何问题的。那么我现在想做一个需求,在这边有一个按钮添加一个新的人比如叫`jingxun2`,`age`是 20,那么我们怎么处理? |
|
|
|
|
|
|
|
```jsx |
|
|
|
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]}); |
|
|
|
} |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
我们来看一下上面这段代码,展示情况很明显 |
|
|
|
|
|
|
|
![image-20220105151640050](https://file.lynchow.com/image-20220105151640050.png) |
|
|
|
|
|
|
|
但是我点击了按钮之后的效果是什么呢?我们先来看一下我们的`onClick`事件回调中写的都是什么东西。首先获取原有的`satte`中`persons`属性,然后定义一个新的元素和`persons`数组中的结构一致。而且`id`我按照常规来说都是自增长的,所以我们就通过数组的长度来模拟自增长。然后更新`state`,我们用展开运算符来拼接数组。那么当我们点击了按钮之后的效果究竟会怎么样呢? |
|
|
|
|
|
|
|
![image-20220105152133215](https://file.lynchow.com/image-20220105152133215.png) |
|
|
|
|
|
|
|
成功添加了这是不是还挺好的?功能也实现了。但是这样做得话会有一个很严重的效率问题。怎么说呢?我们从两道面试官很爱问的面试题上来说。 |
|
|
|
|
|
|
|
## `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`,这样虽然页面没问题,但是会拖慢效率的。 |
|
|
|
|
|
|
|
可能有些人说,那我不管,能跑就行。我管效率干什么。那么好来看第二条,大家是不是觉得没明白?我们来改一下代码: |
|
|
|
|
|
|
|
```jsx |
|
|
|
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`输入框?我们来看一下结果: |
|
|
|
|
|
|
|
![image-20220105170642894](https://file.lynchow.com/image-20220105170642894.png) |
|
|
|
|
|
|
|
我们在每个`input`框中输入对应的信息,那么来看一下我们点击了按钮之后会变成什么样子: |
|
|
|
|
|
|
|
![image-20220105170733078](https://file.lynchow.com/image-20220105170733078.png) |
|
|
|
|
|
|
|
这是为什么?我们正常来说是不是应该新增的这一条的`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` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|