上节课我们展开介绍了新版生命周期的另一个新增钩子getSnapshotBeforeUpdate,但是官方也明确说明了这个钩子不常用,但是相对于getDerivedStateFromProps钩子在官方描述的罕见来说的话,这个钩子还是相对有点意义的。那么这个钩子到底都有哪些使用场景呢?这节课我们就通过案例来介绍一下。

回顾

在开始今天的案例介绍之前呢,我们还是回头把上节课的几个知识要点简单列一遍,做一个简单的回顾

  • getSnapshotBeforeUpdate钩子在rendercomponentDidUpdate
  • getSnapshotBeforeUpdate钩子必须要有返回值,可以为不是undefined的任何值
  • getSnapshotBeforeUpdate钩子的返回值会顺流传给componentDidUpdate
  • componentDidUpdate会接收组件更新前的snapshotpropsstate

以上便是上节课的一些主要知识点。接下来我们来看一下今天的内容。

案例概述

其实新版生命周期中新增的两个钩子,官方其实也都明确说明了,这两个钩子不常用,但是这节课要讲的这个getSnapshotBeforeUpdate如果真的仔细去研究起来还是有一点意义的,还不至于说像我们另外那一个getDerivedStateFromProps意义不大。

但是这节课的案例我换一种方式来讲。我们先不用react,我们就用原生的来写,然后等用原生写完了之后再来看应该怎么封装成组件。

至于我们要写一个什么案例呢?比如我们要做一个新闻列表的展示,我需要有一个容器,在容器中呢放的是一条一条的新闻。但是我们要像微信朋友圈那种顺序,最新的新闻要放在最上面,按时间顺序依次排下来。

案例演示

这个案例实现起来还是很简单的,我们来看代码:

<div class="list">
  <div class="news">新闻7</div>
  <div class="news">新闻6</div>
  <div class="news">新闻5</div>
  <div class="news">新闻4</div>
  <div class="news">新闻3</div>
  <div class="news">新闻2</div>
  <div class="news">新闻1</div>
</div>

为了方便我就用序号来代替新闻的时间了,序号越大代表越新,大家看我上面这个代码,一个classlist的外层容器。容器中是一条条classnews的新闻详细内容。当然我们还要在给容器加一个样式来方便我们观察:

.list{
  width: 200px;
  height: 170px;
  background-color: skyblue;
}

我们简单地给容器一个宽度和高度,高度我设置的数值看着有些奇怪,为什么是170px。因为我是要让这些新闻刚好都在这个容器中,但是也不要留有空余的地方。大家看我所需要的效果:

image-20220105090359394

就像这样,刚刚好装下所有新闻。接下来呢我们来给每条新闻价格样式:

.news{
  height: 30px;
}

我们看css代码中我给每条新闻都加了一个高度。那么这个时候我们的效果是什么样子呢?

image-20220105090633268

有一条新闻溢出容器了。但是我不希望让它溢出,我希望一旦有新闻溢出,容器就自动给加一个滚动条。这个也简单,就给容器样式加一条就行:

.list{
  width: 200px;
  height: 170px;
  background-color: skyblue;
  overflow: auto;
}

我们给容器加一个overflow属性,那么这样就能实现这个滚动条的功能了

image-20220105091009330

我们来看一下效果,在新闻2下面是不是还有一个新闻1没有被展示出来?当我们滚动滚动条的时候就可以来继续查看那一条了。

那么现在我有一个问题,假如我们的新闻足够多,比如新闻1下面还有新闻a新闻b等等,这个时候我想让新闻6在第一条,我是不是得让滚动条滚动30px?因为每条新闻的高度就是30px嘛。如果我不想用手去拖动这个滚动条,我们想用js应该怎么写?

const container = document.getElementsByClassName("list")[0];
container.scrollTop = 30;

我们来看代码,我们第一步是不是要获取到这个容器啊?这个容器我们没有给id,所以我们就只能用getElementsByClassName方法了。但是这个方法获取到的是不是一个数值啊?所以说我们得加一个索引来拿到这个容器节点。

当我们拿到容器了之后,我们有一个属性不知道大家还记不记得叫scrollTop这个就是用来滚动滚动条的。为了方便观察我把容器的高度减小一点改成120px。容器高 120,每条新闻高 30,那么容器中只能展示出 4 条,有 3 条需要拖动滚动条才能展示,那么我们在控制台中来执行这段js是什么效果:

image-20220105093124507

我们看见新闻6被滚动到了第一条,那么我们再执行一次scrollTop是不是就把新闻5滚动到第一条了呢?

image-20220105093418198

我们发现再次执行的时候滚动条并没有像我们预期中那样被滚动,因为这里并没有一个累加的功能。我们如果想把新闻5滚动上去就必须自己手动计算一下

image-20220105093621698

