Open maicFir opened 2 years ago
可以观察元素是否可见,由于目标元素与视口产生一个交叉区,我们可以观察到目标元素的可见区域,通常称这个API为交叉观察器
元素
API
前段时间内部系统业务需要,用IntersectionObserver实现了table中的上拉数据加载,如果有类似需求,希望本文能带给你一点思考和帮助
IntersectionObserver
table
正文开始...
参考官网vite快速启动一个项目 ::: details code
$ npm init vite@latest
::: 选择一个vue模板快速初始化一个页面后,我们添加路由页面 ::: details code
vue
npm i vue-router@4
::: 在已有项目上添加路由
// main.ts import { createApp } from 'vue'; import route from './router/index'; import App from './App.vue'; const app = createApp(App); app.use(route); app.mount('#app');
修改App模板,另外我们引入elementPlus,引入它主要是我们在实际项目中,我们用第三方 UI 库非常高频,在之前一篇文章中有提到虚拟列表优化大数据量,具体参考测试脚本把页面搞崩了。今天用交叉观察器也算是优化大数据量渲染的一种方案。 ::: details code
App
elementPlus
虚拟列表优化大数据量
交叉观察器
// App.vue <script setup lang="ts"> // This starter template is using Vue 3 <script setup> SFCs // Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup import { ElConfigProvider } from 'element-plus'; import { ref } from 'vue'; const zIndex = ref(1000); const size = ref('small'); </script> <template> <el-config-provider :size="size" :z-index="zIndex"> <router-view></router-view> </el-config-provider> </template> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
::: 创建router文件夹,新建index.ts,添加路由页面 ::: details code
router
index.ts
// router/index.ts import { createWebHashHistory, createRouter } from 'vue-router'; import HelloWorld from '../components/HelloWorld.vue'; import ShopListPage from '../view/shopList/Index.vue'; const routes = [ { path: '/hello', component: HelloWorld }, { path: '/', component: ShopListPage } ]; const router = createRouter({ history: createWebHashHistory(), routes }); export default router;
:::
我们新建一个view/shopList目录,在shopList中新建一个Index.vue开始今天的栗子。
view/shopList
shopList
Index.vue
本地开发环境安装mockjs模拟接口数据
mockjs
npm i mockjs --save-dev
新建mock我们使用它模拟接口随机数据,我们会在main.ts引入该mock/index.js ::: details code
mock
main.ts
mock/index.js
// mock/index.ts import Mockjs from 'mockjs'; import mockFetch from 'mockjs-fetch'; // 拦截mock mockFetch(Mockjs); // 生成随机长度的数组 const createMapRandom = (len: number) => { const data = new Array(len); return data.fill('Maic'); }; Mockjs.mock('/shoplist/list.json', () => { return { code: 0, data: Mockjs.mock({ 'list|10': [ { 'id|+1': createMapRandom(10).map(() => Mockjs.mock('@id')), 'adress|1': createMapRandom(10).map(() => Mockjs.mock('@city')), 'age|1': createMapRandom(10).map(() => Mockjs.mock('@integer(0,100)')), 'name|1': createMapRandom(10).map(() => Mockjs.mock('@cname')) } ] }) }; });
::: 注意我们在使用mockjs时,我们使用了另外一个库mockjs-fetch,如果在项目中使用fetch做ajax请求,那么必须要使用这个库拦截mock请求,在默认情况下,如果你使用的是axios库,那么mock会默认拦截请求。
mockjs-fetch
fetch
ajax
axios
在view/shopList目录下,我们创建Index.vue
<template> <div class="shopList"> <h3>intersectionObserver交叉器实现上拉加载</h3> <el-table :data="tableData" border stripe style="width: 100%"> <el-table-column type="index" width="50" /> <el-table-column property="id" label="id" width="180" /> <el-table-column property="name" label="Name" width="180" /> <el-table-column property="adress" label="Address" /> <el-table-column property="age" label="Age" /> </el-table> <div @click="handleMore" v-if="hasMore">点击加载更多</div> <div v-else>没有数据啦</div> </div> </template>
对应的js,这段js逻辑非常简单,就是请求模拟的mock数据,然后设置table所需要的数据,点击加载更多就继续请求,如果没有数据了,就显示没有数据。 ::: details code
js
点击加载更多
<script setup lang="ts"> import { reactive, ref, onMounted } from "vue"; import { ElTable, ElTableColumn } from "element-plus"; import "element-plus/dist/index.css"; const hasMore = ref(false); const tableData = ref([]); const condation = reactive({ pageParams: { page: 1, pageSize: 10, }, }); // TODO 请求数据 const featchList = async () => { const res = await fetch("/shoplist/list.json", { method: "GET", headers: { "Content-Type": "application/json", }, body: JSON.stringify(condation.pageParams), }); const json = await res.json(); tableData.value = tableData.value.concat(json.data.list); }; onMounted(() => { featchList(); }); // TODO 加载更多 const handleMore = () => { featchList(); }; </script>
::: 我们用vite初始化的项目是vue3,在vue3的script我们使用了setup,那么我们在script中不再用返回一个对象,申明的方法和变量可以直接在模板中使用,这里与组合式API有点区别,但是从功能上并没有什么区别。
vite
vue3
script
setup
组合式API
在传统上,我们实现上拉加载,我们会监听滚动条到底部的距离,我们计算滚动条距离顶部位置、浏览器可视区域的高度、body 的高度,监听滚动事件,判断scrollTop + clientHeight > bodyScrollHeight,然后就判断是否需要加载下一页。
scrollTop + clientHeight > bodyScrollHeight
监听滚动事件,我们会加防抖处理事件,即使这样scroll事件也会高频触发,这样也会影响性能。
scroll
因此我们使用IntersectionObserver这个API实现上拉加载。
我们看下IntersectionObserver这个 API
// callback是一个回调函数,options是可配置的参数 var observer = new IntersectionObserver(callback, options); // target1是一个具体的dom元素 observer.observe(target1); // 开始观察 observer.observe(target2); observer.unobserve(target); // 停止观察 observer.disconnect(); // 停止观察
我们可以在页面中用observer可以观察多个dom,同时我们也需要知道new IntersectionObserver()这个是异步的,并不会随着页面的滚动而时时触发,它只会在线程空闲下来才会执行,因此它在事件循环中,优先级很低,只有等其他任务执行完了,浏览器有了空闲才会执行它。
observer
dom
new IntersectionObserver()
当目标元素可见时,会触发callback,另一次是当元素完全不可见时也会触发该callback
callback
const options = {}; var observer = new IntersectionObserver((entries, observer) => { console.log(entries); // entries 是一个数组,监听几个dom就会有几个 }, options);
在IntersectionObserver中的entries第一个参数里,其中有几个参数我们需要了解下 ::: details code
entries
// entries type clientRect = { top: number; bottom: number, left: number, right: number, width: number, height: number } const entriesRes = { time: 12334, rootBounds: { bottom: 920, height: 1024, left: 0, right: 1024, top: 0, width: 920 } as clientRect, boundingClientRect: { ... } as clientRect, intersectionRect: { } as clientRect, intersectionRatio: 0, target: dom }; const entries = [entriesRes] // observer { delay: 0 root: null rootMargin: "0px 0px 0px 0px" thresholds: [0] trackVisibility: false }
::: 在第二个参数options中可配置参数
options
var options = { threshold: [0, 0.5, 1], root: document.getElementById('box1') };
threshold这个可以设置目标元素可见范围在 0,50%,100%时触发回调callback,root就是可以目标元素所在的祖先节点
threshold
root
我们花了一些时间了解IntersectionObserver这个API,接下来我们用它实现一个上拉加载。
// 关键代码 ... // 自定义一个上拉加载的指令 const vScrollTable = { created: (el, binding, vnode, prevVnod) => { handleScrollTable(el, binding); }, };
然后就是handleScrollTable这个方法 ::: details code
handleScrollTable
... // 自定义指令的created中调用该方法 const handleScrollTable = (el, binding) => { const { infiniteScrollDisable, cb } = binding.value; // 如果el不存在,则禁止后面IntersectionObserver的实例化 if (!el && !cb) { return; } // 核心上拉加载代码 const intersectionObserver = new IntersectionObserver((enteris, observer) => { // console.log(enteris, observer); const [curentEnteris] = enteris; const { intersectionRatio } = curentEnteris; // 不可见的时候,禁止加载 if (intersectionRatio <= 0) return; // 设置一个可以加载更多的开关 if (infiniteScrollDisable) { cb(); } }); // 开始监听 intersectionObserver.observe(el); };
在模板里我们只需在目标元素上绑定指令就行
... <div class="load-more-btn" @click="handleMore" v-if="hasMore" v-scroll-table="{ cb: handleMore, infiniteScrollDisable: hasMore }"> 点击加载更多<el-icon :class="[loading ? 'is-loading' : '']"> <component :is="Loading"></component> </el-icon>({{ tableData.length }}/{{ total }}) </div> <div v-else>没有数据啦</div>
我们直接在元素上绑定自定义的指令v-scroll-table="{cb: handleMore,infiniteScrollDisable: hasMore}"就行
v-scroll-table="{cb: handleMore,infiniteScrollDisable: hasMore}"
完整的全部示例见下面代码 ::: details code
<!--shopList/Index.vue--> <template> <div class="shopList"> <h3>intersectionObserver交叉器实现上拉加载</h3> <el-table :data="tableData" border stripe style="width: 100%" v-loading="loading"> <el-table-column type="index" width="50" /> <el-table-column property="id" label="id" width="180" /> <el-table-column property="name" label="Name" width="180" /> <el-table-column property="adress" label="Address" /> <el-table-column property="age" label="Age" /> </el-table> <div class="load-more-btn" @click="handleMore" v-if="hasMore" v-scroll-table="{ cb: handleMore, infiniteScrollDisable: hasMore }"> 点击加载更多<el-icon :class="[loading ? 'is-loading' : '']"> <component :is="Loading"></component> </el-icon>({{ tableData.length }}/{{ total }}) </div> <div v-else>没有数据啦</div> </div> </template> <script setup lang="ts"> import { reactive, ref, onMounted } from 'vue'; import { ElTable, ElTableColumn, ElIcon } from 'element-plus'; import { Loading } from '@element-plus/icons-vue'; import 'element-plus/dist/index.css'; const hasMore = ref(false); const tableData = ref([]); const loading = ref(false); const condation = reactive({ pageParams: { page: 1, pageSize: 10 } }); const total = ref(100); // TODO 请求数据 const featchList = async () => { const res = await fetch('/shoplist/list.json', { method: 'GET', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(condation.pageParams) }); const json = await res.json(); tableData.value = tableData.value.concat(json.data.list); hasMore.value = true; if (total.value === tableData.value.length) { hasMore.value = false; // 没有更多了 } loading.value = false; }; onMounted(() => { featchList(); }); // TODO 加载更多 const handleMore = () => { loading.value = true; // 加一个延时1s显示loading效果 setTimeout(() => { featchList(); }, 1000); }; const handleScrollTable = (el, binding) => { const { infiniteScrollDisable, cb } = binding.value; // 如果el不存在,则禁止后面IntersectionObserver的实例化 if (!el && !cb) { return; } const intersectionObserver = new IntersectionObserver((enteris, observer) => { // console.log(enteris, observer); const [curentEnteris] = enteris; const { intersectionRatio } = curentEnteris; // 不可见的时候,禁止加载 if (intersectionRatio <= 0) return; // 设置一个可以加载更多的开关 if (infiniteScrollDisable) { cb(); } }); // 开始监听 intersectionObserver.observe(el); }; // 自定义一个上拉加载的指令 const vScrollTable = { created: (el, binding, vnode, prevVnod) => { handleScrollTable(el, binding); } }; </script> <style> .load-more-btn { display: flex; align-items: center; justify-content: center; } </style>
::: 打开页面,我们可以看到 点击加载操作就会加载更多,当滚动到底部时,就会加载更多。当数据加载完时,我们就设置hasMore = false;
hasMore = false
核心代码非常简单,就是利用IntersectionObserver监测目标元素的可见,当目标元素可见时,我们加载更多,在目标元素不可见时,我们禁止加载更多,当数据加载完了,就禁止加载更多。
1.使用vite与vue3模板搭建一个简易的demo模板,结合vue-router、mockjs、elementPlus,fetch实现基本路由搭建,数据请求
demo
vue-router
2.了解核心IntersectionObserverAPI,用vue3指令,实现加载更多,这里用指令的原因是因为可以在多个类似模块复用指令内部那段逻辑,这样可以提高我们业务功能的复用能力
3.我们看到在vue3中script中使用了setup,在注册组件和模板上使用的变量,当前组件可以直接使用。如果你未在script中使用setup,那么就要与组合式API一样使用setup,返回模板中使用的变量以及绑定的方法,并且注册局部组件依旧要像以前方式一样在component中引入
component
4.更多关于IntersectionObserver的实践,我们可以用它做图片懒加载,视频播放暂停与播放等,具体可以参考这篇文章IntersectionObserver
图片懒加载
视频播放暂停与播放
5.本文示例源码地址intersectionObserver
交叉观察器 IntersectionObserver
前段时间内部系统业务需要,用
IntersectionObserver
实现了table
中的上拉数据加载,如果有类似需求,希望本文能带给你一点思考和帮助正文开始...
vite 初始化一个项目
参考官网vite快速启动一个项目 ::: details code
::: 选择一个
vue
模板快速初始化一个页面后,我们添加路由页面 ::: details code::: 在已有项目上添加路由
修改
App
模板,另外我们引入elementPlus
,引入它主要是我们在实际项目中,我们用第三方 UI 库非常高频,在之前一篇文章中有提到虚拟列表优化大数据量
,具体参考测试脚本把页面搞崩了。今天用交叉观察器
也算是优化大数据量渲染的一种方案。 ::: details code::: 创建
router
文件夹,新建index.ts
,添加路由页面 ::: details code:::
我们新建一个
view/shopList
目录,在shopList
中新建一个Index.vue
开始今天的栗子。本地开发环境安装
mockjs
模拟接口数据新建
mock
我们使用它模拟接口随机数据,我们会在main.ts
引入该mock/index.js
::: details code::: 注意我们在使用
mockjs
时,我们使用了另外一个库mockjs-fetch
,如果在项目中使用fetch
做ajax
请求,那么必须要使用这个库拦截mock
请求,在默认情况下,如果你使用的是axios
库,那么mock
会默认拦截请求。在
view/shopList
目录下,我们创建Index.vue
对应的
js
,这段js
逻辑非常简单,就是请求模拟的mock
数据,然后设置table
所需要的数据,点击加载更多
就继续请求,如果没有数据了,就显示没有数据。 ::: details code::: 我们用
vite
初始化的项目是vue3
,在vue3
的script
我们使用了setup
,那么我们在script
中不再用返回一个对象,申明的方法和变量可以直接在模板中使用,这里与组合式API
有点区别,但是从功能上并没有什么区别。在传统上,我们实现上拉加载,我们会监听滚动条到底部的距离,我们计算滚动条距离顶部位置、浏览器可视区域的高度、body 的高度,监听滚动事件,判断
scrollTop + clientHeight > bodyScrollHeight
,然后就判断是否需要加载下一页。监听滚动事件,我们会加防抖处理事件,即使这样
scroll
事件也会高频触发,这样也会影响性能。因此我们使用
IntersectionObserver
这个API
实现上拉加载。我们看下
IntersectionObserver
这个 API我们可以在页面中用
observer
可以观察多个dom
,同时我们也需要知道new IntersectionObserver()
这个是异步的,并不会随着页面的滚动而时时触发,它只会在线程空闲下来才会执行,因此它在事件循环中,优先级很低,只有等其他任务执行完了,浏览器有了空闲才会执行它。当目标元素可见时,会触发
callback
,另一次是当元素完全不可见时也会触发该callback
在
IntersectionObserver
中的entries
第一个参数里,其中有几个参数我们需要了解下 ::: details code::: 在第二个参数
options
中可配置参数threshold
这个可以设置目标元素可见范围在 0,50%,100%时触发回调callback
,root
就是可以目标元素所在的祖先节点我们花了一些时间了解
IntersectionObserver
这个API
,接下来我们用它实现一个上拉加载。然后就是
handleScrollTable
这个方法 ::: details code:::
在模板里我们只需在目标元素上绑定指令就行
我们直接在元素上绑定自定义的指令
v-scroll-table="{cb: handleMore,infiniteScrollDisable: hasMore}"
就行完整的全部示例见下面代码 ::: details code
::: 打开页面,我们可以看到 点击加载操作就会加载更多,当滚动到底部时,就会加载更多。当数据加载完时,我们就设置
hasMore = false
;核心代码非常简单,就是利用
IntersectionObserver
监测目标元素的可见,当目标元素可见时,我们加载更多,在目标元素不可见时,我们禁止加载更多,当数据加载完了,就禁止加载更多。总结
1.使用
vite
与vue3
模板搭建一个简易的demo
模板,结合vue-router
、mockjs
、elementPlus
,fetch
实现基本路由搭建,数据请求2.了解核心
IntersectionObserver
API,用vue3
指令,实现加载更多,这里用指令的原因是因为可以在多个类似模块复用指令内部那段逻辑,这样可以提高我们业务功能的复用能力3.我们看到在
vue3
中script
中使用了setup
,在注册组件和模板上使用的变量,当前组件可以直接使用。如果你未在script
中使用setup
,那么就要与组合式API
一样使用setup
,返回模板中使用的变量以及绑定的方法,并且注册局部组件依旧要像以前方式一样在component
中引入4.更多关于
IntersectionObserver
的实践,我们可以用它做图片懒加载
,视频播放暂停与播放
等,具体可以参考这篇文章IntersectionObserver5.本文示例源码地址intersectionObserver