Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

前言

这是一道面试题,这个问题出来的一刹那,很容易想到的就是 for 循环 100000 次吧,但是这方案着实让浏览器崩溃啊!还有什么解决方案呢?

正文

  1. for 循环 100000 次

虽说for循环有点 low,但是,当面试官问,为什么会让浏览器崩溃的时候,你知道咋解释吗?
来个例子吧,我们需要在一个容器(ul)中存放 100000 项数据(li):

我们的思路是打印 js 运行时间页面渲染时间,第一个console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为 JS 运行所需要的时间;第二个console.log是在 setTimeout 中的,它的触发时间是在渲染完成,在下一次Event Loop中执行的。

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta ></ul>  <script>    let now = Date.now();  //Date.now()得到时间戳    const total = 100000    const ul = document.getElementById('ul')    for (let i = 0; i < total; i++) {      let li = document.createElement('li')      li.innerHTML = ~~(Math.random() * total)       ul.appendChild(li)    }    console.log('js运行时间',Date.now()-now);    setTimeout(()=>{        console.log('总时间',Date.now()-now);    },0)    console.log();  </script></body></html>

运行可以看到这个数据:

image.png

这渲染开销也太大了吧!而且它是十万条数据一起加载出来,没加载完成我们看到的会是一直白屏;在我们向下滑动过程中,页面也会有卡顿白屏现象,这就需要新的方案了。继续看!

  1. 定时器

我们可以使用定时器实现分页渲染,我们继续拿上面那份代码进行优化:

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta ></ul>  <script>    let now = Date.now();  //Date.now()得到时间戳    const total = 100000  //总共100000条数据    const once = 20  //每次插入20条    const page = total / once  //总页数    let index = 1    const ul = document.getElementById('ul')    function loop(curTotal, curIndex) {      if (curTotal <= 0) {  判断总数居条数是否小于等于0        return false      }      let pageCount = Math.min(curTotal, once)  //以便除不尽有余数      setTimeout(() => {        for (let i = 0; i < pageCount; i++) {          let li = document.createElement('li')          li.innerHTML = curIndex + i + ':' + ~~(Math.random() * total)           ul.appendChild(li)        }        loop(curTotal - pageCount, curIndex + pageCount)      }, 0)    }    loop(total, index)  </script></body></html>

运行后可以看到这十万条数据并不是一次性全部加载出来,浏览器右方的下拉条有顺滑的效果哦,如下图:

进度条. gif

但是当我们快速滚动时,页面还是会有白屏现象,如下图所示,这是为什么呢?

st.gif

可以说有两点原因:

  • • 一是setTimeout的执行时间是不确定的,它属于宏任务,需要等同步代码以及微任务执行完后执行。

  • • 二是屏幕刷新频率受分辨率和屏幕尺寸影响,而 setTimeout 只能设置一个固定的时间间隔,这个时间不一定和屏幕刷新时间相同。

    1. requestAnimationFrame

    我们这次采用 requestAnimationFrame 的方法,它是一个用于在下一次浏览器重绘之前调用指定函数的方法,它是 HTML5 提供的 API。

    我们插入一个小知识点, requestAnimationFrame 和 setTimeout 的区别:
    · requestAnimationFrame的调用频率通常为每秒 60 次。这意味着我们可以在每次重绘之前更新动画的状态,并确保动画流畅运行,而不会对浏览器的性能造成影响。
    · setIntervalsetTimeout它可以让我们在指定的时间间隔内重复执行一个操作,不考虑浏览器的重绘,而是按照指定的时间间隔执行回调函数,可能会被延迟执行,从而影响动画的流畅度。

还有一个问题,我们多次创建 li 挂到 ul 上,这样会导致回流,所以我们用虚拟文档片段的方式去优化它,因为它不会触发 DOM 树的重新渲染!

