滚动后目录变化效果
点击后目录变化效果
步骤
生成目录列表
- 找到存放所有html标签的 dom,设置ref。后续获取到该标签
-
定义层级,H1 H2 H3 三层即可
-
定义空数组存放所有目录对象
-
遍历content内容dom根节点 如果符合H1 H2 H3这三个标签则 设置id为header-0 header-2…
-
中途存一份标签目录到titlesDoms,后续遍历获取其位置来 根据视口位置高亮目录标签
-
push目录对象 有id为header打头,后续监听所有a标签以header打头的点击事件,点击后触发方法传递该值找到该a标签设置样式,且样式颜色为!important否则无法覆盖高亮,高亮时移除所有 a标签的highlight样式类,初始化
-
防抖函数,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
}
}
},