仿掘金项目之文章目录滚动高亮实现 | 青训营笔记

1,199 阅读4分钟

这是我参与「第四届青训营 」笔记创作活动的第5天

仿掘金项目之实现文章预览|青训营笔记 - 掘金 (juejin.cn)

仿掘金项目之文章目录实现|青训营笔记 - 掘金 (juejin.cn)

书接上文

前面我已经实现了目录的基本功能,点击跳转和点击跳转时高亮当前目录

image.png

但是当我们通过鼠标滚轮滚动时,目录高亮内容并不会跟着页面内容变化,接下来就是解决这个问题

我们要通过当前页面距离顶部的距离和h标签距离顶部的距离相比较,来判断该高亮哪一个目录内容

获取当前页面距离顶部的距离

const scroll = () => {
  window.addEventListener(
    "scroll",
    (fun = () => {
      if (timer) {
        return;
      }
      timer = setTimeout(() => {
        let _scrollTop =
          window.scrollY ||
          window.pageYOffset ||
          document.documentElement.scrollTop;
        height.value = _scrollTop + 100;
        timer = null;
      }, 500);
    })
  );
};

这里我监听scroll事件,当页面滚动时就去获取当前页面距离顶部的距离

这里fun是一个变量名,指向srcoll的回调函数,用于页面销毁时,销毁srcoll事件

height是一个响应式数据,保存当前页面距离顶部的距离,这里我加了100是因为掘金上滚动不是到可视区域的顶部才改变高亮内容.这里有一段距离的

image.png

这里我用了节流来控制计算距离顶部的次数,如果不控制的话,会浪费性能,而且频繁计算也没多大用处

所以为啥不用防抖呢?

其实我一开始用的也是防抖,但是我在测试的时候发现,防抖用简单的话讲,事件被触发之后只会执行最后一次.

这里不妨想象一下,如果我不停的滚动,页面的h标签已经过了好几个了,但是你会发现目录还是一开始的那个内容高亮,等你停下来之后,还得过个500ms(我这里是500ms)才能高亮当前目录.而且还是从一开始的高亮内容直接跳到当前内容,这明显没有我们想要的滚动高亮效果

使用节流,简单的讲就是事件被触发后一段时间内只执行第一次 同样的场景,我不停的滚动,但是只要我滚动了,在接下来的500ms之后,我就能获取一次当前页面距离顶部的值.所以我不停的滚动并不会影响我每500ms获取一次值.值的变化可以带来目录高亮内容更新

封装scroll事件

因为后面我还写了回到顶部这个小功能,在这个功能中,只有页面距离顶部一定距离之后才会显示回到顶部的按钮,所以这里也要使用scroll事件,这个项目中还有不少地方要使用scroll事件,所以我干脆提取出来封装一下,免得重复写.

节流函数

export function throttle(func, delay) {
  let timer;
  return function () {
    if (timer) return   
      timer = setTimeout(() => {
        func.call(this, ...arguments);
        timer = null
      }, delay);
    }
}

滚动事封装

import { throttle } from "./throttle"
export const windowScroll = (ref) => {
  let resFun
  window.addEventListener(
    "scroll",
    (resFun = throttle(function () {
      ref.value =
        window.scrollY ||
        window.pageYOffset ||
        document.documentElement.scrollTop;
    }, 500)
    )
  )
  return resFun
}

使用和销毁

onMounted(() => {
  resFun =  windowScroll(curHeight)
})
onUnmounted(() => {
  window.removeEventListener('scroll',resFun)
})

这里要传一个响应式数据,返回的是scroll的回调函数,用于销毁监听事件

获取h标签距离顶部的距离

首先我们要拿到文章内容部分的所有h标签元素.

因为一开始我并不知道到querySelectorAll('h1,h2,h3,h4,h5,h6')这种写法,所以我是在前面给所有h标签包裹一层div的同时,给div都加了同一个类名jump-site,用来获取所有h标签

toc.forEach((item, index) => {
        let _toc = `<div class="jump-site" id=${index}>${item}</div>`;
        data = data.replace(item, _toc);
      });

这样我就可以获取页面中所有h标签距离顶部的位置

const getHtagHeight = () => {
  let tag = document.querySelectorAll(".jump-site");
  let arr = [];
  for (let i = 0; i < tag.length; i++) {
    arr.push(tag[i].offsetTop);
  }
  hTagHeight.value = arr;
};

image.png

判断目录高亮内容

const activeScroll = () => {
  let arr = hTagHeight.value;//所有h标签距离顶部的距离的数组
  if (arr[0] > height.value) return (activeIndex.value = 0);
  //height.value 当前页面距离顶部的距离
  else if (arr[arr.length - 1] < height.value) {
    activeIndex.value = arr.length - 1;
  }
  for (let i = 0; i < arr.length - 1; i++) {
    if (arr[i] < height.value && arr[i + 1] > height.value) {
      return (activeIndex.value = i);
    }
  }
};

这里我已经得到了当前页面距离顶部的距离和所有h标签距离顶部的距离

当前页面小于第一个h标签时,高亮第一个目录内容,大于最后一个标签时,高亮最后一个目录内容

当前页面在位于第i个h标签和第i+1之间时,显示第i个标签对应的目录内容

在滚动事件的回调函数中调用该函数,每次滚动都会执行,通过修改activeIndex的值,给目录中的li元素添加active类来改变样式呈现高亮效果

到这里最基本的目录滚动高亮效果就实现了.

但是...真的这么简单吗?

image.png

image.png

仔细观察这两张图,除了内容不同,还有啥不一样?

答案是滚动条

如果目录的内容过多,超出一定范围,就只能隐藏一部分. 正是因为内容过多,按照前面的写法,滚动高亮效果有一个大bug,这也是最麻烦的部分,下次再说.