当前位置: 首页 > 工具软件 > t-scroll > 使用案例 >

vue muit-ui infinite-scroll源码解析

唐渊
2023-12-01

infinite scroll基本使用

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时的检查是否滚动到底部的判断处理等。

封一个不依赖vue的infinite-scroll简易库

封装后的代码如下:

(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这一个属性。

width="100%" height="500" src="//jsrun.net/5iYKp/embedded/all/light/" allowfullscreen="allowfullscreen">
 类似资料: