滚动加载--优化滚动性能

hello~

背景

  • 最近有用阿里云 sdk 中的日志服务,在它的 api 里发现它并不支持分页查询,倒是有个 offset 指定日志起点,所以需要配合鼠标滚动做一个懒加载。

思路

  1. 最容易想到的就是做一个底部检测,当用户滚动到底部时再请求下一部分内容,并将结果拼接到上一次结果之后。
  2. 但很多情况下返回内容不只有字符,还有媒体文件,这个时候浏览器的下载通道很容易被占满。这个时候 我们实际需要的其实是用户看得到的内容去做加载就可以,用户看不到的可以先用墓碑(占位符)。这么做可以大大减少流量浪费~
  3. 理清思路后大致实现不难,不过滚动是个高频发事件,这在一些极端 case 下容易引起卡顿,需要进行优化,这也是本文重点。

优化滚动

  • 常用两种优化方式
  1. Throttle: 允许我们限制激活响应的数量。通过限制每秒回调的数量的方式来达到优化目的;
// const fn = throttle(e => console.log(e, "123"), 300)
// const scroll = listen(document.body, "mousewheel", fn)
function throttle(...props) {
  const [fn, threshhold = 250, scope] = props
  let last
  let deferTimer
  return event => {
    const context = scope || this
    const now = +new Date()
    if (last && now < last + threshhold) {
      clearTimeout(deferTimer)
      deferTimer = setTimeout(() => {
        // 停止滚动延迟后触发
        last = now
        fn.apply(context, [event, ...props])
      }, threshhold)
    } else {
      last = now
      fn.apply(context, [event, ...props])
    }
  }
}
  1. Debounce: 事件发生时,不会立即激活回调。而是等待一定的时间并检查相同的事件是否再次触发。如果是,我们重置定时器,并再次等待。如果在等待期间没有发生相同的事件,我们就立即激活回调。 => 使用于滚动结束后执行操作
// 基本防抖
function debounce(...props) {
  const [action, wait = 200] = props
  let last
  return function(event) {
    const ctx = this
    clearTimeout(last)
    last = setTimeout(function() {
      action.apply(ctx, [event, ...props])
    }, wait)
  }
}
// lodash 实现
import { isObject, isFn } from "./is"

/**
 * 截流函数
 *
 * @param {function} fn
 * @param {number} [wait=250]
 * @param {object} options
 * @returns
 */
function throttle(func, wait = 250, options) {
  let leading = true
  let trailing = true

  if (isObject(options)) {
    leading = "leading" in options ? !!options.leading : leading
    trailing = "trailing" in options ? !!options.trailing : trailing
  }
  return debounce(func, wait, {
    leading,
    trailing,
    maxWait: wait
  })
}

/**
 * 防抖函数
 *
 * @param {function} func
 * @param {*} wait
 * @param {object} options
 *    - leading:Boolean 开始时调用
 *    - trailing:Boolean 结束时调用
 *    - maxWait:Number 最大等待时间
 * @returns
 */
function debounce(func, wait, options) {
  let lastArgs, lastThis, maxWait, result, timerId, lastCallTime

  let lastInvokeTime = 0
  let leading = false
  let maxing = false
  let trailing = true
  const isBrowser = typeof window == "object" && window !== null
  // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
  const useRAF =
    !wait &&
    wait !== 0 &&
    isBrowser &&
    typeof window.requestAnimationFrame === "function"

  if (!isFn(func)) {
    throw new TypeError("Expected a function")
  }
  wait = +wait || 0
  if (isObject(options)) {
    leading = !!options.leading
    maxing = "maxWait" in options
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
    trailing = "trailing" in options ? !!options.trailing : trailing
  }
  function invokeFunc(time) {
    const args = lastArgs
    const thisArg = lastThis

    lastArgs = lastThis = undefined
    lastInvokeTime = time
    result = func.apply(thisArg, args)
    return result
  }

  function startTimer(pendingFunc, wait) {
    if (useRAF) {
      window.cancelAnimationFrame(timerId)
      return window.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }

  function cancelTimer(id) {
    if (useRAF) {
      return window.cancelAnimationFrame(id)
    }
    clearTimeout(id)
  }

  function leadingEdge(time) {
    lastInvokeTime = time
    timerId = startTimer(timerExpired, wait)
    return leading ? invokeFunc(time) : result
  }

  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const timeWaiting = wait - timeSinceLastCall

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
  }

  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime

    // 第一次调用 || 上一次已经停止 || 系统时间倒退 || 已经达到max限制
    return (
      lastCallTime === undefined ||
      timeSinceLastCall >= wait ||
      timeSinceLastCall < 0 ||
      (maxing && timeSinceLastInvoke >= maxWait)
    )
  }

  function timerExpired() {
    const time = Date.now()
    if (shouldInvoke(time)) {
      // 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
      return trailingEdge(time)
    }
    // Restart the timer.
    timerId = startTimer(timerExpired, remainingWait(time))
  }

  function trailingEdge(time) {
    timerId = undefined

    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }

  function cancel() {
    if (timerId !== undefined) {
      cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(Date.now())
  }

  function pending() {
    return timerId !== undefined
  }

  function debounced(...args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time)

    lastArgs = args
    lastThis = this
    lastCallTime = time

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
      if (maxing) {
        timerId = startTimer(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}

export { throttle, debounce }

判断到达底部

  • scrollHeight <=> scrollTop + offsetHeight
    对于预加载可以适当加个预加载高度 这样提前加载会更加流畅

dom 回收

由于每一个节点都会增加一些额外的内存、布局、样式和绘制。进行 DOM 回收可以使 dom 保持在一个比较低的数量上,进而加快上面提到的这些处理过程。在 dom 移除页面一定高度后对节点进行 move() 操作。为了保证滚动条的正确比例和防止高度塌陷,需要显示的声明高度。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值