<!DOCTYPE html><html lang="en">![rf.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3eab42b37f53408b981411ee54088d5a~tplv-k3u1fbpfcp-watermark.image?)<head>  <meta charset="UTF-8">  <meta >  <title>Document</title></head>![st.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3e922cc57a044f5e9e48e58bda5f6756~tplv-k3u1fbpfcp-watermark.image?)<body>  <ul id="ul"></ul>  <script>    let now = Date.now();  //Date.now()得到时间戳    const total = 10000    const once = 20    const page = total / once    let index = 1    const ul = document.getElementById('ul')    function loop(curTotal, curIndex) {      if (curTotal <= 0) {        return false      }      let pageCount = Math.min(curTotal, once)  //以便除不尽有余数      requestAnimationFrame(()=>{        let fragment = document.createDocumentFragment() //虚拟文档        for (let i = 0; i < pageCount; i++) {          let li = document.createElement('li')          li.innerHTML = curIndex + i + ':' + ~~(Math.random() * total)            fragment.appendChild(li)        }        ul.appendChild(fragment)        loop(curTotal - pageCount, curIndex + pageCount)      })    }    loop(total, index)  </script></body></html>

可以看到它白屏时间没有那么长了:

rqf.gif

还有没有更好的方案呢?当然有!往下看!

  1. 虚拟列表

我们可以通过这张图来表示虚拟列表红框代表你的手机黑条代表一条条数据

思路:我们只要知道手机屏幕最多能放下几条数据,当下拉滑动时,通过双指针的方式截取相应的数据就可以了。
🚩 PS: 为了防止滑动过快导致的白屏现象,我们可以使用预加载的方式多加载一些数据出来。

代码如下:

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta https://unpkg.com/vue@3/dist/vue.global.js"></script>  <title>虚拟列表</title>  <style>    .v-scroll {      height: 600px;      width: 400px;      border: 3px solid #000;      overflow: auto;      position: relative;      -webkit-overflow-scrolling: touch;    }    .infinite-list {      position: absolute;      left: 0;      top: 0;      right: 0;      z-index: -1;    }    .scroll-list {      left: 0;      right: 0;      top: 0;      position: absolute;      text-align: center;    }    .scroll-item {      padding: 10px;      color: #555;      box-sizing: border-box;      border-bottom: 1px solid #999;    }  </style></head><body>  <div id="app">    <div ref="list" class="v-scroll" @scroll="scrollEvent($event)">      <div class="infinite-list" :style="{ height: listHeight + 'px' }"></div>            <div class="scroll-list" :style="{ transform: getTransform }">        <div ref="items" class="scroll-item" v-for="item in visibleData" :key="item.id"          :style="{ height: itemHeight + 'px',lineHeight: itemHeight + 'px' }">{{ item.msg }}</div>      </div>    </div>  </div>  <script>    var throttle = (func, delay) => {  //节流      var prev = Date.now();      return function () {        var context = this;        var args = arguments;        var now = Date.now();        if (now - prev >= delay) {          func.apply(context, args);          prev = Date.now();        }      }    }    let listData = []    for (let i = 1; i <= 10000; i++) {      listData.push({        id: i,        msg: i + ':' + Math.floor(Math.random() * 10000)      })    }    const { createApp } = Vue    createApp({      data() {        return {          listData: listData,          itemHeight: 60,          //可视区域高度          screenHeight: 600,          //偏移量          startOffset: 0,          //起始索引          start: 0,          //结束索引          end: null,        };      },      computed: {        //列表总高度        listHeight() {          return this.listData.length * this.itemHeight;        },        //可显示的列表项数        visibleCount() {          return Math.ceil(this.screenHeight / this.itemHeight)        },        //偏移量对应的style        getTransform() {          return `translate3d(0,${this.startOffset}px,0)`;        },        //获取真实显示列表数据        visibleData() {          return this.listData.slice(this.start, Math.min(this.end, this.listData.length));        }      },      mounted() {        this.start = 0;        this.end = this.start + this.visibleCount;      },      methods: {        scrollEvent() {          //当前滚动位置          let scrollTop = this.$refs.list.scrollTop;          //此时的开始索引          this.start = Math.floor(scrollTop / this.itemHeight);          //此时的结束索引          this.end = this.start + this.visibleCount;          //此时的偏移量          this.startOffset = scrollTop - (scrollTop % this.itemHeight);        }      }    }).mount('#app')  </script></body></html>

可以看到白屏现象解决了!

结语

解决十万条数据渲染的方案基本都在这儿了,还有更好的方案等待大佬输出!