likes
comments
collection
share

手动封装一个内容无限滚动的hooks,给你的项目增添技能难点前言 最近都在准备面试,好久没有写文章总结知识点了,这个点也

作者站长头像
站长
· 阅读数 21

前言

最近都在准备面试,好久没有写文章总结知识点了,这个点也是在面试中常见的问题:你了解Vue/React的hooks么? 这个时候我们可不能蒙啊,应该很有底气的和面试官说,hooks?那不是很简单嘛(可千万别乱说哦,被拷打了别来找我)。所以今天我们来聊聊hooks是什么东西吧,以及我们应该如何去实现一个复杂的hooks封装。

何为hooks ?

首先,在React中,我们一般把useXXX的API看成一个Hooks(钩子函数)比如,常见的hooks:useState、useEffect等,在Vue中不是这样的,在 Vue.js 的 Composition API 中,并没有使用 use 前缀的 hooks。Composition API 使用的是与 Vue 生命周期相关的函数,如 setup(),以及一些内置的选项如 refreactivecomputedwatch 等来组织和管理组件的逻辑。

所以按照上面的说法,对于Vue来讲,它没有Hooks这种概念,但是Vue和React它们都有这种理念,就是通过封装类似于叫hooks的函数,去帮助开发者以更简洁、更易读的方式编写组件逻辑。

在Vue中,我更愿意将hooks函数看成是将一个响应式业务(比如:ref,reactive,computed,watch,生命周期,method这类)封装后的函数。这样方便响应式业务的复用和维护方便,也就是说可以把响应式业务从组件里拆分开,放到hooks函数中复用(把这个封装的函数可以称为函数组件),封装响应式业务的细节,对团队协作非常友好,极大的提高了生产效率。

那么接下来,在封装一个内容懒加载hooks之前,我们需要了解一个构造函数IntersectionObserver,那么这个东西是什么呢?

了解IntersectionObserver

IntersectionObserver 是一个 JavaScript API,用于异步通知元素何时出现在视口内或消失在视口外。这个方法通常作用在图片懒加载、无限滚动或者监控某个元素的可见性的时候。

