目录
懒加载实现方式及底层原理
一、浏览器的底层渲染机制:
1、构建 DOM 树
2、样式计算
3、布局阶段
4、分层
5、绘制
6、分块
7、光栅化
8、合成
二、几个API
1、document.documentElement.clientHeight
获取屏幕可视区域的高度。
2、element.offsetTop
获取元素相对于文档顶部的高度。
3、document.documentElement.scrollTop
获取浏览器窗口顶部与文档顶部之间的距离,也就是滚动条滚动的距离。
4、如果满足offsetTop-scroolTop<clientHeight,则图片进入了可视区内,我们就去请求进入可视区域的图片。
三、getBoundingClientRect()
该函数返回一个rectObject对象,该对象有 6 个属性:top, left, bottom, right, width, height;这里的top、left和css中的理解很相似,width、height是元素自身的宽高,但是right,bottom和css中的理解有点不一样。right是指元素右边界距窗口最左边的距离,bottom是指元素下边界距窗口最上面的距离。
通过这个 API,我们就很容易获取img元素相对于视口的顶点位置rectObject.top,只要这个值小于浏览器的高度window.innerHeight就说明进入可视区域:
function isInSight(el){
const bound = el.getBoundingClientRect();
const clientHeight = window.innerHeight;
return bound.top <= clientHeight;
}
四、基于 IntersectionObserver 实现图片懒加载
有一个新的 IntersectionObserver API,可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做交叉观察器。
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)
虚拟滚动
一、二者区别
•懒渲染:这个就是常见的无限滚动的,每次只渲染一部分(比如 10 条),等剩余部分滚动到可见区域,就再渲染另一部分。
•可视区域渲染(虚拟滚动):只渲染可见部分,不可见部分不渲染。
[注]:实际上考虑页面流畅性,不可能完全不渲染视区之外的内容,建议是预留2-3屏.
二、二者的优缺点
1、懒渲染有三点重大缺陷:
•边滚边加载的模式,会导致页面越发卡顿。(实际上是把锅丢到了后面)
•无法实现动态反映选中状态
•滚动条无法正确反映操作者当前浏览的信息在全部列表中的位置。而且我百万级数据加载,你一次给我加载十几条,滚到底太慢了,是想愚弄用户吗!
2、虚拟滚动有两个重要的基本概念:
•可滚动区域:假设有 1000 条数据,每个列表项的高度是 30,那么可滚动的区域的高度就是 1000 * 30。当用户改变列表的滚动条的当前滚动值的时候,会造成可见区域的内容的变更。•可见区域:比如列表的高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可见区域。
相比较于懒渲染,虚拟滚动要求一次性全部拿到数据,但是滚动条能够完全正确地反映当前页面在全部数据的位置。滚动无非是对几十个dom进行操作,可以达到极高的后续渲染性能。而且一旦实现,可以把页面慢的锅完全丢给后端了。
3、简单实现
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
}
参考文章:
链接:
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