优化实战 第 13 期 - 无限异步长列表的最佳方案

1,111 阅读2分钟

性能分析

当页面中包含大量元素和复杂布局的时候,页面会有明显的卡顿感,会严重影响用户体验

常见场景:大数据量的列表渲染,如 无限滚动的列表 或 表格

卡顿根源:一次性渲染的 DOM 元素太多,导致性能开销较大

优化方案

监听已渲染 DOM 列表的尾部节点,在其进入视口的时候渲染下一页的数据

判断某个元素是否进入了视口

  • 老式解决方案

    const { top, right, bottom, left } = Element.getBoundingClientRect()
    

    通过 getBoundingClientRect() 获取目标元素对应于浏览器视窗的位置,再通过 scroll 事件进行监听

  • 新式解决方案

    通过 IntersectionObserver API 自动观察元素是否可见,即 目标元素与视口产生一个交叉区就视为可见

掌握 Intersection Observer 观察器

  • API 使用

    // 创建观察器
    const io = new IntersectionObserver(callback, option)
    // 开始观察 DOM 节点元素
    io.observe(element)
    // 停止观察
    io.unobserve(element)
    // 关闭观察器
    io.disconnect()
    
  • callback回调函数

    const io = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        // 目标元素的区域信息,也就是 getBoundingClientRect() 的返回值
        console.log(entry.boundingClientRect)
        // 目标元素的可见比率
        console.log(entry.intersectionRatio)
        // 目标元素与根元素交叉的区域信息
        console.log(entry.intersectionRect)
        // 目标元素是否进入可视区域
        console.log(entry.isIntersecting)
        // 根元素的矩形区域信息
        console.log(entry.rootBounds)
        // 被观察的目标,是一个 DOM 节点元素
        console.log(entry.target)
        // 可见性发生变化的时间,相交发生时距离页面打开时的毫秒数,精度为微秒
        console.log(entry.time)
      })
    })
    

    callback 回调函数一般会被触发两次,一次是目标元素刚进入视口,另一次是完全离开视口

  • option配置对象

    const io = new IntersectionObserver(entries => {
      console.log(entries)
    }, {
      // 设置什么时候触发回调函数,默认值为 [0],即交叉比例 intersectionRatio 为 0 时触发回调函数
      threshold: [0, 0.25, 0.5, 0.75, 1],
      // 指定目标元素所在的容器节点,也就是滚动容器
      root: document.querySelector('#scrollContainer'),
      // 定义滚动容器的 margin 值
      rootMargin: 10px 10px 10px 10px,
    })
    
  • 注意事项

    IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发

  • 兼容性概览

    通过 https://caniuse.com/ 工具查看

分页加载 + 增量渲染

  • 模板结构

    <ul class="container" ref="scrollContainer">
      <li v-for="(item, index) of records" :key="index">{{ item.name }}</li>
      <template v-if="records.length !== total">
        <li ref="tailItem" v-loading="true" element-loading-text="拼命加载中"></li>
      </template>
      <template v-else>
        <li v-if="total !== 0">没有更多了</li>
      </template>
    </ul>
    
  • 初始化设置

    async created() {
      this.getRecords()  // 获取第一页数据
      this.$nextTick(() => {
        const { scrollContainer, tailItem } = this.$refs
        this.nodeObserver(scrollContainer, tailItem)  // 开启观察器
      }
    }
    
  • 获取分页数据

    async getRecords() {
      const { total, data } = await getData(this.searchParams)
      this.total = total
      this.records.push(...data)
    }
    
  • 观察尾项实现数据分页加载

    nodeObserver(scrollContainer, tailItem) {
      const io = new IntersectionObserver(entries => {
        entries.forEach(({ isIntersecting, target }) => {
          if (isIntersecting && target === node) {
            this.searchParams.page += 1
            this.getRecords()
          }
        })
      }, {
        root: scrollContainer
      })
      io.observe(tailItem)
    }
    

    一起学习,加群交流看 沸点