- 作者:老汪软件技巧
- 发表时间: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函数去控制,也就是方法被返回到外部,如果需要改变也是在外部操作这个变量。
整个过程大概就是这样了,大家感兴趣可以将代码拷回去细品,然后自己去做测试。希望本文的内容对你有帮助,感谢你的观看哦!