有关虚拟滚动 以及 懒加载的原理

懒加载实现方式及底层原理

一、浏览器的底层渲染机制:

1、构建 DOM 树
2、样式计算
3、布局阶段
4、分层
5、绘制
6、分块
7、光栅化
8、合成

二、几个API

1、document.documentElement.clientHeight
获取屏幕可视区域的高度。
在这里插入图片描述
2、element.offsetTop
获取元素相对于文档顶部的高度。
在这里插入图片描述
3、document.documentElement.scrollTop
获取浏览器窗口顶部与文档顶部之间的距离,也就是滚动条滚动的距离。

4、如果满足offsetTop-scroolTop<clientHeight,则图片进入了可视区内,我们就去请求进入可视区域的图片。
在这里插入图片描述

三、getBoundingClientRect()

该函数返回一个rectObject对象,该对象有 6 个属性:top, left, bottom, right, width, height;这里的top、left和css中的理解很相似,width、height是元素自身的宽高,但是right,bottom和css中的理解有点不一样。right是指元素右边界距窗口最左边的距离,bottom是指元素下边界距窗口最上面的距离。
在这里插入图片描述

通过这个 API,我们就很容易获取img元素相对于视口的顶点位置rectObject.top,只要这个值小于浏览器的高度window.innerHeight就说明进入可视区域:

function isInSight(el){
    
    
  const bound = el.getBoundingClientRect();
  const clientHeight = window.innerHeight;
  return bound.top <= clientHeight;
}

四、基于 IntersectionObserver 实现图片懒加载

有一个新的 IntersectionObserver API,可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做交叉观察器。

const imgs = document.querySelectorAll('img') //获取所有待观察的目标元素
var options = {
    
    }
function lazyLoad(target) {
    
    
  const observer = new IntersectionObserver((entries, observer) => {
    
    
    entries.forEach(entrie => {
    
    
      if (entrie.isIntersecting) {
    
    
        const img = entrie.target;
        const src = img.getAttribute('data-src');
        img.setAttribute('src', src)
        observer.unobserve(img); // 停止监听已开始加载的图片
      }

    })
  }, options);
  observer.observe(target)
}

imgs.forEach(lazyLoad)

虚拟滚动

一、二者区别

•懒渲染:这个就是常见的无限滚动的,每次只渲染一部分(比如 10 条),等剩余部分滚动到可见区域,就再渲染另一部分。
•可视区域渲染(虚拟滚动):只渲染可见部分,不可见部分不渲染。
[注]:实际上考虑页面流畅性,不可能完全不渲染视区之外的内容,建议是预留2-3屏.
二、二者的优缺点
1、懒渲染有三点重大缺陷:
•边滚边加载的模式,会导致页面越发卡顿。(实际上是把锅丢到了后面)
•无法实现动态反映选中状态
•滚动条无法正确反映操作者当前浏览的信息在全部列表中的位置。而且我百万级数据加载,你一次给我加载十几条,滚到底太慢了,是想愚弄用户吗!

2、虚拟滚动有两个重要的基本概念:

•可滚动区域:假设有 1000 条数据,每个列表项的高度是 30,那么可滚动的区域的高度就是 1000 * 30。当用户改变列表的滚动条的当前滚动值的时候,会造成可见区域的内容的变更。•可见区域:比如列表的高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可见区域。

相比较于懒渲染,虚拟滚动要求一次性全部拿到数据,但是滚动条能够完全正确地反映当前页面在全部数据的位置。滚动无非是对几十个dom进行操作,可以达到极高的后续渲染性能。而且一旦实现,可以把页面慢的锅完全丢给后端了。

3、简单实现

export default class VirtualScroll {
    
    
  constructor($list, list, itemGenerator, options = {
    
    }) {
    
    
    this.$list = $list
    this.list = list
    this.itemGenerator = itemGenerator
    this._offset = options.initalOffset || 0
    this.cacheCount = options.cacheCount || 5
    this.renderListWithCache = []
    this.offsetToEdge = 0

    this.initItem(list)
    this.initContainer($list)
    this.initScroll($list)
    this.bindEvent()

    this.offset = this._offset
  }

