当我们有 10 万条或者更多的数据需要展示在页面中,我们应该怎样处理呢?
这里主要的方式有两种:
- 懒加载:即监听 scroll 事件或使用
IntersecionObserver
监听; - 可视区域的渲染:仅在可视区域展示数据,为保证滚动条的完整性,非可视区域使用占位元素的高度后者容器的位移来撑开。
1. 懒加载 #
懒加载的方式有两种:监听 scroll 事件或使用IntersecionObserver
监听某个元素。
1.1 scroll 事件 #
监听滚动事件应该是我们使用的最多的事件了,当滚动条滑动到页面最底部或者将要滑动到最底部的时候,去加载下一页的数据。同时图片懒加载或者其他组件的懒加载也可以依赖于滚动事件!
为了优化滚动事件,我们也会给滚动事件添加上防抖和节流
。
这是一个很常见的 demo,无限加载数据系列,当滚动条滑动到底部时,加载下一页的数据:监听滚动事件懒加载数据。
请分别用微信和 QQ 扫下这个二维码体验一下,体验时,请点击“向左”的按钮关闭代码的部分:
其实我们发现会有一个很有意思的体验:在 android 中的微信和 QQ 中基本可以无限的滑动,在 iOS 的微信中也可以,但是在 iOS 的 QQ 中,当我们一直滑动时,需要滑动到底部,然后才能加载新的数据,才能继续滑动!
这种原因是 iOS 不同的 webview 造成的。在 iOS 中,UIWebView 是在 iOS2 中出现的,性能更好的 WKWebView 跟随 iOS8 一起出现。从性能方面来说,WKWebView 会比 UIWebView 高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的 H5 页面支持,类比 UIWebView 还多提供了一个加载进度的属性。同时,在 UIWebView 中,无论是页面的 body 滚动,还是 div 级别的局部滚动,都不会实时触发,而是在滚动结束后才会触发!
可以分别体验下面的两个二维码,左侧的为 body 级别的滚动,右侧为 div 的局部滚动:
总结下滚动事件在不同的 webview 中的表现情况:
机型 | body 滚动 | 局部滚动 |
---|---|---|
iOS WKWebView | 实时触发 | 实时触发 |
iOS UIWebView | 无法实时触发 | 无法实时触发 |
Android | 实时触发 | 实时触发 |
那么该如何优化滚动的性能呢?
首先就是防抖和节流。在 Chrome 浏览器中使用performance.now()
测试滚动每次触发的间隔,可以看到,频率大概在 16ms 左右,但很多时候我们并不需要这么快地触发我们的监听函数,而且,比如图片懒加载等场景,也要及时的更新一次,这时候就用到了防抖和节流!
再有就是使用requestAnimationFrame
与requestIdleCallback
代替定时器。若使用定时器进行间歇性触发监听函数,会出现掉帧的情况,我们也知道setTimeout(fn, 100)
,不一定是正好在 100ms 后触发 fn 函数,而是等浏览器空闲之后才调用 fn 函数,当多次积累后就会产生掉帧的,那么使用requestAnimationFrame
或requestIdleCallback
,则浏览器会根据自己渲染的频率适时地执行回调。在追求高性能的渲染效果时,可以考虑用 requestIdleCallback()和 requestAnimationFrame()代替定时器。前者适合流畅的动画效果场景,后者适用于分离一些优先级低的操作逻辑,使用时需要考虑清楚
1.2 IntersecionObserver #
使用 IntersecionObserver 也可以实现无限滚动,比如在底部监听一个透明的元素,当该元素可见时就加载新的资源!
demo: IntersecionObserver 实现无限滚动
不过也应当看到的是 IntersecionObserver 的支持程度还不太好,如果要使用的话,还是需要 polyfill 方案!
而且 IntersecionObserver 在图片懒加载和组件懒加载的过程中,非常有用,我们后续会进行了解!
2. 模拟滚动 #
模拟滚动最代表的例子就是iScroll
,监听手势的 touchmove 事件,然后使用 CSS3 中的 transform 产生位移。
不过在模拟滚动的过程中,若图片比较多时,可以感觉到滚动时有明显的卡顿感。查看这个样例:https://www.xiabingbao.com/demos/20190423/iscroll-scroll.html。
这是利用 IScroll 中的scroll
事件,无限地向页面中添加元素。不过当页面的数据是无限加载时,应当使用iscroll-infinite
这个类库更好,这部分留到后面讲解!
3. 可视区域数据的渲染 #
上面的几种方法里,我们都是无限地直接往页面中添加元素,滑动的页码越大,页面中的 DOM 元素也就越多。但实际上,这是没有必要的,滚动到可视区域外的元素,用户是看不到的,也没不要保留在页面中,可以用一个占位元素或者 transform 撑开上面所有不可见元素的高度,让滚动条能够正常的上下滑动即可!
撑开上半部分不可见区域的高度,有两种方法,一种是在容器内的最顶部设置一个占位元素,这个占位元素的高度就是消失的所有 DOM 的高度;再一个就是给容器或者占位元素一个 transform 的位移!这里我们使用给占位元素一个高度撑起顶部的滚动区域。
3.1 固定高度的 item #
若每个 item 的 height/margin/padding 等数据都是固定写死的,这种情况比较容易实现!获取一次数据后,不用每次都重新计算。
每次滚动时,都要计算应当渲染列表的哪部分!start
表示列表开始的位置,fixedScrollTop
表示顶部占位元素的距离
// 滚动处理函数
function handleScroller() {
let lastStart = 0; // 上次开始的位置
const item = document.querySelector('.container .item');
const itemStyle = getComputedStyle(item);
const itemHeight = item.offsetHeight + parseInt(itemStyle['marginTop']) + parseInt(itemStyle['marginBottom']);
return function () {
const currentScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
const fixedScrollTop = currentScrollTop - (currentScrollTop % itemHeight);
let start = Math.floor(currentScrollTop / itemHeight); // 可视区域开始渲染的位置
if (lastStart !== start) {
lastStart = start;
createDom(start, count, fixedScrollTop);
}
};
}
设置顶部滚动的高度:
function createDom(start, count, height) {
const container = document.querySelector('.container');
// container.style.transform = `translateY(${height}px)`; // 给容易一个位移
let div = document.createDocumentFragment();
// 创建占位元素
if (height) {
let p = document.createElement('p');
p.style.height = height + 'px';
div.appendChild(p);
}
for (let i = start, len = start + count; i < len; i++) {
let item = document.createElement('div');
item.className = 'item';
item.innerHTML = i;
div.appendChild(item);
}
// 为了方便处理,我们这里采用了更新container中的全部元素
// 你也可以尝试只增加/删除首位的元素,中间元素不变
container.innerHTML = '';
container.appendChild(div);
}
可以点击链接查看样例:可视区域渲染之 DOM 元素增减,审查元素可以看得更清晰,无论我们怎么滚动,.container
中永远只有那么几个元素!
3.2 每个 item 的高度都不一定 #
待定,有个问题一直搞不定
3.3 DOM 的复用 #
重点来了,在上面的章节里,我们进行了进一步的优化,只渲染可视区域内的数据。但是还是存在一个重要的问题:频繁的改动 DOM,这样会频繁的引起页面的重绘。这里我们的思想是对页面中 DOM 元素进行复用,如下图中所示,从上面滑出的元素,可以直接定位下面再重新装填元素,反之亦然!
可以点击链接查看 demo,在审查元素中我们可以看到,在滚动的过程中,DOM 并没有被删掉,而是改变了 transform,放到了最下面:可视区域渲染之 DOM 元素复用。
// 更新页面中DOM元素的位置
function updateDom(start, count, itemHeight, height) {
document.querySelector('.container .content').style.transform = 'translateY(' + height + 'px)';
for (var i = start, len = start + count; i < len; i++) {
var index = i % count;
var cssIndex = (i - start) % len;
document.querySelector('.item' + index).innerHTML = i;
document.querySelector('.item' + index).style.transform = 'translateY(' + itemHeight * cssIndex + 'px)';
}
}
这里我们还是监听了元素的 scroll 事件,我们在上面一笔带过的iScroll-infinite
类库,是使用了模拟滚动+DOM 复用。点击链接查看 demo:iScroll-infinite 实现的无限加载。
4. 总结 #
最有效的方案应当是模拟滚动+DOM 复用,既解决了滚动不能实时生效的问题,又能解决页面中 DOM 元素过多,造成滚动中卡顿的问题。
总是想着要讲的有很多,不过最后好像哪方面都欠缺了一点,后面有机会再针对其中的某一个点再深入进行探讨!例如使用IntersecionObserver
实现 Vue 组件的懒加载,如何自己实现一个简单的模拟滚动等。
参考: