背景
有一天我在逛淘宝的时候发现一个现象,就是淘宝pc 端的搜索列表页,在滚动的时候,它的那些图片总有一种从无到有的感觉,作为一个前端人,我就很好奇他是做了什么优化,是不是对元素做了懒加载处理。没进入视口的内容只做占位处理。于是我就检查了一下元素,果不其然,第一张图是列表第一个商品的dom结果,第二张图是列表中最后一个商品的dom,它还没有进入视口,所以只有一个高度为 260 的占位的 div
。 感兴趣的小伙伴也可以去淘宝上验证一下。
思考
我在想能不能把这个指令做得更通用一点,只处理图片好像有点局限性,我希望能做成一个对任何组件都能实现懒加载。
业务场景
- 背景:由于我们的页面比较大,在一个页面上会加载比较多这样的模块,大致的样式看下图。
- 问题:由于页面一加载,就会请求页面所需要的所有数据,往往一个页面所需要请求的数据,多达几十个,造成页面加载非常的缓慢。且我们都知道并不是先发出去的请求就会先回来,有时候遇到一些情况是这样的,下面的组件发出去的请求都回来了,但是视口的请求还没回来。导致没有意义的等待。
- 处理方案:
实现
Intersection Observer API
交叉观察器 API(Intersection Observer API)提供了一种异步检测目标元素与祖先元素或顶级文档的视口相交情况变化的方法。
具体的可以查看 MDN 文档 。这里头写的非常的清楚。简单地来说,我就可以通过这个api 来判断元素是否已经进入我们的视口。而且这个api 并不存在性能问题,所以大家可以放心使用。
自定义hooks useVisibilityHook
代码很简单,就是用于监听组件或者元素是否已经进入视口
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
const defaultConfig = {
root: null,
threshold: 0.2,
rootMargin: "0px",
};
export function useVisibility(options = {}, initialVisible = false) {
const isVisible = ref(initialVisible);
const element = ref(null);
const observer = ref(null);
onMounted(() => {
// 初始化监听器
observer.value = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
isVisible.value = true;
observer.value.disconnect();
}
},
{
...defaultConfig,
...options,
}
);
});
watch(isVisible, (newVisible) => {
if (newVisible) {
isVisible.value = true;
}
});
onMounted(() => {
if (element.value) {
observer.value.observe(element.value);
}
});
onBeforeUnmount(() => {
// 卸载的时候要注意 disconnect
if (observer.value) {
observer.value.disconnect();
}
});
return {
element,
isVisible,
};
}
封装懒加载组件
我们接触自定义的hooks useVisibilityHook 封装了下面这样的组件,由于我不想引入类似 ant-design-vue 这样的组件库,来增加复杂度,小伙伴们可以根据自己的需要来修改
<template>
<div :style="{ minHeight: '200px' }" ref="element">
<template v-if="isVisible">
<slot></slot>
</template>
<template v-else>
<!-- 使用具名插槽 fallback,提供默认内容 -->
<slot name="fallback">组件还未进入视口,组件进入视口后渲染...</slot>
</template>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { useVisibility } from "../hooks/useVisibilityHook";
export default defineComponent({
props: {
visible: {
type: Boolean,
default: false,
},
options: {
type: Object,
default: () => ({}),
},
},
setup(props) {
const { element, isVisible } = useVisibility(props.options, props.visible);
return {
element,
isVisible,
};
},
});
</script>
总计
至此,我们的工作就做完了。我把代码放到 sandbox 中,方便大家直接预览效果