上节课我们讲了一下如何使用回调式refs,为什么要使用回调式refs呢?因为我们的字符串式refs有问题,当然不是我们写得有问题,是官方说使用字符串式的refs会有问题,而且后期可能会把字符串式的refs废弃,但是官方推荐我们用回调式refs或者createRef

当然了我们不能一口吃个胖子,一步一步来,上节课我们先学习了回调式的refs。总所周知,我们都有一个习惯,在我们写了一个回调函数啊,或者一次循环啊,或者是递归啊,我们都会喜欢关注一下到底调用了多少次,或者循环了多少次以及递归了多少层。

那么这节课我们就来研究一下:

回顾

当然了,在学习这节课的内容之前呢,我们先对上节课来做一个简单的回顾

  • 回调式refs不会自动收集到this.refs
  • 字符串式refs有问题,后期可能会废弃
  • 回调式refs所传入的ref属性是一个回调函数
  • 回调式refs是将传入的标签节点传入回调函数,并通过回调函数将标签节点挂在实例自身

以上便是上节课所提到的一些要点。这节课我们来开始对回调式ref来进行这一细节性的探讨。

回调式refs回调执行次数

我们先来看一下我们的示例代码

class Demo extends React.Component {
  render() {
    return(
      <div>
        <input ref={c => this.input1 = c} type="text" placeholder="click button" />
        <button onClick={this.showData} type="button">Click me</button>
      </div>
    );
  }
  showData = () => {alert(this.input1.value);};
}

这个代码我就不在多罗嗦了,这就是上节课的代码,只不过我删掉了一个input标签,以及对应的事件处理方法也删除掉了。

接下来我们来讨论一下这个input标签中的ref,这是不是一个回调函数啊?首先这是一个函数,而且是我们定义的,我们也没有调用,最终这个函数还是执行了。所以这是一个回调函数。

那么这个回调函数被执行了几次呢?我们目前已知的是不是在页面第一次渲染的时候执行了一次?如果不执行的话那我们this上面是拿不到input1的啊。那我们来验证一下吧:

