• 作者:老汪软件技巧
  • 发表时间:2024-08-26 04:02
  • 浏览量:

前言

最近都在准备面试,好久没有写文章总结知识点了,这个点也是在面试中常见的问题:你了解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(),以及一些内置的选项如 ref、reactive、computed 和 watch 等来组织和管理组件的逻辑。

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

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

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

了解IntersectionObserver

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

IntersectionObserver的使用:

创建Observer实例对象:

观察目标元素:

取消观察:

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

图片懒加载的实现:

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

Demo代码放下面:

html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lazyloadtitle>
    <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进入视口了,说明需要开始加载更多数据了。

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

首先做好仓库的假数据:

这里使用的是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方法,返回一些信息给调用者。

接下来就是页面渲染了。

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


<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结构。

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

封装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 : Refnull>,
    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函数去控制,也就是方法被返回到外部,如果需要改变也是在外部操作这个变量。

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