神兵利器:IntersectionObserver下的虚拟列表、懒加载、无限列表

开发 · 2023-12-12 · 46 人浏览

IntersectionObserver提供了以异步的方式,观察目标元素与祖先元素的交叉状态,即相当于被观察元素是否出现在祖先元素视口中

0x01 兼容性

2023-12-12T15:32:08.png
对于生产环境来说可以无脑上了,如果还要考虑IE环境可以上个PolyFill解决,其本质就是用MutionObserver替代,兼容到IE11

0x02 基本使用

// [触发交叉状态的回调,配置项]
const Ob = new IntersectionObserver(callback,options)
Ob.observe(element) //监听的元素 一般会遍历要监听的元素
Ob.unobserve() //关闭监听
Ob.disconnect() //回收

Callback参数

IntersectionObserverEntry 数组,它包含了所有触发了交叉状态的被监听元素的成员,我们主要使用它的这些属性:

  1. boundingClientRect 边界信息
  2. isintersection 是否可见
  3. 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>
IntersectionObserver
Theme Jasmine by Kent Liao