有关网站设计与制作的论文,乌市网站建设为,经营网站的备案,wordpress 主机伪静态404.php seo一、引入 在前端性能优化中#xff0c;关于图片/视频等内容的懒加载一直都是优化利器。当用户看到对应的视图模块时#xff0c;才去请求加载对应的图像。 原理也很简单#xff0c;通过浏览器提供的 IntersectionObserver - Web API 接口参考 | MDN (mozilla.org)#xff0c…一、引入 在前端性能优化中关于图片/视频等内容的懒加载一直都是优化利器。当用户看到对应的视图模块时才去请求加载对应的图像。 原理也很简单通过浏览器提供的 IntersectionObserver - Web API 接口参考 | MDN (mozilla.org)观察“哪个元素和视口交叉”从而进行懒加载。 这个API具有很好的性能因为它的监听是异步的不会影响JS的主线程所以比传统的“监听页面滚动”更佳。关于API的使用这里就不做过多说明了主要操作如下
const DOM document.querySelector(img)
const io new IntersectionObserver((entries) {entries.forEach((k) {//回调函数可以利用 k.target 是否和我们要监听的DOM元素相等来判断当前是否是我们要监听的目标元素if(k.target DOM){ /* 做懒加载的操作 */}});
}, {/*一些配置详见MDN文档*/});
io.observe(DOM) //添加监听 二、可优化的点 值得注意的是一个observer实例可以监听多个DOM元素。如果我们需要封装一个图片组件并实现它的懒加载那么“每个组件都创建一个IntersectionObserver实例” 显然是不划算的如果页面上有上百个图片就会创建出上百个实例。 针对这种情况并且不想破坏组件的封装性于是考虑把实例提升到全局封装一个hook从而每个组件都能自行添加入该实例的观察对象中。但是监听的回调函数是创建实例的时候就决定的后续添加进入的DOM元素在回调函数中无法判断“是否轮到自己”了。 三、观察者模式 有什么办法能够让DOM元素动态的进入回调函数呢 我们可以利用对象引用地址不变的特性动态的往对象里添加数据这样在回调函数触发时就能够取出正确的数据了 这里我的灵感其实来源于Vue3的响应式原理 收集依赖 -- 监听 -- 触发依赖。Vue3是多对多的发布-订阅模式 这里是 一对多的观察者模式
/**回调函数的类型*/
type ObserverCallback (entryData: IntersectionObserverEntry) void
/** 键是DOM元素值是该元素的回调函数Set 考虑到可能一个元素会有多个回调 */
const watchMap new WeakMapElement, SetObserverCallback()
const io new IntersectionObserver((entries) {entries.forEach((k) {const set watchMap.get(k.target)if(set){set.forEach((fn) fn(k)) //从weakMap中取出对应的监听事件触发} });
}, {/*一些配置详见MDN文档*/}); 剩下要做的就是“依赖收集”了。基于面向对象的思想 可以创建多个实例多处复用互不干扰。 当有DOM元素需要被监听时添加进weakMap中需要取消监听时移除 observer触发回调时取出对应的元素的依赖执行回调函数。 手写过观察者模式或者发布订阅模式的小伙伴应该对下面的代码构造很熟悉。
/**视口监听器 - 观察者模式 */
export class ViewportObserverWatcher {/**IntersectionObserver 实例 */io: IntersectionObserver/**当前正在监听的元素的weakMap */watchMap new WeakMapElement, SetObserverCallback()constructor(options?: IntersectionObserverInit) {this.io new IntersectionObserver((entries) {entries.forEach((k) {this.watchMap.get(k.target)?.forEach((fn) fn(k)) //从weakMap中取出对应的监听事件触发});}, options);}/**添加对元素的一个监听回调可以选择触发条件* param target 目标元素* param callback 回调函数* param condition 触发回调条件 true | false | undefined 分别对应 与视口边界交叉 | 不与视口交叉 | 都*/addWatch (target: Element, callback: ObserverCallback, condition?: boolean) {const _callback: ObserverCallback (k) {if (condition undefined) { }//无论如何都触发 else if ((condition ! k.isIntersecting)) return //当触发条件和实际情况不相同时不触发 callback(k)}if (this.watchMap.has(target)) {this.watchMap.get(target)!.add(_callback)} else {this.io.observe(target)this.watchMap.set(target, new Set([_callback]))}}/**取消对元素的某个回调 */removeWatch (target: Element, callback: ObserverCallback) {const set this.watchMap.get(target)if (set) {set.delete(callback)if (set.size 0) {this.watchMap.delete(target)this.io.unobserve(target)}}}/**取消对该元素的全部回调 */cancelWatch (target: Element) {this.watchMap.delete(target)this.io.unobserve(target)}
}
四、写个Hook吧
1. 元素创建时加入io的监听
2. 触发懒加载之后取消对该元素的监听。
3. 依赖项变化后重复前面的逻辑。 4. 只要是元素都能进行监听不只是图片/视频。有需要使用到该功能的元素都能使用。
import { DependencyList, RefObject, useEffect, useRef } from react;/**视口监听器 - 单例模式 */
const viewportObserver new ViewportObserverWatcher() //注如果你是NextJs, 在NextJS build的时候不能直接实例化IntersectionObserver否则会报错 因为在走服务端代码 可以先设置为null后续给这个变量赋值/**懒加载Hook。懒加载触发后将会取消监听* param watchRef 要监听的DOM元素* param onEntering 元素进入视口的回调函数* param onDestroy useEffect的return中要做的事* param deps useEffect的依赖数组 当什么变化时需要重新开始懒加载流程*/
const useLazyLoad (watchRef: RefObjectHTMLElement, onEntering: ObserverCallback, onDestroy?: () void, deps: DependencyList []) {/**是否完成懒加载 */const isLazySuccess useRef(false);useEffect(() {if (!watchRef.current) return; const callback: ObserverCallback (k) {//因为只要和视口在交叉就会不断触发这个函数故需要使用一个标识符来限制 if (isLazySuccess.current false) {onEntering(k)isLazySuccess.current true;viewportObserver!.removeWatch(watchRef.current!, callback) //加载完成就取消监听onEntering(k)}}viewportObserver.addWatch(watchRef.current, callback, true)return () {if (watchRef.current viewportObserver) viewportObserver.removeWatch(watchRef.current, callback); //卸载时也要取消监听 isLazySuccess.current false;onDestroy onDestroy()};}, deps)
}
使用方法 核心思想到了视口才赋值真实路径其它时候使用占位符。
/**视频组件 */
export default function Video({ src, className, otherProps }: VideoProps) {const outRef useRefHTMLDivElement(null); //被监听的元素const [realSrc, setRealSrc] useStatestring(); //存放展示的src如果还没到视口就不展示useLazyLoad(outRef, () setRealSrc(src));return (div className{cn(className, rounded)} ref{outRef}{/* 其它逻辑.... */}{/* 正常展示视频 */}{realSrc video src{realSrc} {...otherProps} /}{/* 其它逻辑.... */}/div);
}
五、使用效果 结合前面文章写的的瀑布流组件实现以下效果 图片链接来源于 岁月小筑随机图片API接口-随机背景图片-随机图片API (xjh.me)