详解无限滚动插件vue

最近在项目中遇到一个需求,有一个列表需要滚动加载,类似于微博的无限滚动。当时第一反应时监听滚动事件,在判断滚动到达底部时加载下一页,同时心里也清楚,监听滚动事件需要做好截流。顺手搜索了下发现有一个现成的插件vue-infinite-scroll ,用法也很简单,于是乎就用了起来。 需求上线后,对它的实现挺好奇的,于是研究了一番源码,这篇文章就是源码解析笔记。

插件使用方法

这是一个 vue 的指令,按照 github 仓库上的介绍,用法挺简单的,例如:

<div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10"> <div></div> <div v-show="busy">loading.....</div> </div>

.app { height: 1000px; border: 1px solid red; width: 600px; margin: 0 auto; overflow: auto; } .content { height: 1300px; background-color: #ccc; width: 80%; margin: 0 auto; } .loading { font-weight: bold; font-size: 20px; color: red; text-align: center; }

var app = document.querySelector('.app'); new Vue({ el: app, directives: { InfiniteScroll, }, data: function() { return { busy: false }; }, methods: { loadMore: function() { var self = this; self.busy = true; console.log('loading... ' + new Date()); setTimeout(function() { var target = document.querySelector('.content'); var height = target.clientHeight; target.style.height = height + 300 + 'px'; console.log('end... ' + new Date()); self.busy = false; }, 1000); }, }, });

这里的指令宿主元素自身设置了 overflow:auto ,内部元素用来支撑滚动,当滚动到底部时,增加内部元素的高度从而模拟了无限滚动。效果如下:

详解无限滚动插件vue

另外可以将父元素设置为滚动,当自身滚动到父元素底部时,增加自身的高度,模拟拉取下一页数据的操作。 例如:

<div> <div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10"></div> <div v-show="busy">loading.....</div> </div>

达到的效果和上面完全相同。

源码解析

接下来就是看看内部怎么实现的。照例从入口开始看起。因为这个插件就是一个 vue 的指令,所以入口还是挺简单的:

指令入口

export default { bind(el, binding, vnode) { el[ctx] = { el, vm: vnode.context, expression: binding.value, // 滚动到底部时需要的监听函数,通常用于加载下一页数据 }; const args = arguments; // 监听宿主元素所在组件的mounted事件 el[ctx].vm.$on('hook:mounted', function() { el[ctx].vm.$nextTick(function() { // 判断元素是否已经在页面上 if (isAttached(el)) { // 获取各项指令相关属性,执行各种事件绑定 doBind.call(el[ctx], args); } el[ctx].bindTryCount = 0; // 间隔50ms轮训10次,判断元素是否已经在页面上 var tryBind = function() { if (el[ctx].bindTryCount > 10) return; //eslint-disable-line el[ctx].bindTryCount++; if (isAttached(el)) { doBind.call(el[ctx], args); } else { setTimeout(tryBind, 50); } }; tryBind(); }); }); }, unbind(el) { // 事件解绑 if (el && el[ctx] && el[ctx].scrollEventTarget) el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener); }, };

核心就是在宿主元素渲染后,执行 doBind 方法,我们猜测会在 doBind 绑定滚动父元素的 scroll 事件。

isAttached 方法用于判断一个元素是否已渲染在页面上,判断方法是查看是否有组件元素的标签名为 HTML :

// 判断元素是否已经在页面上 var isAttached = function(element) { var currentNode = element.parentNode; while (currentNode) { if (currentNode.tagName === 'HTML') { return true; } // 11 表示DomFragment if (currentNode.nodeType === 11) { return false; } currentNode = currentNode.parentNode; } return false; };

参数解析与事件绑定

现在看看 doBind 方法,逻辑比较多,不过都不难。

var doBind = function() { if (this.binded) return; // 只绑定一次 this.binded = true; var directive = this; var element = directive.el; // throttleDelayExpr: 截流间隔。 设置在元素的属性上 var throttleDelayExpr = element.getAttribute('infinite-scroll-throttle-delay'); var throttleDelay = 200; if (throttleDelayExpr) { // 优先尝试组件上的throttleDelayExpr属性值, 如 <div infinite-scroll-throttle-delay="myDelay"></div> throttleDelay = Number(directive.vm[throttleDelayExpr] || throttleDelayExpr); if (isNaN(throttleDelay) || throttleDelay < 0) { throttleDelay = 200; } } directive.throttleDelay = throttleDelay; // 监听滚动父元素的scroll时间,监听函数设置了函数截流 directive.scrollEventTarget = getScrollEventTarget(element); // 设置了滚动的父元素 directive.scrollListener = throttle(doCheck.bind(directive), directive.throttleDelay); directive.scrollEventTarget.addEventListener('scroll', directive.scrollListener); this.vm.$on('hook:beforeDestroy', function() { directive.scrollEventTarget.removeEventListener('scroll', directive.scrollListener); }); // infinite-scroll-disabled: 是否禁用无限滚动 // 可以为表达式 var disabledExpr = element.getAttribute('infinite-scroll-disabled'); var disabled = false; if (disabledExpr) { this.vm.$watch(disabledExpr, function(value) { directive.disabled = value; // 当disable为false时,重启check if (!value && directive.immediateCheck) { doCheck.call(directive); } }); disabled = Boolean(directive.vm[disabledExpr]); } directive.disabled = disabled; // 宿主元素到滚动父元素底部的距离阈值,小于这个值时,触发listen-for-event监听函数 var distanceExpr = element.getAttribute('infinite-scroll-distance'); var distance = 0; if (distanceExpr) { distance = Number(directive.vm[distanceExpr] || distanceExpr); if (isNaN(distance)) { distance = 0; } } directive.distance = distance; // immediate-check:是否在bind后立即检查一遍,也会在disable失效时立即触发检查 var immediateCheckExpr = element.getAttribute('infinite-scroll-immediate-check'); var immediateCheck = true; if (immediateCheckExpr) { immediateCheck = Boolean(directive.vm[immediateCheckExpr]); } directive.immediateCheck = immediateCheck; if (immediateCheck) { doCheck.call(directive); } // 当组件上设置的此事件触发时,执行一次检查 var eventName = element.getAttribute('infinite-scroll-listen-for-event'); if (eventName) { directive.vm.$on(eventName, function() { doCheck.call(directive); }); } };

整个看下来,核心就是利用各种参数控制 doCheck 的调用,包括时间间隔、 disabled 、距离阈值、 immediate-check 、组件事件。

doCheck 因为会非常频繁的调用,所以用 throttle 进行了截流,具体逻辑这里不再赘述。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/ddd72b8b768dff007dd573ba412b2e4f.html