element-ui element-plus infinite-scroll - 分析

源代码地址 - infinite-scroll

version:element-plus 1.0.1-beta.0

infinite-scroll是指令,不是组件
源码中涉及很多原生js属性,可以参考这里,对照观看

import { nextTick } from 'vue'
import { isFunction } from '@vue/shared'
import throttle from 'lodash/throttle'
import { entries } from '@element-plus/utils/util'
import { getScrollContainer, getOffsetTopDistance } from '@element-plus/utils/dom'
import throwError from '@element-plus/utils/error'
import type { ObjectDirective, ComponentPublicInstance } from 'vue'

export const SCOPE = 'ElInfiniteScroll'
export const CHECK_INTERVAL = 50
export const DEFAULT_DELAY = 200
export const DEFAULT_DISTANCE = 0

//可以传入的属性
const attributes = {
  // 节流时延,单位为ms
  delay: {
    type: Number,
    default: DEFAULT_DELAY,
  },
  // 触发加载的距离阈值,单位为px
  distance: {
    type: Number,
    default: DEFAULT_DISTANCE,
  },
  disabled: {
    type: Boolean,
    default: false,
  },
  // 是否立即执行加载方法,以防初始状态下内容无法撑满容器。
  immediate: {
    type: Boolean,
    default: true,
  },
}

type Attrs = typeof attributes
type ScrollOptions = { [K in keyof Attrs]: Attrs[K]['default'] }
type InfiniteScrollCallback = () => void
type InfiniteScrollEl = HTMLElement & {
  [SCOPE]: {
    container: HTMLElement | Window
    containerEl: HTMLElement
    instance: ComponentPublicInstance
    delay: number // export for test
    lastScrollTop: number
    cb: InfiniteScrollCallback
    onScroll: () => void
    observer?: MutationObserver
  }
}

const getScrollOptions = (el: HTMLElement, instance: ComponentPublicInstance): ScrollOptions => {
  return entries(attributes)
    .reduce((acm, [name, option]) => {
      // option就是attributes中的每个对象
      // 这里type 也是里面的 构造函数
      const { type, default: defaultValue } = option
      const attrVal = el.getAttribute(`infinite-scroll-${name}`)
      let value = instance[attrVal] ?? attrVal ?? defaultValue
      value = value === 'false' ? false : value
      // 这里直接用构造函数转换类型
      value = type(value)
      acm[name] = Number.isNaN(value) ? defaultValue : value
      return acm
    }, {} as ScrollOptions)
}

const destroyObserver = (el: InfiniteScrollEl) => {
  const { observer } = el[SCOPE]

  if (observer) {
    observer.disconnect()
    delete el[SCOPE].observer
  }
}
// scroll 方法
const handleScroll = (el: InfiniteScrollEl, cb: InfiniteScrollCallback) => {
  const {
    container, containerEl,
    instance, observer,
    lastScrollTop,
  } = el[SCOPE]
  const { disabled, distance } = getScrollOptions(el, instance)
  const { clientHeight, scrollHeight, scrollTop } = containerEl
  // 比较差值
  const delta = scrollTop - lastScrollTop

  el[SCOPE].lastScrollTop = scrollTop

  // trigger only if full check has done and not disabled and scroll down
  if (observer || disabled || delta < 0 ) return

  let shouldTrigger = false

  if (container === el) {
    // 判断是否小于设置的距离阈值
    shouldTrigger = scrollHeight - (clientHeight + scrollTop) <= distance
  } else {
    // get the scrollHeight since el might be visible overflow
    const { clientTop, scrollHeight: height } = el
    const offsetTop = getOffsetTopDistance(el, containerEl)
    shouldTrigger = scrollTop + clientHeight >= offsetTop + clientTop + height - distance
  }

  if (shouldTrigger) {
    cb.call(instance)
  }
}

function checkFull(el: InfiniteScrollEl, cb: InfiniteScrollCallback) {
  const { containerEl, instance } = el[SCOPE]
  const { disabled } = getScrollOptions(el, instance)

  if (disabled) return
  // 可滚动高度 <= 容器高度  那么直接调用用户自定的cb
  if (containerEl.scrollHeight <= containerEl.clientHeight) {
    cb.call(instance)
  } else {
    destroyObserver(el)
  }
}

const InfiniteScroll: ObjectDirective<InfiniteScrollEl, InfiniteScrollCallback> = {
  // 指令 https://vue3js.cn/docs/zh/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0
  async mounted(el, binding) {
    // 指令 api 文档  https://vue3js.cn/docs/zh/api/application-api.html#directive
    // instance:使用指令的组件实例。
    // value:传递给指令的值。例如,在 v-my-directive="1 + 1" 中,该值为 2。
    const { instance, value: cb } = binding
    // 要求 v-infinite-scroll 绑定的值是函数
    if (!isFunction(cb)) {
      throwError(SCOPE, '\'v-infinite-scroll\' binding value must be a function')
    }

    // ensure parentNode mounted
    await nextTick()

    // 取到 属行中定义的 infinite-scroll-delay 和 infinite-scroll-immediate
    const { delay, immediate } = getScrollOptions(el, instance)
    const container = getScrollContainer(el, true)
    const containerEl = container === window ? document.documentElement : (container as HTMLElement)
    const onScroll = throttle(handleScroll.bind(null, el, cb), delay)

    if (!container) return

    el[SCOPE] = {
      instance, container, containerEl,
      delay, cb, onScroll,
      lastScrollTop: containerEl.scrollTop,
    }

    if (immediate) {
      // https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
      const observer = new MutationObserver(throttle(checkFull.bind(null, el, cb), CHECK_INTERVAL))
      el[SCOPE].observer = observer
      observer.observe(el, { childList: true, subtree: true })
      checkFull(el, cb)
    }

    container.addEventListener('scroll', onScroll)
  },
  unmounted(el) {
    const { container, onScroll } = el[SCOPE]

    container?.removeEventListener('scroll', onScroll)
    destroyObserver(el)
  },
}

export default InfiniteScroll

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值