You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// 防抖:触发高频事件 n 秒后只会执行一次,如果 n 秒内事件再次触发,则会重新计时。functiondebounce(fn,delay=200){returnfunction(args){const_this=thisclearTimeout(fn.id)fn.id=setTimeout(function(){fn.apply(_this,args)},delay)}}
在说这个插件之前,可以先去 https://bubuzou.com/ 这个网站体验下这个插件的效果,好有个大概的印象。
通过阅读这篇文章,你可以收获什么?
JS
直接操作DOM
BOM
相关知识背景
平时我们在用
Markdown
写的文章,如果放到自己的博客或者投稿到掘金等平台,其内容会被浏览器解析成HTML
。而Markdown
里面的一级(#)、二级(##)、三级标题(###)分别对应于页面的文章标题、一级目录、二级目录。基于此,就可以很好的利用该生成的页面实现一个目录,便于更直观方便的的浏览文章内容,而这就是最初我想为博客里的文章页实现一个目录功能的理由。需求分析
位置
首先我打算将文章的目录放置在文章内容的右侧,且是悬浮固定在那里不随浏览器的滚动而滚动。因为我们都习惯从上到下从左到右的去浏览文章,所以我希望首先映入眼帘的是文章内容而不是文章目录;另外因为我的博客的首页和文章列表页都是左侧是浏览区域而右侧是操作区域,所以把目录放到右侧能保持整站的布局统一以及操作的便利性。
需要做什么
滚动目录的机制是怎样的
对于的第三点提到在合适的时候滚动目录,那什么时候才算合适呢?目录能不能滚动,以及怎么滚动总共分为以下 5 种情况:
功能实现
由于文章页是由
Hexo
基于Markdown
生成而来的,所以它有自己特定的HTML
格式,它的格式大概是如下这样:可以看到所有的二级标题(H2)和三级标题(H3)都是在同一个父元素下面,且每个标题下面都包含了一个带有
headerlink
类名的链接,另外还有标题属性。这个时候我们就能很容易的获取到所有的标题:
上面这串代码的结果会返回一个
NodeList
,有了它我们就可以去生成目录了。生成目录
生成目录无非就是产生一串
HTML
,除此之外还有哪些要做的呢?首先要确定下,目录都包含了哪些部分,如上图所示这里大概是包含了 3 部分:目录条、序号和标题。那么就可以先确定好
HTML
结构:其中,
arCatalog-line
表示目录条,arCatalog-body
是滚动区域,dl
是滚动列表,dd
是目录子项,arCatalog-index
是目录序号,链接里放的是子目录标题。有了HTML
,接下来要做的就是把目录的样式写好,写完后样式比较多,所以就不在这里贴出来了。生成目录到这里就完事了吗?并没有,由于浏览器可视区域是不固定的,所以我们需要计算出目录所在滚动区域的高度。
滚动高度 = n个目录子项 * 子项的实际高度
先说子项的实际高度,对于目录子项的样式上,我这里没有用内间距和外边距,而是通过
line-height
来控制他们之间的间隙,那么:子项的实际高度 = 子项的行高
再说
n
个目录子项,那到底n
是多少呢?在目录的Y
轴方向上,除了有目录,还有顶部的菜单,以及为了美观还需要适当的留白,所以:n = (视口高度 - (顶部菜单高度 + 留白高度))/ 子项行高
所以,最终我们可以计算出滚动高度:
完整的生成目录的函数代码如下:
设置滚动监听事件
给
window
加上滚动事件,用于监听当滚动的时候去做一些操作,这里的操作就是设置高亮和滚动目录。如上这样就能监听浏览器滚动事件,从而做一些事情啦。但是这样会导致函数被频繁调用,从而存在性能问题,其实我们更希望当滚动开始到滚动结束的时候,只执行一次函数即可,那这个直接上防抖即可:
然后我们只需要把
scroll
监听回调里的函数对应换成如下的即可:高亮当前目录
这部分内容开始前,我们先来复习一个
API
,该方法会返回元素的大小及其相对于视口的位置:需求分析的时候有提到,高亮的原则是当前标题所在的位置到浏览器可视区域顶部的距离需要小于或等于一个固定值:
所以当遍历
arContentAnchor
这个列表,某项的位置小于固定值,且差值最小的时候,该项对应的目录就应该被设置为高亮:到此一切都看起来很美好,但是上面这段代码存在性能问题,只要页面一滚动就会从第一个目录到最后一个目录之间进行查找,知道找到那个符合要求的为止,这样的话遍历次数太多了。
我们知道页面滚动无非就是在当前这个位置的前提下,往上滚动或者向下滚动,如果我们把
nextOnIndex
记为滚动前的索引,在根据滚动方向进行加加减减不就可以很好的减少遍历次数嘛?想法貌似不错,来试一下。首先我们要判断当前滚动是向上还是向下滚动,可以根据两次滚动前后的偏移量来判断:
向上滚动 = 滚动后偏移量 < 滚动前偏移量
知道了滚动方向,我们就可以很好的写出设置高亮的优化代码:
优化后的遍历次数明显减少,而且遍历次数基本上是小于或等于滚动前后目录索引的差值。
虽然经过优化后,已经明显的减少了遍历次数,但是我还想再优化一下。纳尼?
很多文章页很长,所以有回到顶部这种功能,试想一下,如果当前页面已经滚动到最底部,这个时候来一下回到顶部,那刚刚写的优化代码会遍历几次?答案是:遍历次数将会是目录子项的总数。文章最开始提到的那个体验地址的那篇文章 34条我能告诉你的Vue之实操篇 有 43 个子目录,所以需要遍历 43 次,真的不能接受结果,所以再来优化一次。
二次优化主要是处理边际问题,即滚动到头尾部的时候加上判断,最终二次优化后的高亮当前目录函数如下:
滚动目录
根据之前需求分析里的说明,我们可以知道当浏览器向下滚动的时候,会分成 3 种情况:
同理,当浏览器向上滚动的时候,也能很好的得出其滚动逻辑:
最终的滚动目录函数完整代码:
子目录点击事件
当点击子目录的时候需要做 2 件事情,第一是滚动页面到对应的目录位置,然后是高亮当前点击的目录;
滚动页面到对应的目录位置:
这样实现页面的滚动是没什么问题,就是体验不太好,突然从一个位置滚动到另外一个位置,显得突兀,能不能来点动画效果?类似
jQuery
的animate()
?没问题,我们来尝试着实现一下。用
JS
实现动画效果,一定离不开定时器,诸如setTimeout
、setInterval
之类,但是这次我不打算用他们,而是用HTML5
中增加的requestAnimationFrame
,这是一个专门为浏览器实现动画而提供的API
。它虽然也是个定时器,但是相比于另外两个,他不需要传递时间,因为传递的回调函数里自带了参数DOMHighResTimeStamp
,这个参数表示回调函数被触发的时间。除此之外,
requestAnimationFrame
中的回调函数执行次数通常是每秒60次,即大概每 16.6 毫秒执行一次回调函数,但在大多数遵循W3C
建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。好了,对于requestAnimationFrame
的介绍就到这里,下面我们直接来说下动画实现滚动的核心原理:每次滚动距离 = ( 滚动距离 / 动画持续时间 ) * 每次动画执行时间距离第一次执行时间的差值 + 当前滚动距离
直接来看完整的动画实现滚动的函数:
好了,现在有了动画函数,我们需要改写下子目录的点击事件,给滚动加上过渡效果,让人体验起来更加舒服:
第一件事已经做好,接下来做第二件事,高亮当前点击某个子目录:
如上即可,但是由于点击了某个子目录,页面会进行滚动,而页面滚动又会触发
setHighlight
函数对目录进行高亮,所以我这里的做法是用了一个全局变量hasStopSetHighlight
用来控制当点击子目录的时候,不进行setHighlight
设置高亮操作。浏览器视口高度变了怎么办
因为我们的滚动高度是根据浏览器视口高度计算出来的,如果浏览器视口高度变化了,那这个时候再去滚动页面,那肯定会出问题的。所以需要做的就是把和视口高度有关的逻辑抽离出来,统一放到一个函数里,当监听到视口高度变化的时候,再去执行这个函数。
先来写监听函数,同样用上了防抖函数处理:
然后去把相关逻辑抽离出来:
浏览器视口高度变化后,这里有个细节需要提一下,那就是滚动目录的
margin-top
以及高亮位置是希望变化的,所以我们需要使用全局变量进行提前保存起来,分别用到的全局变量是marginTop
和lastOnIndex
。组装
上面的代码都是把不同的功能点提取到函数里进行操作,看起来比较散乱,所以我们需要看看一个完整的目录插件应该是什么样子?
使用插件
然后在实际页面里使用的时候,只需要引入
articleCatalog.js
,然后直接用调用函数即可:当然调用的时候也支持传入一些参数,参数说明如下:
注意传入参数也是瞎传的,需要配合该插件的样式,否则容易程问题。比如明明页面中子目录的真实行高是
28px
,你却传入lineHeight: 24
,那肯定是不行的。The text was updated successfully, but these errors were encountered: