Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infinite-scroll 无限滚动 #42

Open
niexia opened this issue Sep 10, 2019 · 0 comments
Open

Infinite-scroll 无限滚动 #42

niexia opened this issue Sep 10, 2019 · 0 comments
Assignees
Labels
effective performance optimization javascript javascript knowledge

Comments

@niexia
Copy link
Owner

niexia commented Sep 10, 2019

InfiniteScroll 无限滚动,也就是滚动到底部时,加载更多的数据。

实现原理

无限滚动的原理就是,用户滚动,当滚动条到底的时候就加载。所以很关键的一点是要知道滚动条和底部的距离。

先了解三个概念:

  • scrollHeight:只读属性,是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容。
  • scrollTop:可以获取或设置一个元素的内容垂直滚动的像素数。
  • clientHeight:只读属性,对于没有定义CSS或者内联布局盒子的元素为0,否则,它是元素内部的高度(单位像素),包含内边距,但不包括水平滚动条、边框和外边距。

javascript-InfiniteScroll-eg1

通过计算就可以的得到滚动条到底部的距离:

distance = Element.scrollHeight - Element.scrollTop - Element.clientHeight

element 的 InfiniteScroll 无限滚动

可以试一下如何使用 InfiniteScroll,然后 element 的源码是怎么实现的:

export default {
  name: 'InfiniteScroll',
  inserted(el, binding, vnode) {
    const cb = binding.value;

    const vm = vnode.context;
    // only include vertical scroll
    const container = getScrollContainer(el, true);
    const { delay, immediate } = getScrollOptions(el, vm);
    const onScroll = throttle(delay, handleScroll.bind(el, cb));

    el[scope] = { el, vm, container, onScroll };

    if (container) {
      container.addEventListener('scroll', onScroll);

      if (immediate) {
        const observer = el[scope].observer = new MutationObserver(onScroll);
        observer.observe(container, { childList: true, subtree: true });
        onScroll();
      }
    }
  },
  unbind(el) {
    const { container, onScroll } = el[scope];
    if (container) {
      container.removeEventListener('scroll', onScroll);
    }
  }
};

通过指令的方式来实现的,inserted 钩子在被绑定元素插入父节点时调用,首先通过 const container = getScrollContainer(el, true); 获取滚动的元素,这个方法定义在 element-ui/src/utils/dom

export const isScroll = (el, vertical) => {
  if (isServer) return;

  const determinedDirection = vertical !== null || vertical !== undefined;
  const overflow = determinedDirection
    ? vertical
      ? getStyle(el, 'overflow-y')
      : getStyle(el, 'overflow-x')
    : getStyle(el, 'overflow');

  return overflow.match(/(scroll|auto)/);
};

export const getScrollContainer = (el, vertical) => {
  if (isServer) return;

  let parent = el;
  while (parent) {
    if ([window, document, document.documentElement].includes(parent)) {
      return window;
    }
    if (isScroll(parent, vertical)) {
      return parent;
    }
    parent = parent.parentNode;
  }

  return parent;
};

从该元素开始循环判断父元素是否定义了 overflow 样式,来确定滚动容器 container

接下来就会监听滚动容器的滚动事件。滚动的时候就执行 onScroll 方法,为了优化性能,这个方法是节流函数 throttle 执行之后返回的,所以实际上执行的是 handleScroll。这个等会再分析。

先看下面的代码对 immediate 的判断,如果是 true,即立即执行,则创建一个 new MutationObserver(onScroll) 实例,并监听容器,内容改变之后会执行回调函数 onScroll

为什么这么处理呢?

因为无限滚动是通过滚动加载实现的,如果初始状态下内容无法撑满容器,就无法出现滚动条,那就会造成后面无法滚动加载数据了!这个默认就是 true,保证加载到出现滚动条。

const onScroll = throttle(delay, handleScroll.bind(el, cb));
// ...
if (container) {
  container.addEventListener('scroll', onScroll);

  if (immediate) {
    const observer = el[scope].observer = new MutationObserver(onScroll);
    observer.observe(container, { childList: true, subtree: true });
    onScroll();
  }
}

handleScroll 的实现如下:

const handleScroll = function(cb) {
  const { el, vm, container, observer } = this[scope];
  const { distance, disabled } = getScrollOptions(el, vm);

  if (disabled) return;

  const containerInfo = container.getBoundingClientRect();
  if (!containerInfo.width && !containerInfo.height) return;

  let shouldTrigger = false;

  if (container === el) {
    // be aware of difference between clientHeight & offsetHeight & window.getComputedStyle().height
    const scrollBottom = container.scrollTop + getClientHeight(container);
    shouldTrigger = container.scrollHeight - scrollBottom <= distance;
  } else {
    const heightBelowTop = getOffsetHeight(el) + getElementTop(el) - getElementTop(container);
    const offsetHeight = getOffsetHeight(container);
    const borderBottom = Number.parseFloat(getStyleComputedProperty(container, 'borderBottomWidth'));
    shouldTrigger = heightBelowTop - offsetHeight + borderBottom <= distance;
  }

  if (shouldTrigger && isFunction(cb)) {
    cb.call(vm);
  } else if (observer) {
    observer.disconnect();
    this[scope].observer = null;
  }

};

看一下主要流程,首先会判断 disabled,如果是 true 直接返回。可以结合官方实例,实际开发中,如果正在加载数据,那么就可以将 disabled 设置为 true,避免多次触发。

根据容器不同,判断方法有些差异:

  1. 如果滚动容器是绑定的元素本身,那就通过前面说明的那种方式:
Element.scrollHeight - Element.scrollTop - Element.clientHeight === 0

判断是否已经到底部了。这里是判断是否小于等于 distance,它提供了一个配置项,距离底部 distance 的时候就可以触发回调了。

const scrollBottom = container.scrollTop + getClientHeight(container);
shouldTrigger = container.scrollHeight - scrollBottom <= distance;
  1. 如果滚动容器不是元素本身,那判断就会麻烦一些:

javascript-InfiniteScroll-eg2

当鼠标往下滚的时候,el 就会向上,heightBelowTop - offsetHeight + borderBottom 其实就是 el 底部到 container 的距离,它和 distance 含义其实是一样的。

javascript-InfiniteScroll-eg3

如果满足条件就会执行回调 cb.call(vm)。同时,上面说过设置 immediate 会立即加载,加载完成之后移除 observer

if (shouldTrigger && isFunction(cb)) {
  cb.call(vm);
} else if (observer) {
  observer.disconnect();
  this[scope].observer = null;
}

参考

@niexia niexia added the javascript javascript knowledge label Sep 10, 2019
@niexia niexia self-assigned this Sep 10, 2019
@niexia niexia changed the title InfiniteScroll 无限滚动 Infinite-scroll 无限滚动 Sep 10, 2019
@niexia niexia added the effective performance optimization label Sep 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
effective performance optimization javascript javascript knowledge
Projects
None yet
Development

No branches or pull requests

1 participant