InfiniteScroll

无限滚动使用的时候,引入相关文件,然后使用Vue.use()注册即可。

下面是infinite-scroll.js:

import InfiniteScroll from './directive';
import '@/ui/style/empty.scss';
import Vue from 'vue';

const install = function(Vue) {//install方法用于插件的安装
  Vue.directive('InfiniteScroll', InfiniteScroll);
  //为Vue注册自定义指令,这样只要在DOM元素上添加自定义指令即可启用无限滚动的功能
};

if (!Vue.prototype.$isServer && window.Vue) {
  window.infiniteScroll = InfiniteScroll;
  Vue.use(install); // eslint-disable-line
}

InfiniteScroll.install = install;
export default InfiniteScroll;

下面是directive.js:

import Vue from 'vue';
const ctx = '@@InfiniteScroll';

var throttle = function(fn, delay) {
  var now, lastExec, timer, context, args; //eslint-disable-line
  //now当期时间,lastExec上一次doCheck的时间,timer定时器
  var execute = function() {
    fn.apply(context, args);
    lastExec = now;
  };

  return function() {
    context = this;
    args = arguments;

    now = Date.now(); //当前时间

    if (timer) { //timer清空
      clearTimeout(timer);
      timer = null;
    }

    if (lastExec) {
      var diff = delay - (now - lastExec); //fn的执行,也就是doCheck的执行不能太频繁,如果之前执行过,那么得等200毫秒才能再次执行
      if (diff < 0) {
        execute();
      } else {
        timer = setTimeout(() => {
          execute();
        }, diff);
      }
    } else {
      execute();
    }
  };
};

var getScrollTop = function(element) { //获取滚动事件目标元素的垂直滚动距离
  if (element === window) {
    return Math.max(window.pageYOffset || 0, document.documentElement.scrollTop);
  }
  //window.pageYOffset就是window.scrollY,是文档在垂直方向已滚动的像素值

  return element.scrollTop;
};

var getComputedStyle = Vue.prototype.$isServer ? {} : document.defaultView.getComputedStyle;
//获取原生的Window.getComputedStyle()方法,document.defaultView返回document对应的window对象
//getComputedStyle方法返回元素的最终样式

var getScrollEventTarget = function(element) {
  var currentNode = element; //currentNode指令绑定的dom
  // bugfix, see http://w3help.org/zh-cn/causes/SD9013 and http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome
  while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
    //循环条件,元素不是html,不是body,nodeType为ELEMENT_NODE元素节点
    var overflowY = getComputedStyle(currentNode).overflowY; //获取dom元素的overflowY样式
    if (overflowY === 'scroll' || overflowY === 'auto') { //如果元素的overflowY样式是scroll或者auto,就返回此节点
      return currentNode;
    }
    currentNode = currentNode.parentNode; //如果overflowY的样式不对,就继续往父级寻找
  }
  return window; //找不到就返回window
};

var getVisibleHeight = function(element) { //返回元素可见高度,clientHeight
  if (element === window) {
    return document.documentElement.clientHeight;
  }

  return element.clientHeight;
};

var getElementTop = function(element) { //获取计算过的scrollTop,因为指令绑定元素不是事件目标元素会造成误差
  if (element === window) {
    return getScrollTop(window); //如果指令绑定的是window,就返回window的滚动距离
  }
  return element.getBoundingClientRect().top + getScrollTop(window); //如果指令绑定的不是window,就返回绑定元素的top值加上window的滚动距离
};

var isAttached = function(element) {
  //isAttached判断dom节点是否在html里,而不是片段节点里
  //element是指令绑定的dom节点
  var currentNode = element.parentNode; //指令绑定dom节点的父节点
  while (currentNode) {
    if (currentNode.tagName === 'HTML') { //如果指令绑定节点的父节点是HTML,就返回true
      return true;
    }
    if (currentNode.nodeType === 11) { //如果父节点类型是DOCUMENT_FRAGMENT_NODE,返回false
      return false;
    }
    currentNode = currentNode.parentNode; //再次寻找上一级父节点
  }
  return false;
};

var doBind = function() {
  if (this.binded) return; // eslint-disable-line
  //如果已经绑定过,就return
  this.binded = true;

  var directive = this; //el[ctx]
  var element = directive.el; //指令绑定的dom

  directive.scrollEventTarget = getScrollEventTarget(element); //获取overflowY样式为scroll或者auto的元素
  directive.scrollListener = throttle(doCheck.bind(directive), 200);
  directive.scrollEventTarget.addEventListener('scroll', directive.scrollListener);

  var disabledExpr = element.getAttribute('infinite-scroll-disabled'); //关闭无限滚动的标识,此值为true说明数据还没加载完
  var disabled = false;

  if (disabledExpr) {
    this.vm.$watch(disabledExpr, function(value) {
      //为组件实例添加侦听,关闭无限滚动的标识发生变化的时候触发,改变el[ctx].disabled的值
      directive.disabled = value;
      if (!value && directive.immediateCheck) { //如果有立即检查标识,就立即执行一次检查是否需要加载数据
        doCheck.call(directive);
      }
    });
    disabled = Boolean(directive.vm[disabledExpr]); //获取实例上对应的无线滚动标识,这个值一般在data里面
  }
  directive.disabled = disabled;

  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;

  var immediateCheckExpr = element.getAttribute('infinite-scroll-immediate-check');
  //是否立即检查标识
  var immediateCheck = true; //默认是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');
  //自定义触发事件名标识,设置后vue实例会监听此事件,如果触发了就会执行检查无线滚动是否需要加载数据
  if (eventName) {
    directive.vm.$on(eventName, function() {
      doCheck.call(directive);
    });
  }
};

var doCheck = function(force) {
  var scrollEventTarget = this.scrollEventTarget; //滚动事件目标元素
  var element = this.el; //指令绑定的dom元素
  var distance = this.distance; //距离

  if (force !== true && this.disabled) return; //eslint-disable-line
  //如果是disabled状态就return,说明数据正在loading
  var viewportScrollTop = getScrollTop(scrollEventTarget); //滚动事件目标元素的垂直滚动距离
  var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget);
  //事件目标元素的 垂直滚动距离 + 元素可见高度

  var shouldTrigger = false; //是否触发加载数据 标识

  if (scrollEventTarget === element) {
    //如果事件目标元素 就是 无线滚动指令绑定的元素
    shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance;
    //事件目标元素的内容高度 - 已经滚动的距离 - 元素可见高度 剩下的距离如果 小于等于 加载数据的滚动阈值,那么shouldTrigger标识就变为true
  } else {
    //如果事件目标元素 不是 指令绑定的元素
    var elementBottom = getElementTop(element) - getElementTop(scrollEventTarget) + element.offsetHeight + viewportScrollTop;
    //计算出 指令绑定元素和事件目标元素top值的误差 + 指令绑定元素的高度 + 事件目标元素的滚动距离
    shouldTrigger = viewportBottom + distance >= elementBottom;
    //事件目标元素的 垂直滚动距离 + 元素可见高度 + 阈值 大于等于 上面计算的elementBottom值,shouldTrigger标识就变为true
  }

  if (shouldTrigger && this.expression) {
    this.expression(); //加载数据
  }
};

export default {
  bind(el, binding, vnode) {
    //指令绑定后执行的钩子函数
    //el 指令所绑定的元素,可以用来直接操作 DOM
    //binding 一个对象,包含name指令名,value指令的绑定值,oldValue指令绑定的前一个值,expression字符串形式的指令表达式,arg传给指令的参数,modifiers一个包含修饰符的对象
    //vnode Vue编辑生成的虚拟节点

    //以下给指令绑定的元素添加@@InfiniteScroll属性,存储一些后面需要使用的参数
    el[ctx] = {
      el, //指令绑定的dom
      vm: vnode.context, //当前vnode所在的Vue实例
      expression: binding.value //指令的绑定值,也就是加载数据的方法
    };
    const args = arguments; //bind函数的参数
    var cb = function() {
      el[ctx].vm.$nextTick(function() { //所在的Vue实例完成DOM更新后执行
        if (isAttached(el)) { //isAttached判断dom节点是否在html里,而不是片段节点里
          doBind.call(el[ctx], args);
        }

        el[ctx].bindTryCount = 0;

        var tryBind = function() { //如果dom节点还不在html里,就不断尝试绑定,超过10次就不尝试了
          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实例mounted的时候调用cb
  },

  unbind(el) {
    //指令解绑时调用的钩子函数
    if (el[ctx] && el[ctx].scrollEventTarget) {//将绑定在scrollEventTarget上的scroll事件处理函数去除
      el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener);
    }
  }
};

无限滚动的原理就是容器有overflow-y:scroll的样式,当页面下拉到底部时,就触发加载新数据的方法。值得注意的是这其中给滚动的时候的事件处理函数中检查距离的时候添加了一个节流函数,滚动的时候每过200毫秒检查一次,以免检查的太频繁影响性能。

posted @ 2018-05-26 21:45  hahazexia  阅读(1615)  评论(0)    收藏  举报