vue实现动态文章目录高亮效果,提供全代码及详细讲解

24 篇文章 0 订阅
23 篇文章 0 订阅

滚动后目录变化效果

directory

点击后目录变化效果

directory

步骤

生成目录列表

  1. 找到存放所有html标签的 dom,设置ref。后续获取到该标签

image-20240712234630148

  1. 定义层级,H1 H2 H3 三层即可

  2. 定义空数组存放所有目录对象

  3. 遍历content内容dom根节点 如果符合H1 H2 H3这三个标签则 设置id为header-0 header-2…

  4. 中途存一份标签目录到titlesDoms,后续遍历获取其位置来 根据视口位置高亮目录标签

  5. push目录对象 有id为header打头,后续监听所有a标签以header打头的点击事件,点击后触发方法传递该值找到该a标签设置样式,且样式颜色为!important否则无法覆盖高亮,高亮时移除所有 a标签的highlight样式类,初始化

  6. 防抖函数,window监听滚动事件,事件触发 函数内部获取所有 目录的dom,遍历map生成所有dom的位置数组, for循环监视所有的dom的距顶变化而高亮内容匹配则break,不进行下次遍历,这里的判断距顶变化分为两种情况,一种是判断头部法,直接判断距顶是否在某个范围内,如果是则高亮,一种是判断身体法,也就是下一个目录标签在视口外,且当前视口 距顶为负值,也就是说当前视口的内容为 当前目录标签的身体部分,我们这个时候可以使用距顶为负数,且下一个目录标签距顶大于视口高度,也就是说其在视口下面
    这个时候进行高亮当前即可,因为在看当前标签目录的身体部分

防抖简介

好比电梯门,每次都会进来新的人,只有最后一次进来的人之后,电梯门才会关上,否则每一个人进来了, 都会重置关门倒计时,也就是防止了持续的scroll事件而不持续的触发回调,也就是说不持续的关门开门关门开门

代码示例

 debounce(fn, delay = 100) {
            let timer = null;
            // 封装一个闭包,内部函数使用外部timer定时器,防止全局污染
            // 该闭包函数有debounce调用后返回
            // 且其返回值作为防抖函数给scroll事件多次调用
            // 同时该函数还能用到timer变量,防止变量名的外部占用
            // 好比开了一个小空间,将变量timer共享在所有调用debounce返回的新fn函数里
            // 绕这么大一圈就是为了不污染全局变量名
            return function (...args) {
                // 下次触发时定时器清空,冷却时间重置
                // 重新计时delay
                clearTimeout(timer);
                timer = setTimeout(() => {
                    // 调用需要防抖的函数fn
                    fn.apply(this, args);
                }, delay)
            }
        },

节流简介

节流类似于技能冷却,第一次放完技能之后需要等待一定才能继续放技能, 而这段时间为 当前时间——触发时的时间>冷却时间, 如果当前时间减去触发事件大于冷却时间,说明中间的时间跨度,也就是其经历的冷却时间达到要求,此时可进行下一次放技能,也就是触发回调函数, 和防抖相反,防抖看最后一次,节流看第一次,后面冷却时间内所有都无效

闭包函数

使用闭包,防止污染全局变量,其内部变量值 在多次调用该防抖函数后产生的多个新函数 之间共享,不断触发,不断清空定时器(防抖) 设置最新的放技能时间(节流)

代码示例

throttle(fn, interval) {
            // 第一个上车的人
            let last = 0
            // 闭包
            return function () {
                // 保存调用函数的this上下文,也就是window
                let context = this
                let args = arguments
                // 记录最新乘客
                let now = +new Date()
                // 如果第一个和最后一个上车的间隔超过规定发车时间
                // 则调用函数,这里第一次last为0直接调用
                // 因此也称第一次调用后一段时间不能使用.技能冷却
                if ((now - last) > interval) {
                    // 更新最新时间,
                    // 然后随着now大到第一次调用last,则可调用下一次
                    last = now
                    fn.apply(context, args)
                }
            }
        },

以下文章目录的js代码+html代码

html

设置遍历每个a标签,并且绑定值和左内边距,绑定点击事件,防止路径变化为#xxx,从而导致路由失效

<ul class="catalog">
                        <li v-for="(item,index) in catelog"
                            :key="index"
                        >
                            <!--<a :href="'#'+item.id">-->
                            <!--    {{ item.title }}-->
                            <!--</a>-->
                            <a @click="changeHash(`#${item.id}`)"
                               :id=item.id
                               :style="{paddingLeft:item.level * 10+'px'}"
                            >
                                {{ item.title }}
                            </a>
                        </li>
                    </ul>

data代码

  data() {
        return {
            article: {},
            loading: false,
            catelog: [],
            titlesDoms: [],
            //存放防抖函数返回的匿名函数,否则后续随着实例的消失匿名函数消失,此时无法移除监听器
            //通过 这个匿名函数来移除,否则后续切换组件时由于给的是window的监听,后续可能监听到其他组件行为
            // 我们将其设置在
            scrollHandler: null
        }
    },

钩子函数

 created() {
        this.getArticleData()
    },
    mounted() {
        // 切换页面时滚动条自动滚动到顶部
        window.scrollTo(0, 0);
        // 监听滚动事件,随着视口高度高亮对应的目录标签
        // window.addEventListener('scroll', this.throttle(this.handleScroll,4000))
          this.scrollHandler = this.debounce(this.handleScroll, 100)
        window.addEventListener('scroll', this.scrollHandler)
    },
    beforeDestroy() {
        //指定存放的匿名函数,以此来移除监听器
        window.removeEventListener('scroll',  this.scrollHandler)
    },

method方法

注意。有些是不必要的,自行分辨即可

methods: {
        changeHash(id) {
            const element = document.querySelector(id)
            const distanceTop = element.offsetTop;
            scrollTo(0, distanceTop - 82)
        },
        getTime(time) {
            return common.timestampToTime(time, 1)
        },
        getArticleData() {
            this.loading = true
            selectById(this.$route.params.articleId).then(res => {
                if (res.code === 20000) {
                    this.article = res.data
                    this.generateCatalog()
                }
                this.loading = false
            })
        },
        // 加工目录数组
        generateCatalog() {
            // 等待所有dom标签挂载完毕
            this.$nextTick(() => {
                // 获取到文章的根标签
                const articleContent = this.$refs.articleContent;
                // 定义要分层的标签
                const titleTag = ["H1", "H2", "H3"];
                // 空数组收集所有的层级对象,id标识每一个div,标题,层级,节点标签名
                let titles = [];
                articleContent.childNodes.forEach((item, index) => {
                    if (titleTag.includes(item.nodeName)) {
                        const id = "header-" + index;
                        item.setAttribute("id", id);
                        // 后续遍历目录标签titleDom,判断顶部高度距离来高亮标签的
                        this.titlesDoms.push(item)
                        titles.push({
                            id: id,
                            title: item.innerHTML,
                            // 截取节点名H3  的数字部分作为层数,
                            // 截取下标为1的字符串,不截取下标为2的字符串,也就是左开右闭
                            // 层级作用为缩进的长度
                            level: Number(item.nodeName.substring(1, 2)),
                            nodeName: item.nodeName
                        })
                    }
                })
                // 等待a标签挂载完毕后设置监听器
                setTimeout(() => {
                    this.setClickListen()

                    // 初始化高亮
                }, 50)
                this.catelog = titles
            })
        },
        // 高亮目录标题
        highlight(header) {
            console.log(header)
            // 清空所有a标签的高亮,进行初始化
            document.querySelectorAll('a.highlight')
                .forEach(a => a.classList.remove('highlight'));
            //将此次点击的header目录进行添加高亮

            document.querySelector(`a#${header.id}`).classList.add('highlight')
        },
        // 给每一个a标签设置点击监听器,点击后触发高亮方法
        setClickListen() {
            const headers = document.querySelectorAll('a');
            headers.forEach(header => {
                // 如果是id为header开始的a标签,则设置监听
                if (header.id.startsWith('header')) {
                    //监听自己,如果被点击了,就触发函数,将自己丢过去
                    header.addEventListener('click', () => {
                        this.highlight(header)
                    })
                }
            })
        },
        // 防抖,
        // 为滚动后目录定位做准备,防止多次触发scroll事件
        // 类似于加一个冷却,在这个冷却阶段过后的值才为最终值
        // 而不是滚轮持续触发并且持续判断
        //默认冷却为100ms
        // 只有在用户停止输入或滚动等操作超过 delay 毫秒后,
        // 才会触发原始函数 fn 的执行。
        // 这样可以大大减少不必要的函数调用,从而提高性能。
        // 再举个例子,好比电梯要关闭上楼,此时不断进来人,电梯门不断的重新刷新关门时间
        // 你老板疯狂给你发任务,而你只听到了最后一件事,中间的事由于你每秒只能接收一件事
        // 中间说太快了超过一件事的被clearTimeOut了
        debounce(fn, delay = 100) {
            let timer = null;
            // 封装一个闭包,内部函数使用外部timer定时器,防止全局污染
            // 该闭包函数有debounce调用后返回
            // 且其返回值作为防抖函数给scroll事件多次调用
            // 同时该函数还能用到timer变量,防止变量名的外部占用
            // 好比开了一个小空间,将变量timer共享在所有调用debounce返回的新fn函数里
            // 绕这么大一圈就是为了不污染全局变量名
            return function (...args) {
                // 下次触发时定时器清空,冷却时间重置
                // 重新计时delay
                clearTimeout(timer);
                timer = setTimeout(() => {
                    // 调用需要防抖的函数fn
                    fn.apply(this, args);
                }, delay)
            }
        },

        handleScroll() {
            // 遍历所有item,获取其位置
            const rects=this.titlesDoms.map(titleDom => {
                // 将所有位置信息封装成新的数组
                return titleDom.getBoundingClientRect()
            })
            const range=200;
            // 每次滚动遍历判断每一个的位置是否满足条件
            for (let i = 0; i < this.titlesDoms.length; i++) {
                // 保存单个的位置和标签
                const titleDom=this.titlesDoms[i];
                const rect=rects[i];
                // console.log(titleDom,'距顶值',rect.top)
                // console.log(rects[i+1].top)
                // 判头顶法
                // 当top大于0时说明目录在视窗内,如果距顶小于范围则高亮
                if (rect.top >=0 && rect.top<=range){
                    this.highlight(titleDom)
                    // 结束循环,后续标签不用管位置了
                    break;
                }
                //判身体法.
                //判断下一个目录的距顶大于视口,也就是在视口外面,且在当前目录里面

                // 当dom在视口上时,且下一个值有值,下一个值的距顶大于了视口高度
                // 也就是说不一定非要距顶小于range,
                // 还要当下一个标题存在且在视口外也就是大于视口高度
                // 且当前的标签内容在视口上面,也就是说正在看标题内容时
                if (rect.top<0
                    &&rects[i+1]
                    &&rects[i+1].top>document.documentElement.clientHeight){
                    this.highlight(titleDom)
                    break
                }


            }
        },
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值