像这样我们便可以将新闻5滚动到第一条了。另外还有一个知识点就是我们这里我们知道容器多高,以为是我们自己设定的嘛,但是容器中的实际内容到底有多高呢?这就是另外的一个属性,我们之前获取到容器container,那么container上就有一个属性叫scrollHeight

image-20220105094010105

当我们调用这个属性,控制台中也就打印出了这个属性的值为 210,为什么是 210?因为每条新闻高 30,总共 7 条新闻,所以总共的内容高度就是 210。

如果以上这些大家是理解的,那么就好办了。

案例组件

经过前面的这些内容的铺垫,那么接下来,我们就回到react中来,我们来想一想,怎么样把这些东西封装成一个组件?

class NewsList extends React.Component {
  render() {
    return (
      <div className="list">
        <div className="news">新闻7</div>
        <div className="news">新闻6</div>
        <div className="news">新闻5</div>
        <div className="news">新闻4</div>
        <div className="news">新闻3</div>
        <div className="news">新闻2</div>
        <div className="news">新闻1</div>
      </div>
    );
  }
}

我们就是直接把之前demo中代码全搬过来了,我们也不费那个劲去mock数据然后for循环遍历加进去了,就先这样写。然后把样式也都加一下:

.list{
  width: 200px;
  height: 120px;
  background-color: skyblue;
  overflow: auto;
}
.news{
  height: 30px;
}

然后我们来看一下我们的初步效果:

image-20220105095648277

理论上应该是和原生的demo是一样的,当然了实际上也确实是一样的。

需求

既然组件已经知道怎么封装了,那么接下来我要提需求了。什么需求呢,我要求这个容器最一开始没有新闻,然后每隔1s就会多出一条新闻。按照顺序是不是应该新闻1,隔1s新闻1上面添加新闻2,然后又依次这样更新。

实现

这样的一个需求,我们的页面是不是一直在变?那么我们是不是就要用state了,而且我们说了隔1s新增 1 条,那么是不是要用定时器?用到了定时器那是不是就要用componentDidMount了?那么好,我们来看一下代码:

class NewsList extends React.Component {
  state = { newsArr: [] };

  componentDidMount() {
    setInterval(() => {
      const { newsArr } = this.state;
      const news = "新闻" + (newsArr.length + 1);
      this.setState({ newsArr:[news,...newsArr] });
    }, 1000);
  }

  render() {
    return (
      <div className="list">
        {this.state.newsArr.map(n => <div className="news">{n}</div>)}
      </div>
    );
  }
}

我们看一下上面的代码都是在干什么?首先我初始化了一个state,里面有一个newsArr的属性是个空数组,而且我们要开定时器,就得用componentDidMount钩子,在钩子里开启一个循环定时器,接收一个执行器函数和定时参数。执行器函数中我们先解构赋值,那么下面的大家能明白吗?

先看接下来的第一行。我是不是要模拟一条新闻?我的新闻都是新闻number,刚开始没有新闻,我结构赋值的newsArr是空的,长度为 0,而且在更新的过程中,我们state发生了变化,是不是要新增新闻?但是我们新增了新闻也没有把原来已有的新闻丢弃掉?是不是没有丢弃?所以说这个数组是一直在变长的。那么我们用这个数组的长度来模拟这个number刚刚好,但是我的第一条新闻不是新闻0所以要在length的基础上加 1 。

再来看下面这一行,我们已经模拟了新的新闻了,那么是不是就要改state了,我这里把新增的news放在数组的第一位,但是我们也不能丢弃掉之前的新闻,所以我用展开运算符拼接了旧数组。如果大家不记得展开运算符了,那就该回头复习一下前面的内容了。

然后我们再来看一下render方法。因为我们要渲染state中的值了,不能再像原来那样了,我们说过,react会帮我们自动遍历渲染数组,那么我们就直接在容器中写一个数组就行,但是数组里面是纯数据啊,我们需要的是虚拟DOM标签,那么用map方法处理一下数组把原数组改成虚拟DOM标签的数组。

好,代码明白了,我们来看一下效果:

image-20220105102636006

刚开始什么都没用然后展示出新闻1。但是我们再看

image-20220105102858590

控制台报错,为什么报错?因为我们没有给key,之前我们就说了,这种遍历的每一条数据要给一个唯一的key,最简单的方法就是用index。但是直接用index会有问题,这个问题我们在后期会单独说,就目前来说就现用index。那么我们改一下代码:

class NewsList extends React.Component {
  state = { newsArr: [] };

  componentDidMount() {...}

  render() {
    return (
      <div className="list">
        {this.state.newsArr.map(
          (n, index) => <div className="news" key={index}>{n}</div>
        )}
      </div>
    );
  }
}

那么我们现在就不会再有之前那个问题了。

新需求

那么到这我们就算把这个案例写完了呗,但是我们没有用到getSnapshotBeforeUpdate钩子啊,我们看现在我们的逻辑是不是就是有新的新闻,新的新闻都会在最上面然后把旧新闻往下顶啊?这个逻辑是不是也挺正常的?

但是呢大家想想在外面刷朋友圈的时候突然有人发了新的朋友圈,会把我们正在看得内容往下顶吗?肯定不会,这样的话用户体验也太差了。但是我们现在的话,就会这样,我们看着其中一条新闻,然后一有新增,我们的新闻就被挤走了。

那么我的需求就来了,当我再看的一条新闻被挤走了之后,我就拖动滚动条使得我再看的内容重新展示在容器中,然后,新闻虽然在一直增加,但是我在看的这部分区域就不要再自动滚动了。

实现

上面这个需求应该怎么实现呢?那么我们今天要说的钩子getSnapshotBeforeUpdate就要上场了。

我们来回忆一下getSnapshotBeforeUpdate钩子是在什么时候调用的?是不是在render方法和componentDidUpdate之间啊?而且从名字上看这个钩子是不是在页面更新之前就调用了?那么这个钩子和新更新的页面之间是不是相差了一条新闻?我们想一想,容器上方一直在加东西,但是我又想保证当前区域不会被挤到下面去,那么是不是意味着每加一条我们要把当前区域往上滚动一条新闻的高度?这样才相当于当前页面没动啊。

那么我是不是可以在这个钩子里动态计算更新前后的内容区域高度的差值来决定内容往上滚动的滚动值。那么我们用这个方法来处理试一下呗。

class NewsList extends React.Component {
  state = { newsArr: [] };

  componentDidMount() {...}
  getSnapshotBeforeUpdate() {
    return this.list.scrollHeight
  }
  componentDidUpdate(preProps,preState,snapshot){
    this.list.scrollTop += this.list.scrollHeight - snapshot;
  }

  render() {
    return (
      <div className="list" ref={c=>{this.list=c}}>
        {this.state.newsArr.map(
          (n, index) => <div className="news" key={index}>{n}</div>
        )}
      </div>
    );
  }
}

我们来看一下代码componentDidMount钩子不变,我们就不再赘述了。看getSnapshotBeforeUpdate钩子,这个钩子要返回一个快照值,当我们滚动了内容区域之后,我们想看一下某个区域的内容这个时候我们是不是可以获取一下当前的那个内容高度。因为我们要让这个区域不被挤走,我们是一定要先获取一下位置的,但是我们通过什么获取位置呢?我们当前来看唯一可以获取到的就是内容高度来做一个标识了。

但是内容高度怎么拿?是不是要拿到容器的节点,用document吗?大家说用document的那就是组件三大属性学得不扎实,赶紧回去复习refs。所以我在render中给容器加了个ref,然后我们再回到钩子里来看,我们通过ref获取到了容器然后把容器的scrollHeight返回出来了。前面我们在介绍这个钩子的时候控制台中曾经报过错,就是写了getSnapshotBeforeUpdate钩子就一定要写componentDidMount钩子。而且getSnapshotBeforeUpdate钩子的返回值是传给componentDidMount钩子的。

那么好,我们来看componentDidMount钩子,这个钩子在什么时候调用?是不是页面已经完成更新时候调用?那么是不是说明我们在看的区域已经被挤下去了?那么我们是不是要在这个钩子里把我们的内容区域往上滚动?但是滚动多少?我们不知道啊如果我们要看得区域和实际区域刚好差一条新闻的话是不是就滚动一条新闻的高度就可以了?那么getSnapshotBeforeUpdate返回的高度和现在的内容高度是不是也刚好差一条新闻的高度?诶?那么现在我们把这两个值一减是不是就是我们要滚动的值呢?

其实不是,由于我当前这个环境无法截动图,就不给大家展示了,实际效果大家测试一下。就是我们没有能成功把某个区域固定住,还是会被往下挤。以为我们刚才说的那个差值是不是都永远想相差 30 啊?因为这是更新前和更新后啊,永远都相差一条新闻,当我们拖动滚动条我们并不一定刚好和更新后相差一条新闻的。

那么我们应该怎么处理呢?我们是不是要累加?因为持续有新的新闻进来,那么我们就要持续把新的新闻的高度累加到我们要滚动的值上?那么我们再来看一看效果:

image-20220105112039151

页面渲染之后我没有做然后操作,然后区域就自动固定在这里了,然后,无论我拖动到哪里,容器区域内容都不会被自动滚动。这样我们便实现了我们想要的效果。

总结

  • scrollTop属性可以使页面向上滚动
  • scrollTop属性没有自动累加功能
  • scrollHeight可以获取当前容器的内容区域的高度

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