class Demo extends React.Component {
  render() {
    return(
      <div>
        <input ref={c => {this.input1 = c;console.log("@",c);} 
          type="text" placeholder="click button" />
        <button onClick={this.showData} type="button">Click me</button>
      </div>
    );
  }
  showData = () => {alert(this.input1.value);};
}

上面的代码其他的不用动,我们来把ref的回调函数加一个打印,方便我们在控制台中观察。那我们来看一下啊刚一开始渲染页面的时候有没有调用这个函数。

image-20211223094032885

在控制台中我们可以看见打印除了当前节点。所以说我们在第一次渲染的时候确实调用了一次这个回调函数。但是还有一个情况,我们来看一下官方所说的:

image-20211223094344791

官方说了如果回调函数是以内联函数的形式定义的,那么在更新过程中会调用两次。至于执行两次的影响是什么,咱们不需要关注。我们先来看看,官方说了内联函数。什么是内联函数啊?我们现在写得这种形式是不是就是内联函数啊?我们过去写代码过程中,把属性写在标签中叫内联属性,把样式写在标签中叫内联样式。那么我们现在这样把函数写在标签中那可不就叫内联函数嘛。

现在我们来谈一下这个执行两次的问题,官方说的是在更新过程中会执行两次,并不是说我们第一次一上来就执行两次。这是什么意思?我们之前是不是说render方法会被调用1 + n次?这里的 1 是什么?是不是第一次渲染页面,那么n又是什么?我们之前说了这个n是状态更新的次数,当state被更新,那么页面是不是也被更新重新渲染?这个时候render方法调用次数是不是就会超过 1 次?当render方法被调用超过 1 次,才会出现ref被调用两次的情况。

那么我们来看一下我们写得这个组件,没有state,没有state上哪更新去啊,所以说在我们当前的组件中从始至终就只调用了一次ref。没有关系,为了验证一下,我们来把之前的天气切换的案例加到这里面来:

class Demo extends React.Component {
  state = { isHot: false }
  render() {
    return(
      <div>
        <h2>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h2>
        <button onClick={this.changeWeather} type="button">点击切换天气</button>
        <input ref={c => {this.input1 = c;console.log("@",c);}} 
          type="text" placeholder="click button" />
        <button onClick={this.showData} type="button">Click me</button>
      </div>
    );
  }
  changeWeather = () => {this.setState({isHot:!this.state.isHot});};
  showData = () => {alert(this.input1.value);};
}

我们看一下代码,一个<h2>标签,里面是天气,然后旁边加一个按钮来点击切换天气,其他的不变。那么我们来看一下效果吧:

image-20211223100410950

我们看到第一次渲染页面的时候控制台是不是调用了一次ref的回调函数?那么我们先把控制台的内容清空一下。

image-20211223100516894

现在控制台里面是什么都没有,那么我们点击按钮切换一下天气,是不是完成了一次state的更新?完成state的更新是不是要重新调用render方法?那我们再看一下效果

image-20211223100657366

这下控制台里打印了两行内容,和官方说的一样,调用了两次,一次传了null,第二次才传当前所在的DOM节点。

现在我们验证了。但是为什么这样呢?

官方也解释了:

这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。

什么意思呢?为了我们写的组件能够展示到页面上,是不是得调用render方法,第一次渲染的时候,程序执行到这个input标签,发现指定了回调式的refs,那么程序就会自动调用这个回调函数。这是第一次渲染并执行了这一次。这一步没有任何问题。

但是当我们点击按钮更新了state之后是不是出现了调用两次的这个情况?所以说问题就出在状态更新这一步。我们都说state会驱动页面更新,怎么驱动的呢?通过重新调用render方法。

那么render方法重新执行的时候,这个input标签这一行是不是也要被重新执行?然后程序又发现你这个标签里面有一个回调式的refs,是不是又要来执行这个函数?但是这个函数已经是一个新函数了,之前的那个函数已经执行完成了,那一块内存就自动释放了。所以这里是一个全新的回调函数。但是程序并没法确定之前那次回调函数都接到了什么。虽然我们都知道上一次函数接到的是当前所在的DOM节点,但是程序是不知道的,不然市场上也不会有那么多我们所谓的人工智障了。正是这个原因。所以程序必须要保证上一次这个回调函数的结果被清空,所以才会先调一次这个回调函数并且传入null来确保上次的结果清空了,然后再调一次这个回调函数来传入当前所在节点。

有人要问了,那这种对我们的程序有什么影响吗?没有影响。一点影响都没用。但是我们想一想,我们的代码在执行的过程中是不是重复的事情干的越少越好?不然算法上为什么要有时间复杂度和空间复杂度呢?同一个函数我们肯定更愿意在达成目的的情况下被调用次数越少越好啊。

react官方也提供了相应的方法:

通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题

这一句又是什么意思?我们想一想之前state那一部分。this指向丢失了,是怎么解决的?是不是两种方法?

  1. 在构造器里调用this.method = this.method.bind(this)
  2. 在类中使用赋值语句配合箭头函数

其实我们上面的第一个方法就是所谓的绑定函数,那么我该怎么写?

class Demo extends React.Component {
  state = { isHot: false }
  render() {
    return(
      <div>
        <h2>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h2>
        <button onClick={this.changeWeather} type="button">点击切换天气</button>
        <input ref={this.bindInput} 
          type="text" placeholder="click button" />
        <button onClick={this.showData} type="button">Click me</button>
      </div>
    );
  }
  changeWeather = () => {this.setState({isHot:!this.state.isHot});};
  showData = () => {alert(this.input1.value);};
  bindInput = c => {
    this.input1 = c;
    console.log("@",c);
  }
}

现在我们看一下效果

image-20211223104136984

第一次调用没有问题,那么我们来改变一下状态

image-20211223104216516

更新了状态也还是一次。所以说通过这种方法就可以解决回调式refs调用两次的情况。但是官方也明确说明了,即便调用两次这种内联函数的形式,其实对我们的程序没有任何影响。所以后期的开发或者是案例,我们还是以写内联函数居多。

以上便是这节课的知识要点。

总结

  • 回调式refs会在页面第一次被渲染时调用一次
  • 如果回调式refs是内联函数形式,那么在state更新之后,会被重新调用两次
  • 内联函数形式的回调式refs调用次数对程序不会有影响
  • 类组件绑定函数的方式可以解决refs被多次调用的问题

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/callback_times/