vue的mint-ui的infinite scroll的基本使用地址:infinite-scroll接入指南.
简单解释一下:
1、指令接受的method:处理loadmore回调
2、自定义属性infinite-scroll-disabled:为false时:不会进行是否到达底部的判断,因此就触发不了loadmore回调
3、自定义属性infinite-scroll-distance:doCheck通过此distance来判断是否到达底部
4、自定义属性infinite-scroll-immediate-check: 是否在绑定到Vue上面就立刻执行 check函数
5、自定义属性infinite-scroll-listen-for-event: 通过Vue.$on()来注册此事件,此事件的回调函数是 当前指令定义回调函数(也就是loadmore)
找到mint-ui源码的infinite-scroll目录,找到它的入口文件index.js,其代码如下:
import 'mint-ui/src/style/empty.css'; // 需要的css文件
export { default } from './src/infinite-scroll.js'; // 核心代码
infinite-scroll.js的代码如下:
import InfiniteScroll from './directive';
import 'mint-ui/src/style/empty.css';
import Vue from 'vue';
//当前 infinite-scroll的 install方法,用于Vue开发插件使用,在Vue.use()里面调用此对象的install函数,在安装此插件的时候,注册当前指令
const install = function(Vue) {
Vue.directive('InfiniteScroll', InfiniteScroll);//注册指令
};
//已经在初始化的时候,帮我们调动了Vue.use()
if (!Vue.prototype.$isServer && window.Vue) {
window.infiniteScroll = InfiniteScroll;
Vue.use(install); // eslint-disable-line
}
InfiniteScroll.install = install;
export default InfiniteScroll;
通过上面得知InfiniteScroll代码在./directive.js里面,查看它的入口函数如下:
export default {
// 指令绑定的钩子函数
bind(el, binding, vnode) {
// 为当前el添加我们需要使用的属性,它的value就是我们后面操作所需的对象
el[ctx] = {
el, // 当前指令绑定的dom节点
vm: vnode.context, // 当前vNode所在的Vue实例
expression: binding.value
};
const args = arguments;
// 当前Vue实例挂载到到dom上之后执行的回调函数
var cb = function() {
//在此次事件循环完成dom相关更新之后,执行当前指令相关业务
el[ctx].vm.$nextTick(function() {
if (isAttached(el)) {// 当前dom在html标签里面 当前dom不在 documentFragment里面
doBind.call(el[ctx], args);
}
el[ctx].bindTryCount = 0;
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();
});
};
if (el[ctx].vm._isMounted) {
cb();
return;
}
el[ctx].vm.$on('hook:mounted', cb);// 如果阅读过vue源码, Vue通过callHook()调用其相关生命周期方法,此时也会调用通过hook注册的回调钩子函数
},
//当前指令解绑定的回调钩子函数
unbind(el) {
//将当前scroll view的 scroll事件remove掉
if (el[ctx] && el[ctx].scrollEventTarget) {
el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener);
}
}
};
通过上面代码的逻辑,我们可知:在当前指令绑定到dom上之后,在dom更新后执行doBind()核心代码。下面来看doBind()代码
var doBind = function() {
//执行过一次了(绑定过了), 直接返回,不继续执行
if (this.binded) return; // eslint-disable-line
this.binded = true;
var directive = this;
var element = directive.el; // 指令绑定的dom节点
directive.scrollEventTarget = getScrollEventTarget(element);//获取滚动的dom
directive.scrollListener = throttle(doCheck.bind(directive), 200); // 节流函数, 时间间隔为200ms
directive.scrollEventTarget.addEventListener('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;
if (!value && directive.immediateCheck) {
doCheck.call(directive);
}
});
disabled = Boolean(directive.vm[disabledExpr]);
}
directive.disabled = disabled;
//infinite-scroll-distance, 注意:只能传递数字或者vm中的data或props数据
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;
//infinite-scroll-immediate-check: 是否立即检查,注意:这个数据的值,只能通过Vue中的data或props中获取
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);
}
//infinite-scroll-listen-for-event的处理
var eventName = element.getAttribute('infinite-scroll-listen-for-event');
if (eventName) {
directive.vm.$on(eventName, function() {
doCheck.call(directive);
});
}
};
上面的逻辑不是太难,基本上就是搜索出当前页面的scroll view,并未当前scrollview注册onscroll的钩子函数,并且对滚动的回调函数就行了节流优化策略;以及对自定义属性的处理。
节流函数是滚动优化中的一个比较常用的优化点,基本原理:保证滚动的回调里面的处理逻辑在>=200ms的时间间隔调用一次(基本上控制在200ms)。
节流函数代码如下:
var throttle = function(fn, delay) {
var now, lastExec, timer, context, args;// now:当前的时间; lastExec:上次执行的时间; timer: 记录timeout的id; context: fn执行的上下文作用域;args:函数执行传递的参数
//scroll回调函数真正执行的核心函数
var execute = function() {
fn.apply(context, args);
lastExec = now;
};
//闭包函数(绑定到scroll事件上的回调函数)
return function() {
context = this;
args = arguments;
now = Date.now();
if (timer) {
clearTimeout(timer);
timer = null;
}
if (lastExec) {
//判断是否超过指定时间间隔,超过则执行
var diff = delay - (now - lastExec);
if (diff < 0) {
execute();
} else {
timer = setTimeout(() => {
execute();
}, diff);
}
} else {
execute();
}
};
};
还有就是doCheck()函数,其内部的逻辑就是检查:当前滚动view的scrollHeight - 滚动底部到达的位置 <= 我们上面设置的distance。他内部最好做的最好就是针对scrollview是当前绑定指令的dom和其外层的dom的进行了容错处理。
1、infinite-scroll是通过Vue中的指令实现的。
2、针对滚动回调事件的处理增加了节流的优化。
3、针对不同情况作了容错处理,包括自己寻找scrollView,scrollView是不同dom时的检查是否滚动到底部的判断处理等。
封装后的代码如下:
(function () {
window.infiniteScroll = function(selector, cb, options) {
if(!selector || !cb || !document.querySelector(selector)){
console.error("the scroll element and the callback function can't be null");
return;
}
var obj = {
ele: selector,
callback: cb
};
var initOptions = function(options) {
var opts = options || {};
opts = typeof opts === 'object' ? opts : {};
opts.disabled = Boolean(opts.disabled);
opts.distance = Number(opts.distance);
opts.checkImmediate = Boolean(opts.checkImmediate);
return opts;
}
var initScroll = function(obj) {
obj.scrollTarget = getScrollTarget(document.querySelector(obj.ele));
obj.scrollListener = throttle(doCheck.bind(obj), 200);
obj.scrollTarget.addEventListener('scroll', obj.scrollListener, false);
}
var getScrollTarget = function(ele) {
var currentNode = ele;
while (currentNode && currentNode.targetName !== 'body' && currentNode.targetName !== 'html' && currentNode.nodeType === 1){
var overflowY = document.defaultView.getComputedStyle(currentNode, 'overflowY');
if(overflowY === 'scroll' || overflowY === 'auto'){
return currentNode;
}
currentNode = currentNode.parentNode;
}
return window;
}
//节流函数
var throttle = function(fn, delay) {
var now, lastExec, timer, context, args;
var handle = function () {
fn.call(context, args);
lastExec = now;
}
return function () {
context = this;
args = arguments;
if(timer){
clearTimeout(timer);
timer = null;
}
now = new Date();
if(lastExec){
if(now - lastExec > delay){
handle();
}else {
timer = setTimeout(handle, (delay- (now-lastExec)));
}
}else {
handle();
}
}
}
var doCheck = function() {
var scrollTarget = this.scrollTarget;
var element = document.querySelector(this.ele);
var distance = this.options.distance;
if(this.options.disabled){
return;
}
var triggered = false; // 是否触发 回调(符合check的条件)
var viewportScrollTop = getScrollTop(scrollTarget); // 获取滚动对象的 scrollTop
var viewportBottom = viewportScrollTop + getClientHeight(scrollTarget);// 获取当前滚动对象的滚动底部的位置
if(scrollTarget === element){
triggered = scrollTarget.scrollHeight - viewportBottom <= distance;
}else {
//计算当前element的底部的高度
var elementBottom = getElementTop(element) - getElementTop(scrollTarget) + viewportScrollTop + element.clientHeight;
triggered = elementBottom - viewportBottom <= distance;
}
if(triggered && this.callback){// 触发回调
//创建给用户一个代理的options给使用者, 这个options只暴露了 disabled属性
if(!this.cbOptions){
this.cbOptions = createCallbackOptions(this.options);
}
this.callback(this.cbOptions);
}
}
var createCallbackOptions= function (options) {
var cbOptions = {};
Object.defineProperty(cbOptions, 'disabled', {
get: function () {
return options.disabled;
},
set: function (val) {
options.disabled = val;
}
});
return cbOptions;
};
//获取当前element相对于 窗口顶部的距离
var getScrollTop = function(element) {
if(element === window){
return Math.max(window.pageYOffset || 0 , document.documentElement.scrollTop);
}
return element.scrollTop;// 当前dom滚动的高度, 如果当前滚动的不是当前dom(比如window),他就是0
}
var getClientHeight = function(element) {
if(element === window){
return document.documentElement.clientHeight;
}
return element.clientHeight;
}
var getElementTop = function(element) {
if(element === window){
return getScrollTop(window);
}
return element.getBoundingClientRect().top + getScrollTop(window);
}
// init options
obj.options = initOptions(options);
// init scroll
initScroll(obj);
//立刻进行检查,防止首次加载,没有加载数据,无法滚动,无法进行后续操作
if(obj.options.checkImmediate){
doCheck.call(obj);
}
}
})();
使用方法:
只需要调用infiniteScroll(element, cb, options)函数,它被附加到window上面了,它接受三个参数:
selector(string) : 当前功能应用的dom节点的selector(内部通过querySelector()获取),也就是存储滚动内容的dom节点;同上面指令绑定的dom
cb(function) : 触发的回调函数(也就是上面指令绑定的回调函数)
options(object) : 配置项, 可配置选项有:
disabled:如果为true,当前不能检查是否滑动到底部
distance : 距离底部多少距离符合触发回调的条件
checkImmediate: 是否在执行此方法的时候就立刻执行检查操作。(防止初始的时候没有加载数据,造成数据不够而触发不了后面的滚动事件的回调)。
注: 第一个参数不能传递dom对象,因为此dom存储起来,后面获取此dom的offsetHeight和clientHeight,获取不到,需要重新获取dom节点。并且两次的dom对象是不相等,也就是它们的引用地址是不同的。
问题: 为什么mint-ui中的infinite-scroll中存储dom对象就没问题呢,猜测:因为它获取的vue中的el对象,我猜测这个应该是vue中的vNode,所以是唯一的。后面阅读vue的渲染部分源码再来解释这个问题。
使用示例如下:
window.infiniteScroll(scrollWrapper, function (options) {
options.disabled = self.loading = true;
setTimeout(function () {
var last = self.list[self.list.length -1];
for (var i =1; i<= 10; i++){
self.list.push(last + i);
}
options.disabled = self.loading = false;
}, 2500);
}, {
disabled: false,
distance: 50,
checkImmediate: true
})
注意到上面处理loadmore回调函数的时候,传递了一个options的参数,其实我们只需要disabled这个参数(用来决定是是否可以执行doCheck()操作);这里传递了一个对象,是因为要内部要接收到disabled的修改。
注意:回调函数中的参数options并不是用户传递进去的options对象,也不是内部使用的options对象,他只是一个options的代理对象,对外只公开了disabled这一个属性。