  get offset() {
    
    
    return this._offset
  }
  set offset(val) {
    
    
    this.render(val)
    return (this._offset = val)
  }

  initItem(list) {
    
    
    this._list = list.map((item, i) => ({
    
    
      height: 40,
      index: i,
      raw: item,
    }))
  }

  initContainer($list) {
    
    
    this.containerHeight = $list.clientHeight
    this.contentHeight = sumHeight(this._list)
    $list.style.overflow = "hidden"
    // $list.style.overflow = "visible"
  }

  initScroll($list) {
    
    
    const $scrollTrack = document.createElement("div")
    const $scrollBar = document.createElement("div")
    $scrollTrack.classList.add("vs__scroll")
    $scrollBar.classList.add("vs__scrollbar")

    $scrollTrack.appendChild($scrollBar)
    $list.appendChild($scrollTrack)
    this.$scrollTrack = $scrollTrack
    this.$scrollBar = $scrollBar
  }

  bindEvent() {
    
    
    let y = 0
    const contentSpace = this.contentHeight - this.containerHeight
    const noThrolttle = (e) => {
    
    
      e.preventDefault()
      y += e.deltaY
      y = Math.max(y, 0)
      y = Math.min(y, contentSpace)
    }
    const updateOffset = (e) => {
    
    
      if (y !== this.offset) {
    
    
        this.offset = y
      }
    }

    let lastPostion = 0
    const recordPostion = (e) => {
    
    
      const offset = extractPx(this.$scrollBar.style.transform)
      lastPostion = offset

      const noThrolttle = (e) => {
    
    
        const scrollSpace = this.$scrollTrack.clientHeight - this.$scrollBar.clientHeight
        lastPostion += e.movementY
        lastPostion = Math.max(lastPostion, 0)
        lastPostion = Math.min(lastPostion, scrollSpace)
      }
      const updatePostion = (e) => {
    
    
        const scrollSpace = this.$scrollTrack.clientHeight - this.$scrollBar.clientHeight
        const contentSpace = this.contentHeight - this.containerHeight
        const rate = lastPostion / scrollSpace

        const contentOffset = contentSpace * rate
        y = contentOffset

        this.offset = contentOffset
        this.$scrollBar.style.transform = `translateY(${
      
      lastPostion}px)`
      }
      const _updatePosition = throttle(updatePostion, 30)
      const removeEvent = () => {
    
    
        document.removeEventListener("mousemove", _updatePosition)
        document.removeEventListener("mousemove", noThrolttle)
        document.removeEventListener("mouseup", removeEvent)
      }

      document.addEventListener("mousemove", noThrolttle)
      document.addEventListener("mousemove", _updatePosition)
      document.addEventListener("mouseup", removeEvent)
    }

    const _updateOffset = throttle(updateOffset, 30)

    this.$list.addEventListener("mousewheel", noThrolttle)
    // this.$list.addEventListener("mousewheel", updateOffset)
    this.$list.addEventListener("mousewheel", _updateOffset)

    this.$scrollBar.addEventListener("mousedown", recordPostion)

    this.unbindEvent = function () {
    
    
      // this.$list.removeEventListener("mousewheel", updateOffset)
      this.$scrollBar.removeEventListener("mousedown", recordPostion)
      this.$list.removeEventListener("mousewheel", _updateOffset)
      this.$list.removeEventListener("mousewheel", noThrolttle)
    }
  }

