上节课我们对新版生命周期做了一个阶段性的总结。这节课我们不继续往下进行,我们先来插一个话题,关于我们之前提起过的,reactdiffing算法。

回顾

理论上我们在开始新的一课的内容的时候都是要先对上一节课的内容来做一个简单的回顾,但是上一节其实没有什么可回顾的内容,简单来说新版生命周期和旧版生命周期之间核心部分都没用什么区别,而被废弃以及新增的钩子都不常用,只要了解就好,大家感兴趣的可以往深处去研究一下。接下来我们就开始介绍一下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

那么我们的代码就很明确了,我们要在页面上展示一个标题,一个输入框,和一个时间字符串,而且这个时间字符串是每秒更新一次的。

image-20220105133634223

像这样,实际效果中这边的时间是一直在更新的。那么我跟大家说,现在这个diffing算法其实就正在起作用。我们再回到代码中,我们的state是不是每秒都在变?state一变,是不是就要驱动页面更新?是不是就要调render方法?一旦重新调用了render方法,就会获取到一堆新的虚拟DOM,按照我们说的diffing算法,这时候就要拿新的虚拟DOM和之前原有的虚拟DOM来做比对。

在完成比对之后发现<h2><input>还是原来的那写节点,所以在页面展示上并没有发生变化,唯一变化的是<span>diffing算法也没有那么智能发现<span>中是我们从state中取的时间变了,但是diffing算法可以发现,这个标签已经不是原来的那个标签了。它比对的最小单位是以标签为单位的。

所以diffing算法并不是说发现了<span>中前面几个词没有变只是后面时间在变,而是直接把整个<span>都给替换掉了。而其他没有发生改变的标签就直接还是使用原来的,不做任何改变。

可能有人要说了,你说了这么半天,就一个劲儿说diffing算法做了什么。但是你怎么证明diffing算法真的存在,而且还真的就这么做了呢?

我们来看一下,页面上是不是有一个输入框?如果说diffing算法不存在,那么每秒更新一次页面,新的页面是不是就会生成新的输入框?那么我们输入框的内容应该会在下一次更新之后被直接清空才对吧?那么我们来看一下我们输入之后在更新之后会不会被清空。

image-20220105135858594

而实际上我们输入的并没有被清空,所以可以见得我们的输入框这个DOM在更新的过程中并没有被更新。

所以说我们得到一个结论就是diffing算法是存在的。那么我们再来想一个问题,如果我们在<span>里面再来写一个<input>,然后输入点东西。那么在更新之后,我们输入的东西会被清空吗?我们来看一下:

image-20220105140658606

为什么这个也没有被清空呢?我们说了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>
    );
  }
}

首先我们定义了一个组件,初始化了statestatepersons属性是一个数组,里面都是对象。然后我们在render方法中渲染展示nameage。那么我们来看一下效果:

image-20220105150730963

展示方面来说是没有任何问题的。那么我现在想做一个需求,在这边有一个按钮添加一个新的人比如叫jingxun2age是 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]});
  }
}

我们来看一下上面这段代码,展示情况很明显

image-20220105151640050

但是我点击了按钮之后的效果是什么呢?我们先来看一下我们的onClick事件回调中写的都是什么东西。首先获取原有的sattepersons属性,然后定义一个新的元素和persons数组中的结构一致。而且id我按照常规来说都是自增长的,所以我们就通过数组的长度来模拟自增长。然后更新state,我们用展开运算符来拼接数组。那么当我们点击了按钮之后的效果究竟会怎么样呢?

image-20220105152133215

成功添加了这是不是还挺好的?功能也实现了。但是这样做得话会有一个很严重的效率问题。怎么说呢?我们从两道面试官很爱问的面试题上来说。

key的作用是什么

在大家面试中面试官其实是很喜欢问一些底层原理的东西的,比如react/vue方面,面试官都会比较爱问这个key的作用是什么呢?基本原理是什么呢?

简单地说key是虚拟DOM对象的标识,在更新显示时key起着机器重要的作用。但是这么说实在也太笼统了,详细来解释吧:

state中数据发生变化的时候,react会根据新数据生成新的虚拟DOM,随后react进行新虚拟DOM和旧虚拟DOMdiff比对。这里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都没能成功复用下来啊。所以我们刚才说的那种方法虽然功能已经实现了,但是存在严重的效率问题。

keyindex

另外一个问题,就是我们之前说的,我们之前遍历列表,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输入框?我们来看一下结果:

image-20220105170642894

我们在每个input框中输入对应的信息,那么来看一下我们点击了按钮之后会变成什么样子:

image-20220105170733078

这是为什么?我们正常来说是不是应该新增的这一条的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/