前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
前言
这是一道面试题,这个问题出来的一刹那,很容易想到的就是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 name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul id="ul"></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>
运行可以看到这个数据:
这渲染开销也太大了吧!而且它是十万条数据一起加载出来,没加载完成我们看到的会是一直白屏;在我们向下滑动过程中,页面也会有卡顿白屏现象,这就需要新的方案了。继续看!
2. 定时器
我们可以使用定时器
实现分页渲染
,我们继续拿上面那份代码进行优化:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul id="ul"></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>
运行后可以看到这十万条数据并不是一次性全部加载出来,浏览器右方的下拉条有顺滑的效果哦,如下图:
但是当我们快速滚动时,页面还是会有白屏现象,如下图所示,这是为什么呢?
可以说有两点原因:
- 一是
setTimeout的执行时间是不确定的
,它属于宏任务,需要等同步代码以及微任务执行完后执行。 - 二是
屏幕刷新频率受分辨率和屏幕尺寸影响
,而setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕刷新时间相同。
3. requestAnimationFrame
我们这次采用requestAnimationFrame的方法,它是一个用于在下一次浏览器重绘之前调用指定函数的方法,它是 HTML5 提供的 API。
我们插入一个小知识点, requestAnimationFrame 和 setTimeout 的区别:
·
requestAnimationFrame
的调用频率通常为每秒60次。这意味着我们可以在每次重绘之前更新动画的状态,并确保动画流畅运行,而不会对浏览器的性能造成影响。·
setInterval
与setTimeout
它可以让我们在指定的时间间隔内重复执行一个操作,不考虑浏览器的重绘,而是按照指定的时间间隔执行回调函数,可能会被延迟执行,从而影响动画的流畅度。
还有一个问题,我们多次创建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 name="viewport" content="width=device-width, initial-scale=1.0"> <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>
可以看到它白屏时间没有那么长了:
还有没有更好的方案呢?当然有!往下看!
4. 虚拟列表
我们可以通过这张图来表示虚拟列表,红框代表你的手机,黑条代表一条条数据。
思路:我们只要知道
手机屏幕
最多能放下几条数据
,当下拉
滑动时,通过双指针
的方式截取
相应的数据就可以了。🚩 PS:为了防止
滑动过快
导致的白屏
现象,我们可以使用预加载
的方式多加载一些数据出来。
代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="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>
可以看到白屏现象解决了!
结语
解决十万条数据渲染的方案基本都在这儿了,还有更好的方案等待大佬输出!