  render(offset) {
    
    
    updateScrollBar(this.$scrollBar, offset, this.contentHeight, this.containerHeight, this.navigating)

    const headIndex = findOffsetIndex(this._list, offset)
    const tailIndex = findOffsetIndex(this._list, offset + this.containerHeight)

    if (withinCache(headIndex, tailIndex, this.renderListWithCache)) {
    
    
      // 改变translateY
      const headIndexWithCache = this.renderListWithCache[0].index
      const offsetToEdge = offset - sumHeight(this._list, 0, headIndexWithCache)
      this.$listInner.style.transform = `translateY(-${
      
      offsetToEdge}px)`
      return
    }
    console.log("重新渲染")

    const headIndexWithCache = Math.max(headIndex - this.cacheCount, 0)
    const tailIndexWithCache = Math.min(tailIndex + this.cacheCount, this._list.length)

    this.renderListWithCache = this._list.slice(headIndexWithCache, tailIndexWithCache)
    this.offsetToEdge = offset - sumHeight(this._list, 0, headIndexWithCache)

    if (!this.$listInner) {
    
    
      const $listInner = document.createElement("div")
      $listInner.classList.add("vs__inner")
      this.$list.appendChild($listInner)
      this.$listInner = $listInner
    }

    const fragment = document.createDocumentFragment()

    for (let i = 0; i < this.renderListWithCache.length; i++) {
    
    
      const item = this.renderListWithCache[i]
      const $item = this.itemGenerator(item)

      if ($item && $item.nodeType === 1) {
    
    
        fragment.appendChild($item)
      }
    }

    this.$listInner.innerHTML = ""
    this.$listInner.appendChild(fragment)
    this.$listInner.style.transform = `translateY(-${
      
      this.offsetToEdge}px)`

    function withinCache(currentHead, currentTail, renderListWithCache) {
    
    
      if (!renderListWithCache.length) return false
      const head = renderListWithCache[0]
      const tail = renderListWithCache[renderListWithCache.length - 1]
      const withinRange = (num, min, max) => num >= min && num <= max

      return withinRange(currentHead, head.index, tail.index) && withinRange(currentTail, head.index, tail.index)
    }

    function updateScrollBar($scrollBar, offset, contentHeight, containerHeight, navigating) {
    
    
      // 移动滑块时不用再更新滑块位置
      if (navigating) return

      const barHeight = $scrollBar.clientHeight
      const scrollSpace = containerHeight - barHeight
      const contentSpace = contentHeight - containerHeight

      let rate = offset / contentSpace
      if (rate > 1) {
    
    
        rate = 1
      }
      const barOffset = scrollSpace * rate
      $scrollBar.style.transform = `translateY(${
      
      barOffset}px)`
    }
  }

  destory() {
    
    
    this.unbindEvent()
  }
}

function sumHeight(list, start = 0, end = list.length) {
    
    
  let height = 0
  for (let i = start; i < end; i++) {
    
    
    height += list[i].height
  }

  return height
}

function findOffsetIndex(list, offset) {
    
    
  let currentHeight = 0
  for (let i = 0; i < list.length; i++) {
    
    
    const {
    
     height } = list[i]
    currentHeight += height

    if (currentHeight > offset) {
    
    
      return i
    }
  }

  return list.length - 1
}

function throttle(fn, wait) {
    
    
  let timer, lastApply

  return function (...args) {
    
    
    const now = Date.now()
    if (!lastApply) {
    
    
      fn.apply(this, args)
      lastApply = now
      return
    }

    if (timer) return
    const remain = now - lastApply > wait ? 0 : wait

    timer = setTimeout(() => {
    
    
      fn.apply(this, args)
      lastApply = Date.now()
      timer = null
    }, remain)
  }
}

function extractPx(string) {
    
    
  const r = string.match(/[\d|.]+(?=px)/)
  return r ? Number(r[0]) : 0
}

参考文章:
链接:
https://juejin.cn/post/6844904183582162957#heading-14
https://juejin.cn/post/6924918404444848136
https://cloud.tencent.com/developer/article/1658852
https://cloud.tencent.com/developer/article/1645202

猜你喜欢

转载自blog.csdn.net/Beth__hui/article/details/113944082