IntersectionObserver的使用

  1. 创建Observer实例对象:

    • 使用 new IntersectionObserver() 去创建实例对象。
    • 它有两个参数(第一个参数是传递一个回调函数,当目标元素进入视口时会被调用,第二个参数是配置对象,如: 默认情况下,rootMargin 属性的值为 "0px 0px 0px 0px",表示没有边缘偏移。以及配置阈值(threshold):表示多长时间后触发更新。
  2. 观察目标元素:

    • 使用 observe() 方法来指定要观察的 DOM 元素。
    • 一旦元素进入视口,就会触发回调函数。
  3. 取消观察:

    • 使用 unobserve() 方法来停止对某个元素的观察。
    • 使用 disconnect() 方法来断开整个 IntersectionObserver 实例,并停止观察所有已注册的元素。

下面我们将用一个图片懒加载的示例,同时展示IntersectionObserver如何实现懒加载操作的。

图片懒加载的实现

过程: 利用observer去对观察的元素进行监控,observer是IntersectionObserver的实例对象,它的observe()方法可以实现监听一个目标DOM元素。然后配合上面在创建Observer实例对象时,传入的回调函数,实现图片的加载。

手动封装一个内容无限滚动的hooks,给你的项目增添技能难点前言 最近都在准备面试,好久没有写文章总结知识点了,这个点也

Demo代码放下面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lazyload</title>
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        .gallery{
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
        }
        .lazy {
            width: 600px;
            height: 600px;
            background-color: #eee;
            margin: 10px;
        }
        .lazy[data-src] {
            background-image: url("https://img.36krcdn.com/hsossms/20240722/v2_4e58611ea2804647a9e6cf98f6676928@5689903_oswg73902oswg1053oswg495_img_jpg?x-oss-process=image/resize,m_mfit,w_960,h_400,limit_0/crop,w_960,h_400,g_center/format,webp");
            background-size: cover;
            background-position: center;
        }

    </style>
</head>
<body>
    <div class="gallery">
        <img class="lazy" data-src="https://img.36krcdn.com/hsossms/20240722/v2_4e58611ea2804647a9e6cf98f6676928@5689903_oswg73902oswg1053oswg495_img_jpg?x-oss-process=image/resize,m_mfit,w_960,h_400,limit_0/crop,w_960,h_400,g_center/format,webp" alt="Image 1">
        <img class="lazy" data-src="https://img.36krcdn.com/hsossms/20240712/v2_ea0b096a66474200962d776772cbbfbf@000000_oswg36338oswg503oswg503_img_000?x-oss-process=image/resize,m_mfit,w_960,h_400,limit_0/crop,w_960,h_400,g_center" alt="Image 2">
        <img class="lazy" data-src="https://img.36krcdn.com/hsossms/20240719/v2_75a198c51e634a68a58adaefaae74223@000000_oswg199535oswg432oswg288_img_jpg?x-oss-process=image/format,webp" alt="Image 2">
        <img class="lazy" data-src="https://img.36krcdn.com/hsossms/20240722/v2_f09934fb22cd468cb6683bc6cf1f8b56@5888275_oswg1002452oswg1053oswg495_img_png?x-oss-process=image/resize,m_mfit,w_600,h_400,limit_0/crop,w_600,h_400,g_center/format,webp" alt="Image 2"/>
        <img class="lazy" data-src="https://img.36krcdn.com/hsossms/20240722/v2_79638a0dbb0140b6b14ca96bc479456a@5888275_oswg633813oswg1053oswg495_img_png?x-oss-process=image/resize,m_mfit,w_600,h_400,limit_0/crop,w_600,h_400,g_center/format,webp" alt="Image 2"/>
        <img class="lazy" data-src="https://img.36krcdn.com/hsossms/20240722/v2_5b7c31cf000c4e80874e7a8f8e539a76@5888275_oswg1150789oswg1053oswg495_img_png?x-oss-process=image/resize,m_mfit,w_600,h_400,limit_0/crop,w_600,h_400,g_center/format,webp" alt="Image 2"/>
        <img class="lazy" data-src="https://img.36krcdn.com/hsossms/20240722/v2_f1c8c63177d449cf92412120198bdceb@5888275_oswg741252oswg1053oswg495_img_png?x-oss-process=image/resize,m_mfit,w_600,h_400,limit_0/crop,w_600,h_400,g_center/format,webp" alt="Image 2"/>
        <img class="lazy" data-src="https://img.36krcdn.com/hsossms/20240722/v2_b4acb4d77d28416c926560369ea9bbb4@5888275_oswg576270oswg1053oswg495_img_png?x-oss-process=image/resize,m_mfit,w_600,h_400,limit_0/crop,w_600,h_400,g_center/format,webp" alt="Image 2"/>
        <!-- 更多图片 -->
    </div>
    <script>
        document.addEventListener('DOMContentLoaded',()=>{
            const images = document.querySelectorAll('.lazy');
            const loadImg = (image) =>{
                image.src = image.dataset.src;
                image.classList.remove('lazy');
            }
            // 在不在可视区域
            const observer = new IntersectionObserver((entries) =>{
                console.log(entries,'我在测试');
                entries.forEach(entry =>{
                    if(entry.isIntersecting){
                        loadImg(entry.target);
                        observer.unobserve(entry.target);
                    }
                })
            },{
                rootMargin: '0px',
                threshold: 0.5,
            }
            )
            // 观察者模式
            images.forEach((image)=>{
                // 添加一个image 监听
                observer.observe(image);
            })
        })
    </script>
</body>
</html>

封装内容无限滚动的hook函数

那么接下来,我们可以开始对功能进行hooks封装了,首先我们需了解这一场景的使用过程

主要就是先加载比如说10条数据,用一个变量拿到最后一条数据的DOM结构,hooks内主要做的操作就是对DOM进行IntersectionObserver的监视并封装。如果发现所监测的DOM进入视口了,说明需要开始加载更多数据了。

这个时候就需要看是否有更多数据,有,就加载更多,执行传入的回填函数(加载更多数据的函数),没有,通知标识符修改值,表明没有值了,无需加载更多。

  1. 首先做好仓库的假数据:

    这里使用的是pinia做的数据状态管理,其创建和全局使用可以去看一下它的使用配置和方法,我会对以下代码进行解释。

    import { defineStore } from "pinia";
    import { ref } from 'vue'
    import type { Article } from '../types/article'
    export const useArticleStore = defineStore('article', ()=>{
        // 私有的 所有数据  也是源数据
        const _artciles:Article[] = [
            {
                id: 1,
                title: '这是第一篇文章'
            },
            {
                id: 2,
                title: '这是第二篇文章'
            },
            {
                id: 3,
                title: '这是第二篇文章'
            },
            {
                id: 4,
                title: '这是第二篇文章'
            },
            {
                id: 5,
                title: '这是第二篇文章'
            },
            {
                id: 6,
                title: '这是第二篇文章'
            },
            {
                id: 7,
                title: '这是第二篇文章'
            },
            {
                id: 8,
                title: '这是第二篇文章'
            },
            {
                id: 9,
                title: '这是第二篇文章'
            },
            {
                id: 10,
                title: '这是第二篇文章'
            },
            {
                id: 11,
                title: '这是第二篇文章'
            },
            {
                id: 12,
                title: '这是第二篇文章'
            },
            {
                id: 13,
                title: '这是第二篇文章'
            },
            {
                id: 14,
                title: '这是第二篇文章'
            }
        ]
        const articles = ref<Article[]>([])
        // 滚动加载更多
        const getArtiles = (page: number,size: number=10) =>{  // 传入你要显示的数据大小,其实也就是一个分页查询
            return new Promise((resolve)=>{  // 以一个promise对象返回,这里简单模拟一个异步数据请求。
                setTimeout(()=>{
                    // 某一页的数据
                    const data = _artciles.slice((page-1)*size, page*size); // 对源数据进行切割,返回一个新数组。
                    articles.value = [...articles.value,...data];  // 对数据进行拼接
                    resolve({  // 拿到数据后抛出
                        data,
                        page,
                        total: _artciles.length,
                        hasMore: page * size < _artciles.length  // 判断是否还需加载更多
                    })
                },500)
            })
        }
        return {
            articles,
            getArtiles
        }
    })
    

    这里的代码不是很难,且代码内都有注释,简单说一下,注意创建一个全部的假数据_artciles,也是源数据,然后创建一个空数组artciles用作数据展示,方法getArtiles它主要是在调用时传入当前所在页面的下标,如:1、2等,表示第几页数据,然后通过参数对源数据进行切割,拼接到需要显示的数据数组内。执行完成后通过promise的resolve方法,返回一些信息给调用者。

  2. 接下来就是页面渲染了。

    为useLoadMore函数提供回调函数,和需监测的DOM对象。

    <script setup lang="ts">
    import { toRefs, onMounted, ref} from 'vue'
    import { useArticleStore } from './store/article';
    import { useLoadMore } from './hooks/useLoadMore'
    const store = useArticleStore()
    const currentPage = ref(1)
    const { articles } = toRefs(store)
    
    let hasMore = ref<boolean>(true)
    const itemRef = ref(null)
    
    const { setHasMore } = useLoadMore(itemRef,()=>{
      handleNextPage(setHasMore);
    })
    
    const handleNextPage = async (setHasMore:(value:boolean) => boolean) => {
        currentPage.value++; // 页码加一
        const res = await store.getArtiles(currentPage.value)  // 获取新一页的数据
        if(!res.hasMore) { // 在数据仓库内置一个变量来记录是否还有更多数据
            setHasMore(false)  // 没有更多数据了
            hasMore.value = false
        }
    }
    
    onMounted(async () => { // 第一次加载数据,当数据加载出来之后
        await store.getArtiles(currentPage.value)
        console.log(itemRef.value,'被监测的DOM元素')
    })
    
    </script>
    
    <template>
      <section>
        <article 
            class="item"
            v-for="(item, index) in articles"
            :key="index"
            :ref="(el)=>(index === articles.length -1) ? (itemRef = el):''"
        >
            <div>{{index}} {{ item.title }}</div>
        </article>
    
        <div v-if="!hasMore">
            没有数据了
        </div>
      </section>
    </template>
    
    <style scoped>
    .item{
        height: 20vh;
    }
    </style>
    
    

    数据进行首次加载,并用itemRef记录最后一个数据的DOM结构。

    手动封装一个内容无限滚动的hooks,给你的项目增添技能难点前言 最近都在准备面试,好久没有写文章总结知识点了,这个点也

    完成之后开始加载useLoadMore函数,他就是我们今天的主角,它主要接受两个参数,第一个是nodeRef,第二个是fn回调函数。

    手动封装一个内容无限滚动的hooks,给你的项目增添技能难点前言 最近都在准备面试,好久没有写文章总结知识点了,这个点也

  3. 封装useLoadMore钩子函数:

    这个函数的功能就比较单一了,监测传入的nodeRef节点是否发生改变,改变了,我们就用把之前所监测的节点进行取消监测,然后监测新节点,监测的同时也使用IntersectionObserver监测DOM节点是否进入当前的视口(也就是滑到底部了,该加载更多内容了。)

    //  手写 加载更多的hook , 基于IntersectionObserver
    import { ref,watch } from "vue"
    import type { Ref } from 'vue'
    // hooks 就是把响应式封装到内部
    export const useLoadMore = (
        // Ref 类型
        // HTMLElement DOM节点
        // HTMLElement | numm ts 联合类型
        nodeRef : Ref<HTMLElement | null>,
        loadMore: ()=> void
    ) =>{
        let observer :IntersectionObserver | null = null
        const hasMore = ref<boolean>(true)
        // 监听最后元素的改变 , oberse 新的元素
        watch(nodeRef, (newNodeRef,oldNodeRef)=>{
            // 之前就有值, observer 已经实例化
            if(oldNodeRef && observer){
                observer.unobserve(oldNodeRef)
            }
            // 第一次
            if(newNodeRef){
                observer = new IntersectionObserver(([entry])=>{
                    // 只有一个,因为在这之前oldNodeRef的观察者被移除了。
                    if(entry.isIntersecting){
                        loadMore()   // 做handleNextPage
                    }
                })
                observer.observe(newNodeRef)
            }
        })
    
        watch(hasMore, (value)=>{
            if(observer){
                if(value && nodeRef.value){
                    observer.observe(nodeRef.value)
                }else{
                    // 释放observer 对象的
                    observer.disconnect()
                }
            }
        })
    
        return {
            hasMore,
            setHasMore: (value: Boolean)=>{
                hasMore.value = value
            }
        }
    }
    

    监听hasMore变量主要就是看看是否全部加载完毕,做到一个添加监听和释放监听的功能,且这个变量有返回的setHasMore函数去控制,也就是方法被返回到外部,如果需要改变也是在外部操作这个变量。

整个过程大概就是这样了,大家感兴趣可以将代码拷回去细品,然后自己去做测试。希望本文的内容对你有帮助,感谢你的观看哦!

转载自:https://juejin.cn/post/7406160036539203599
评论
请登录