The principle of virtual scrolling and lazy loading

Implementation of lazy loading and underlying principles

1. The underlying rendering mechanism of the browser:

1. Build DOM tree
2. Style calculation
3. Layout stage
4. Layering
5. Drawing
6. Blocking
7. Rasterization
8. Synthesis

Two, several API

1. Document.documentElement.clientHeight
gets the height of the visible area of ​​the screen.
Insert picture description here
2. element.offsetTop
gets the height of the element relative to the top of the document.
Insert picture description here
3. Document.documentElement.scrollTop
gets the distance between the top of the browser window and the top of the document, that is, the distance of the scroll bar.

4. If offsetTop-scroolTop<clientHeight is satisfied, the picture enters the visible area, and we go to request the picture to enter the visible area.
Insert picture description here

三、getBoundingClientRect()

This function returns a rectObject object, the object has 6 properties: top, left, bottom, right, width, height; here top, left and CSS understanding are very similar, width, height are the width and height of the element itself, but Right, the understanding in bottom and css is a bit different. right refers to the distance from the right edge of the element to the leftmost side of the window, and bottom refers to the distance from the bottom edge of the element to the topmost part of the window.
Insert picture description here

Through this API, we can easily get the vertex position rectObject.top of the img element relative to the viewport. As long as this value is less than the browser's height window.innerHeight, it means entering the visible area:

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

Fourth, realize lazy loading of images based on IntersectionObserver

There is a new IntersectionObserver API that can automatically "observe" whether an element is visible or not. Chrome 51+ already supports it. Since the essence of visible is that the target element generates an intersection with the viewport, this API is called an intersection observer.

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)

Virtual scroll

1. The difference between the two

• Lazy rendering: This is the common infinite scrolling, each time only a part (such as 10) is rendered, and the remaining part is scrolled to the visible area, and then another part is rendered.
•Visual area rendering (virtual scrolling): Only the visible part is rendered, and the invisible part is not rendered.
[Note]: In fact, considering page fluency, it is impossible not to render content outside the viewport at all. It is recommended to reserve 2-3 screens.
2. Advantages and disadvantages of both
1. Lazy rendering has three major flaws:
• Side The mode of piping loading will cause the page to freeze more and more. (Actually, the pot is left behind)
• Cannot dynamically reflect the selected state
• The scroll bar cannot correctly reflect the position of the information currently browsed by the operator in the entire list. And I load millions of data, you load me more than a dozen at a time, it is too slow to roll, do you want to fool the user!

2. There are two important basic concepts of virtual scrolling:

• Scrollable area: Assuming there are 1000 pieces of data and the height of each list item is 30, then the height of the scrollable area is 1000 * 30. When the user changes the current scroll value of the scroll bar of the list, it will cause the content of the visible area to change. •Visible area: For example, the height of the list is 300, and there is a vertical scroll bar on the right to scroll, then the visually visible area is the visible area.

Compared with lazy rendering, virtual scrolling requires all data to be obtained at one time, but the scroll bar can completely accurately reflect the position of all data on the current page. Scrolling is nothing more than operating on dozens of doms, which can achieve extremely high subsequent rendering performance. And once it is implemented, the slowness of the page can be completely lost to the back end.

3. Simple implementation

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
}

Reference article:
Link:
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

Guess you like

Origin blog.csdn.net/Beth__hui/article/details/113944082