最近在项目中遇到一个需求,有一个列表需要滚动加载,类似于微博的无限滚动。当时第一反应时监听滚动事件,在判断滚动到达底部时加载下一页,同时心里也清楚,监听滚动事件需要做好截流。顺手搜索了下发现有一个现成的插件vue-infinite-scroll,用法也很简单,于是乎就用了起来。 需求上线后,对它的实现挺好奇的,于是研究了一番源码,这篇文章就是源码解析笔记。
插件使用方法
这是一个 vue 的指令,按照 github 仓库上的介绍,用法挺简单的,例如:
1 | <div class="app" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10"> |
1 | .app { |
1 | var app = document.querySelector('.app'); |
这里的指令宿主元素自身设置了overflow:auto
,内部元素用来支撑滚动,当滚动到底部时,增加内部元素的高度从而模拟了无限滚动。效果如下:
另外可以将宿主元素的父元素设置为滚动,当宿主元素滚动到父元素底部时,增加宿主元素的高度,模拟拉取下一页数据的操作。 例如:
1 | <div class="app"> |
达到的效果和上面完全相同。
源码解析
接下来就是看看内部怎么实现的。照例从入口开始看起。因为这个插件就是一个 vue
的指令,所以入口还是挺简单的:
指令入口
1 | export default { |
核心就是在宿主元素渲染后,执行doBind
方法,我们猜测会在doBind
绑定滚动父元素的scroll
事件。
isAttached
方法用于判断一个元素是否已渲染在页面上,判断方法是查看是否有组件元素的标签名为HTML
:
1 | // 判断元素是否已经在页面上 |
参数解析与事件绑定
现在看看doBind
方法,逻辑比较多,不过都不难。
1 | var doBind = function() { |
整个看下来,核心就是利用各种参数控制doCheck
的调用,包括时间间隔、disabled
、距离阈值、immediate-check
、组件事件。doCheck
因为会非常频繁的调用,所以用throttle
进行了截流,具体逻辑这里不再赘述。
在getScrollEventTarget
查找滚动父元素时,有一个细节就是会从自身开始查找,这也就是我们上面的demo
中可以将指令宿主元素自身设置为overflow:auto
的原因:
1 | // 从自身开始,寻找设置了滚动的元素。 overflow-y 为scroll或auto |
doCheck
这个函数用于判断是否已经滚动到底部,可以说是整个插件的核心逻辑。由于设置oveflow
的元素可以是宿主元素自身,也可以是宿主元素的某个父元素,所以判断会分成两个分支。
1 | var doCheck = function(force) { |
这里涉及到了多种尺寸值,包括scrollTop
、offsetTop
、clientHeight
、scrollHeight
等等,如果不清楚的话整个函数的逻辑就很难看懂,关于它们的具体意义可以参考我之前写的一篇博客。
这里我用两幅图来辅助理解上面的逻辑,相信会好懂很多。
宿主元素自身设置overflow
如下,我们的目标是判断元素是否已滚动到底部的距离阈值之内,很容易可以看出来,距离内容底部的距离公式为:
1 | const { scrollHeight, clientHeight, scrollTop } = scrollEventTarget; |
这也就是函数if
分支的逻辑,当currentDistance
小于distance
时,我们就可以加载下一页数据了。
宿主元素的父级元素设置overflow
此时就没有scrollTop
属性可以操作了,但是元素的高度仍然可以用上面的属性:滚动父元素的高度可以用scrollEventTarget.clientHeight
,子元素内容高度可以用element.offsetHeight
,剩下的就是计算topGap
了。
我们知道DOM
的坐标有两种:文档坐标、视口坐标,计算topGap
只要始终在其中一个坐标系计算就可以了,这里我们采用视口坐标。ele.getBoundingClientRect().top
可以知道一个元素距离视口顶部的距离,那么topGap
的计算公式就是:
1 | const topGap = scrollEventTarget.getBoundingClientRect().top - element.getBoundingClientRect().top; |
综上,子元素底部与父元素底部的距离公式就是:
1 | const currentDistance = |
这也就是函数的else
分支逻辑。
以上就是doCheck
的核心检测逻辑了,同时针对scrollEventTarget
为document
时做了一些特殊处理,留给大家自己去看。