IntersectionObserver提供了以异步的方式,观察目标元素与祖先元素的交叉状态,即相当于被观察元素是否出现在祖先元素视口中
0x01 兼容性
对于生产环境来说可以无脑上了,如果还要考虑IE环境可以上个PolyFill解决,其本质就是用MutionObserver
替代,兼容到IE11
0x02 基本使用
// [触发交叉状态的回调,配置项]
const Ob = new IntersectionObserver(callback,options)
Ob.observe(element) //监听的元素 一般会遍历要监听的元素
Ob.unobserve() //关闭监听
Ob.disconnect() //回收
Callback参数
IntersectionObserverEntry 数组,它包含了所有触发了交叉状态的被监听元素的成员,我们主要使用它的这些属性:
- boundingClientRect 边界信息
- isintersection 是否可见
- target 被监听元素的DOM
Options参数
主要使用root
字段和rootMargin
,前者是监听的祖先元素,后者是 检测检查状态的边界范围,用于缓存区的设计等等
0x03 懒加载实现
后续例子都会以原生形式实现,方便理解
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>懒加载</title>
<style>
body,html{
margin:0;
padding: 0;
height:100%;
}
div{
box-sizing: border-box;
}
.skin{
height:500px;
overflow: hidden;
position:relative;
width:auto;
margin-bottom:20px;
}
#app{
padding:20px;
height:100%;
overflow: auto;
}
</style>
</head>
<body>
<div id="app">
</div>
<script>
/**
* 这里需要注意的是 根元素的高度控制出现滚动,否则默认是整个根元素完整可见 无懒加载效果
*/
const appDom = document.querySelector('#app')
const renderImgList = [
'//game.gtimg.cn/images/lol/act/img/skinloading/412017.jpg',
'//game.gtimg.cn/images/lol/act/img/skinloading/4013.jpg',
'//game.gtimg.cn/images/lol/act/img/skinloading/64031.jpg',
'//game.gtimg.cn/images/lol/act/img/skinloading/28015.jpg',
'//game.gtimg.cn/images/lol/act/img/skinloading/246002.jpg',
'//game.gtimg.cn/images/lol/act/img/skinloading/421009.jpg'
]
const Ob = new IntersectionObserver((entries)=>{
entries.forEach((item)=>{
if(item.isIntersecting){
console.log('进入可视区域')
item.target.src = item.target.dataset.src
Ob.unobserve(item.target)
}
})
},{
root:appDom //监听的祖先元素,默认文档窗口
})
window.addEventListener('load',()=>{
//碎片化渲染
const fg = document.createDocumentFragment()
renderImgList.forEach((Url)=>{
const div = document.createElement('div')
div.classList.add('skin')
const img = document.createElement('img')
img.setAttribute('data-src',Url)
div.appendChild(img)
fg.appendChild(div)
})
// 将占位的DIV渲染到页面
appDom.appendChild(fg)
// 此时页面上有占位元素了然后遍历占位元素并进行子元素的监听
const imgList = [...document.querySelectorAll('img')]
imgList.forEach((img)=>Ob.observe(img))
})
</script>
</body>
</html>
0x04 无限列表实现
核心就是监听底部的Dom的出现即可
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无限滚动</title>
<style>
body,html{
margin:0;
padding: 0;
height:100%;
}
div{
box-sizing: border-box;
}
.content-item{
height:150px;
line-height: 150px;
text-align: center;
background: blue;
color:#fff;
margin-bottom:20px;
}
.content-item:nth-child(odd){
background-color: rebeccapurple;
}
#app{
padding:20px;
height:100%;
overflow: auto;
}
</style>
</head>
<body>
<div id="app">
<div class="content-item">something</div>
<div class="content-item">something</div>
<div class="content-item">something</div>
<div class="content-item">something</div>
<div class="content-item">something</div>
<div class="content-item">something</div>
<div id="bottomDom"></div>
</div>
<script>
/**
* 这里需要注意的是 底部需要增加一个触底元素,我们只监听这个底部元素,当它可见时 就触发一次加载即可
*/
const btDom = document.querySelector('#bottomDom')
const faDom = document.querySelector('#app')
const loadMore = () => {
const Ary = new Array(6).fill('something')
const fg = document.createDocumentFragment()
Ary.forEach((item)=>{
const dom = document.createElement('div')
dom.classList.add('content-item')
dom.innerText = item
fg.appendChild(dom)
})
// 插入一个元素到指定的子节点之前
faDom.insertBefore(fg,btDom)
}
const Ob = new IntersectionObserver((list)=>{
// 这里只监听了一个尾部元素 默认下标0
if(list[0].isIntersecting) loadMore()
},{
root:faDom
})
window.addEventListener('load',()=>{
Ob.observe(btDom) //监听尾部元素即可,每次出现都加载一次
})
</script>
</body>
</html>
0x05 虚拟列表实现
这里不需要考虑定高不定高的问题,利用占位符,IntersectionObserver会帮我们解决这个问题
这里的实现方案本质就是 先渲染空的div占位,然后元素进入视口了(默认第一次会回调所有元素的触发,第一次触发要裁剪元素),然后元素进去时 渲染内容,离开时候清空内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虚拟列表</title>
<style>
body,html{
margin:0;
padding: 0;
height:100%;
}
div{
box-sizing: border-box;
}
.content-item{
min-height:30px;
text-align: center;
border:1px solid #000;
margin-bottom:20px;
word-break: break-all;
white-space: pre-wrap;
}
#app{
padding:20px;
height:100%;
overflow: auto;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
/**
* 如果你是在vue中开发,那么你的data数据中可以增加一个 显示属性,用v-if来控制显示,而不是通过dom操作
* 这里需要注意的是 我们通过占位内容的方式 实现虚拟列表,当元素出现时候加载内容,元素离开时记录高度和隐藏即可
*/
const app = document.querySelector('#app')
const data = new Array(10000).fill(null).map((it,ix)=>{
return {
id:ix,
value:`第${ix}个内容为${'abcd'.repeat(Math.random()* 100)}`,
}
})
//渲染占位内容
const fg = document.createDocumentFragment()
data.forEach((item)=>{
const div = document.createElement('div')
// div.innerText = item.value 默认不渲染内容 空div性能开销小的忽略不计
div.classList.add('content-item')
div.setAttribute('data-index',item.id)
fg.appendChild(div)
})
//渲染占位内容
app.appendChild(fg)
const Ob = new IntersectionObserver((list)=>{
let viceList = list
console.log(list)
// list 在第一次回调时 默认是全部 这里进行一次裁剪 将在可视区域的内容取出来
if(list.length === data.length){
viceList = list.reduce((pre,cur,index,ary)=>{
if(cur.isIntersecting) pre.push(cur)
return pre
},[])
}
// isIntersecting是否可见
viceList.forEach((item)=>{
// 如果离开可见区域 则存储它的高度信息
if(!item.isIntersecting){
item.target.style.height = `${item.target.clientHeight}px`
item.target.style.display = 'none'
}else{
// 如果进入可见区域 则 取消高度设置 渲染内容
item.target.style.height = ''
item.target.style.display = 'block'
item.target.innerText = data[item.target.dataset.index].value
}
})
},{
root:app,
rootMargin:'400px 0px' //扩展检测的边界,因为我们需要缓存区的设计,由rootMargin实现 取400px是假设 13行 * 最小的 30px高度计算
})
//监听所有占位元素
const AllListDom = [...document.querySelectorAll('.content-item')]
AllListDom.forEach((dom)=>Ob.observe(dom))
</script>
</body>
</html>