在Detail组件中使用DetailNavBar组件时,其center插槽中放置了四个tag:['商品', '参数', '评论', '推荐']。事实上,这四个tag分别对应详情页面的四个部分。现在我们希望点击某个tag,页面会自动滚动到对应的区域;反之,当页面滚动到某个区域时,对应的tag被高亮(字色变红)。

 

我们先来看如何实现前半部分,即点击tag—自动滚动。由于滚动的操作要在Scroll组件中完成,而Scroll也是Detail的子组件,所以我们把主要逻辑放在公共的父组件Detail中完成。这样,点击事件发生后,要先将这一事件发送给父组件,一同发送的还有被点击tag的索引。

this.$emit('titleClick', index)

父组件Detail监听到事件后,执行titleClick方法:

<detail-nav-bar class="detail-nav" @titleClick="titleClick" ref="nav"></detail-nav-bar>
titleClick(index) {
      this.$refs.scroll.scrollTo(0, -this.themeTopYs[index], 200);
}

给子组件的标签添加属性ref="xxx"后,父组件就可以通过this.$refs.xxx来访问子组件了。这条语句的意思是,点击后,滚动到-this.themeTopYs[index]的位置,滚动时间为200ms。

那么可想而知,themeTopYs数组中存放的便是那四个tag所对应的页面位置,那么这个数组怎么得到的呢? “商品”对应的滚动距离是0,“参数”对应的滚动距离应该是参数组件的根元素的offsetTop属性,“评论”和“推荐”对应的滚动距离与之同理,如下:

this.themeTopYs = [];
this.themeTopYs.push(0);
this.themeTopYs.push(this.$refs.params.$el.offsetTop);
this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);

第二个问题是:什么时候去拿这些距离值呢?

我们先假设在mounted()钩子函数中进行上述操作,结果themeTopYs数组的第二、三个值均为undefined,说明此时参数组件和评论组件的根元素不存在,这一点也不奇怪,因为这两个组件中的根元素上都添加了vif判断,只有当父组件请求到数据且发送给子组件时,才会对模板进行渲染。而数据请求是在created中开始的,由于请求是异步的,所以当生命周期到mounted时,并不一定能拿到数据。

<div class="param-info" v-if="Object.keys(paramInfo).length !== 0">
...

为了保证拿到数据后再进行操作,我们又将上面的代码转移到网络请求函数的then回调的末尾。再次测试,发现themeTopYs数组的第二、三个值仍然是undefined,这是因为此时虽然拿到了数据,但渲染也需要一定的时间。而渲染完成对应的钩子函数为updated,但是updated太过于敏感,容易被触发多次。我们采用另一个函数:this.$nextTick( () => {}),它的功能为:

  将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

将该函数套在原代码外面,还是放在在then回调的末尾,结果显示可以拿到距离值,且点击tag可以跳转到正常的位置。

      this.$nextTick(() => {
        this.themeTopYs = [];
        this.themeTopYs.push(0);
        this.themeTopYs.push(this.$refs.params.$el.offsetTop);
        this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
        this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
        console.log(this.themeTopYs)
      });

但是这个方法仍然存在bug:当进入详情页后立刻点击tag,还是会滚动到错误的位置,这是因为虽然此时DOM渲染完成了,但图片还未加载完成,自然有一部分高度没有被计算在内。

因此我们的最终方案是在图片加载完成后去读取各个位置的距离值。由于这几个位置与最下面推荐图片的加载无关,故只要当前商品的大图加载完成就可以了。与之对应的事件为:

<detail-goods-info :detail-info="detailInfo" @imageload="imageLoad" class="goods-info"></detail-goods-info>

我们将上面的操作封装一个函数,并套一层防抖(只需要在最后一张图片加载完成后执行)。

      this.getThemeTopY = debounce( () => {
        this.themeTopYs = [];
        this.themeTopYs.push(0);
        this.themeTopYs.push(this.$refs.params.$el.offsetTop);
        this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
        this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
      }, 200)
    imageLoad() {
      this.$refs.scroll.refresh();
      this.getThemeTopY()
    }

这次,无论什么时候点击tag,都可以滚动到对应的位置了。

 

接着我们先来看如何实现反向的效果:滚动到某个tag对应的位置,该tag高亮。这就必然要求我们去监听滚动事件,并获取垂直方向上的滚动距离。因此,由scroll组件将滚动事件发射出来:

<scroll class="content" ref="scroll" :probeType="3" @scroll="contentScroll">

在contentScroll中,我们进行如下操作:

    contentScroll(position){
      const positionY = -position.y
      let length = this.themeTopYs.length
      for(let i = 0; i < length; i++){
        parseInt(i)
        if((this.currentIndex !== i) && ((i < length - 1 && positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i+1]) || 
        (i == length - 1 && positionY >= this.themeTopYs[i]))){
          this.currentIndex = i
          this.$refs.nav.currentIndex = this.currentIndex
        }
      }
      //  是否显示回到顶部
      this.isShowBackTop = -position.y > 1000;
    }

显然,上面代码的含义是:在滚动过程中,不断检测滚动位置positionY和themeTopYs数组中四个元素的大小关系,通过改变currentIndex来切换高亮的tag。我们重点研究一下上面代码中的if判断条件:

if(
    (this.currentIndex !== i) 
&& 
(
    (i < length - 1 && positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i+1])
|| 
    (i == length - 1 && positionY >= this.themeTopYs[i]) ) )    

整体结构为:if(A&&((B&&C&&D)|| (E&&F)))。假设themeTopYs中的四个高度为x=0、y、z、w。则有如下关系:

当x<高度<y时,currentIndex=0;

当y<高度<z时,currentIndex=1;

当z<高度<w时,currentIndex=2;

当高度>z时,currentIndex=3。

所以,上面的(B&&C&&D)就对应这前三种情况,需要满足两个边界条件;而(E&&F)对应最后一种情况,只需要满足一个边界条件。

最后,A条件是为了优化性能,只有当currentIndex不等于当前 i 时,才有必要进行后面的判断。

还有一种思路,是将themeTopYs的最后一个元素设为Number.MAX_VALUE,把循环的终止条件改为length - 1,这样第四个tag的判断条件就与其它三个一致了。这种思路的性能更高。

posted on 2021-06-18 22:42  springxxxx  阅读(117)  评论(0)    收藏  举报