当节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP (插入)、MOVE_EXISTING (移动)和 REMOVE_NODE (删除)。
-
INSERT_MARKUP :新的组件类型不在旧集合里,即全新的节点,需要对新节点执行插入操作。
-
MOVE_EXISTING :旧集合中有新组件类型,且 element 是可更新的类型,generateComponentChildren 已调用receiveComponent ,这种情况下 prevChild=nextChild ,就需要做移动操作,可以复用以前的 DOM 节点。
-
REMOVE_NODE :旧组件类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。
旧集合中包含节点A、B、C和D,更新后的新集合中包含节点B、A、D和C,此时新旧集合进行diff差异化对比,发现B!=A,则创建并插入B至新集合,删除旧集合A;以此类推,创建并插入A、D和C,删除B、C和D。
我们发现这些都是相同的节点,仅仅是位置发生了变化,但却需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。React针对这一现象提出了一种优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分。 虽然只是小小的改动,性能上却发生了翻天覆地的变化!我们再来看一下应用了这个策略之后,react diff是如何操作的。
通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置,此时React 给出的diff结果为:B、D不做任何操作,A、C进行移动操作即可。
具体的流程我们用一张表格来展现一下:
| index | 节点 | oldIndex | maxIndex | 操作 | |
| — | — | — | — | — | — |
| 0 | B | 1 | 0 | oldIndex(1)>maxIndex(0),maxIndex=oldIndex | |
| 1 | A | 0 | 1 | oldIndex(0)<maxIndex(1),节点A移动至index(1)的位置 | |
| 2 | D | 3 | 1 | oldIndex(3)>maxIndex(1),maxIndex=oldIndex | |
| 3 | C | 2 | 3 | oldIndex(2)<maxIndex(3),节点C移动至index(2)的位置 | |
-
index: 新集合的遍历下标。
-
oldIndex:当前节点在老集合中的下标。
-
maxIndex:在新集合访问过的节点中,其在老集合的最大下标值。
操作一栏中只比较oldIndex和maxIndex:
-
当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex
-
当oldIndex=maxIndex时,不操作
-
当oldIndex<maxIndex时,将当前节点移动到index的位置
上面的例子仅仅是在新旧集合中的节点都是相同的节点的情况下,那如果新集合中有新加入的节点且旧集合存在 需要删除的节点,那么 diff 又是如何对比运作的呢?
| index | 节点 | oldIndex | maxIndex | 操作 |
| — | — | — | — | — |
| 0 | B | 1 | 0 | oldIndex(1)>maxIndex(0),maxIndex=oldIndex |
| 1 | E | - | 1 | oldIndex不存在,添加节点E至index(1)的位置 |
| 2 | C | 2 | 1 | 不操作 |
| 3 | A | 0 | 3 | oldIndex(0)<maxIndex(3),节点A移动至index(3)的位置 |
注:最后还需要对旧集合进行循环遍历,找出新集合中没有的节点,此时发现存在这样的节点D,因此删除节点D,到此 diff 操作全部完成。
同样操作一栏中只比较oldIndex和maxIndex,但是oldIndex可能有不存在的情况:
- oldIndex存在
-
当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex
-
当oldIndex=maxIndex时,不操作
-
当oldIndex<maxIndex时,将当前节点移动到index的位置
- oldIndex不存在
- 新增当前节点至index的位置
当然这种diff并非完美无缺的,我们来看这么一种情况:
实际我们只需对D执行移动操作,然而由于D在旧集合中的位置是最大的,导致其他节点的oldIndex < maxIndex,造成D没有执行移动操作,而是A、B、C全部移动到D节点后面的现象。针对这种情况,官方建议:
在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在一定程度上会影响React的渲染性能。
由于key的存在,react可以准确地判断出该节点在新集合中是否存在,这极大地提高了diff效率。我们在开发过中进行列表渲染的时候,若没有加key,react会抛出警告要求开发者加上key,就是为了提高diff效率。但是加了key一定要比没加key的性能更高吗?我们再来看一个例子:
现在有一集合[1,2,3,4,5],渲染成如下的样子:
现在我们将这个集合的顺序打乱变成[1,3,2,5,4]。
1.加key
操作:节点2移动至下标为2的位置,节点4移动至下标为4的位置。
2.不加key
操作:修改第1个到第5个节点的innerText
如果我们对这个集合进行增删的操作改成[1,3,2,5,6]。
1.加key
操作:节点2移动至下标为2的位置,新增节点6至下标为4的位置,删除节点4。
2.不加key
操作:修改第1个到第5个节点的innerText
通过上面这两个例子我们发现:
由于dom节点的移动操作开销是比较昂贵的,没有key的情况下要比有key的性能更好。
通过上面的例子我们发现,虽然加了key提高了diff效率,但是未必一定提升了页面的性能。因此我们要注意这么一点:
对于简单列表页渲染来说,不加key要比加了key的性能更好
根据上面的情况,最后我们总结一下key的作用:
-
准确判断出当前节点是否在旧集合中
-
极大地减少遍历次数
==============================================================
示例代码地址:https://github.com/ruichengpi…
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AgBbpToV-1590739472382)(https://segmentfault.com/img/bVbrxNK?w=840&h=426)]
现在有这么一个需求,当用户身份变化时,当前页面重新加载数据。猛一看过去觉得非常简单,没啥难度的,只要在componentDidUpdate这个生命周期里去判断用户身份是否发生改变,如果发生改变就重新请求数据,于是就有了以下这一段代码:
import React from ‘react’;
import {connect} from ‘react-redux’;
let oldAuthType = ‘’;//用来存储旧的用户身份
@connect(
state=>state.user
)
class Page1 extends React.PureComponent{
state={
loading:true
}
loadMainData(){
//这里采用了定时器去模拟数据请求
this.setState({
loading:true
});
const timer = setTimeout(()=>{
this.setState({
loading:false
});
clearTimeout(timer);
},2000);
}
componentDidUpdate(){
const {authType} = this.props;
//判断当前用户身份是否发生了改变
if(authType!==oldAuthType){
//存储新的用户身份
oldAuthType=authType;
//重新加载数据
this.loadMainData();
}
}
componentDidMount(){
oldAuthType=this.props.authType;
this.loadMainData();
}
render(){
const {loading} = this.state;
return (
{`页面1${loading?'加载中...':'加载完成'}`}
)
}
}
export default Page1;
看上去我们仅仅通过加上一段代码就完成了这一需求,但是当我们页面是几十个的时候,那这种方法就显得捉襟见肘了。哪有没有一个很好的方法来实现这个需求呢?其实很简单,利用react diff的特性就可以实现它。对于这个需求,实际上就是希望当前组件可以销毁在重新生成,那怎么才能让其销毁并重新生成呢?通过上面的总结我发现两种情况,可以实现组件的销毁并重新生成。
-
当组件类型发生改变
-
当key值发生变化
接下来我们就结合这两个特点,用两种方法去实现。
第一种:引入一个loading组件。切换身份时设置loading为true,此时loading组件显示;切换身份完成,loading变为false,其子节点children显示。
第二种:在刷新区域加上一个key值就可以了,用户身份一改变,key值就发生改变。
第一种和第二种取舍上,个人建议的是这样子的:
如果需要请求服务器的,用第一种,因为请求服务器会有一定等待时间,加入loading组件可以让用户有感知,体验更好。如果是不需要请求服务器的情况下,选用第二种,因为第二种更简单实用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zt4JNYab-1590739472383)(https://segmentfault.com/img/remote/1460000018914265)]
针对这个需求,我们喜欢将搜索条件封装成一个组件,查询列表封装成一个组件。其中查询列表会接收一个查询参数的属性,如下所示:
import React from ‘react’;
import {Card} from ‘antd’;
import Filter from ‘./components/filter’;
import Teacher from ‘./components/teacher’;
export default class Demo2 extends React.PureComponent{
state={
filters:{
name:undefined,
height:undefined,
age:undefined
}
}
handleFilterChange=(filters)=>{
this.setState({
filters
});
}
render(){
const {filters} = this.state;
return
{/* 过滤器 */}
{/* 查询列表 */}
}
}
现在我们面临一个问题,如何在组件Teacher中监听filters的变化,由于filters是一个引用类型,想监听其变化变得有些复杂,好在lodash提供了比较两个对象的工具方法,使其简单了。但是如果后期给Teacher加了额外的props,此时你要监听多个props的变化时,你的代码将变得比较难以维护。针对这个问题,我们依旧可以通过key值去实现,当每次搜索时,重新生成一个key,那么Teacher组件就会重新加载了。代码如下:
import React from ‘react’;
import {Card} from ‘antd’;
import Filter from ‘./components/filter’;
import Teacher from ‘./components/teacher’;
export default class Demo2 extends React.PureComponent{
state={
filters:{
name:undefined,
height:undefined,
age:undefined
},
tableKey:this.createTableKey()
}
createTableKey(){
return Math.random().toString(36).substring(7);
}
handleFilterChange=(filters)=>{
this.setState({
filters,
//重新生成tableKey
tableKey:this.createTableKey()
});
}
render(){
const {filters,tableKey} = this.state;
return
{/* 过滤器 */}
{/* 查询列表 */}
}
}
即使后期给Teacher加入新的props,也没有问题,只需拼接一下key即可:
最后
==
就答题情况而言,第一问100%都可以回答正确,第二问大概只有50%正确率,第三问能回答正确的就不多了,第四问再正确就非常非常少了。其实此题并没有太多刁钻匪夷所思的用法,都是一些可能会遇到的场景,而大多数人但凡有1年到2年的工作经验都应该完全正确才对。
只能说有一些人太急躁太轻视了,希望大家通过此文了解js一些特性。
并祝愿大家在新的一年找工作面试中胆大心细,发挥出最好的水平,找到一份理想的工作。