组件简介
Lazyload懒加载,当页面需要加载大量内容时,使用懒加载可以实现延迟加载页面可视区域外的内容,从而使页面加载更流程。
如何使用
Lazyload是Vue指令,使用前需要对指令进行注册
1. 注册指令
javascript
import { createApp } from 'vue';
import { Lazyload } from 'vant';
const app = createApp();
app.use(Lazyload); // 注册时可以配置额外的选项 app.use(Lazyload, { lazyComponent: true, });
2. 将v-lazy指令的值设置为需要懒加载的图片
ini
<img v-for="img in imageList" v-lazy="img" />
3. 背景图片懒加载,要使用v-lazy:background-image
arduino
<div v-for="img in imageList" v-lazy:background-image="img" />
4. 组件懒加载,将需要懒加载的组件放在lazy-component标签中
javascript
// 注意注册时设置"lazyComponent"选项
app.use(Lazyload, { lazyComponent: true, });
<lazy-component>
<img v-for="img in imageList" v-lazy="img" />
</lazy-component>
源码解析
1. vue-lazyload/index.js
index.js是lazyload组件的入口文件,该js导出一个Lazyload对象,Lazyload对象会声明一个install方法, install函数是组件注册的必用函数,这里不再赘述。
php
import Lazy from './lazy';
import LazyComponent from './lazy-component';
import LazyContainer from './lazy-container';
import LazyImage from './lazy-image';
export const Lazyload = {
/*
* install function
* @param {App} app
* @param {object} options lazyload options
*/
install(app, options = {}) {
const LazyClass = Lazy();
// 创建了一个laze实例
const lazy = new LazyClass(options);
// 创建了一个lazeContainer实例
const lazyContainer = new LazyContainer({ lazy });
// 将laze实例放到vue实例的global属性上
app.config.globalProperties.$Lazyload = lazy;
// 如果options中存在lazyComponent, 注册LazyComponent组件,这里是我们上文"如何使用第4点"强调使用组件的话需要增加传参lazyComponent: true了
if (options.lazyComponent) {
app.component('LazyComponent', LazyComponent(lazy));
}
// 根据传参lazeImage注册LazyImage组件
if (options.lazyImage) {
app.component('LazyImage', LazyImage(lazy));
}
// 注册指令lazy, 我们就知道为啥指令是v-lazy了啦
app.directive('lazy', {
beforeMount: lazy.add.bind(lazy),
updated: lazy.update.bind(lazy),
unmounted: lazy.remove.bind(lazy),
});
// 注册指令lazy-container
app.directive('lazy-container', {
beforeMount: lazyContainer.bind.bind(lazyContainer),
updated: lazyContainer.update.bind(lazyContainer),
unmounted: lazyContainer.unbind.bind(lazyContainer),
});
},
};
2. vue-lazyload/lazy.js
lazy.js导入了很多工具函数,导出一个匿名函数,外部执行这个匿名函数,会return一个类class Lazy。
函数可以将内部逻辑和变量放到当前函数做作用域中,避免外包访问和修改。
2.1 constructor构造函数
constuctor函数初始化一些配置和函数
kotlin
import { nextTick } from 'vue';
import { inBrowser, getScrollParent } from '@vant/use';
import {
remove,
on,
off,
throttle,
supportWebp,
getDPR,
getBestSelectionFromSrcset,
hasIntersectionObserver,
modeType,
ImageCache,
} from './util';
import { isObject } from '../../utils';
import ReactiveListener from './listener';
const DEFAULT_URL =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const DEFAULT_EVENTS = [
'scroll',
'wheel',
'mousewheel',
'resize',
'animationend',
'transitionend',
'touchmove',
];
const DEFAULT_OBSERVER_OPTIONS = {
rootMargin: '0px',
threshold: 0,
};
export default function () {
return class Lazy {
constructor({
preLoad,
error,
throttleWait,
preLoadTop,
dispatchEvent,
loading,
attempt,
silent = true,
scale,
listenEvents,
filter,
adapter,
observer,
observerOptions,
}) {
this.mode = modeType.event;
this.listeners = [];
this.targetIndex = 0;
this.targets = [];
this.options = {
silent,
dispatchEvent: !!dispatchEvent,
throttleWait: throttleWait || 200,
preLoad: preLoad || 1.3,
preLoadTop: preLoadTop || 0,
error: error || DEFAULT_URL,
loading: loading || DEFAULT_URL,
attempt: attempt || 3,
scale: scale || getDPR(scale),
ListenEvents: listenEvents || DEFAULT_EVENTS,
supportWebp: supportWebp(),
filter: filter || {},
adapter: adapter || {},
observer: !!observer,
observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS,
};
this.initEvent();
this.imageCache = new ImageCache({ max: 200 });
this.lazyLoadHandler = throttle(
this.lazyLoadHandler.bind(this),
this.options.throttleWait,
);
this.setMode(this.options.observer ? modeType.observer : modeType.event);
}
...
}
}
2.2 remove & on & off
-
remove是移除数组中的元素
-
on和off是通过addEventListener绑定和解绑函数
-
为什么addEventListener要设置capture和passive呢?
capture: false表示监听器在 事件冒泡阶段 触发(这是最常用的模式)
passive: true明确告诉浏览器:此监听器绝不会调用
event.preventDefault()。浏览器因此可以立即执行默认行为(比如滚动、缩放),无需等待 JS 执行完毕,从而提升流畅度。
typescript
// 移除数组中的元素
export function remove(arr, item) {
if (!arr.length) return;
const index = arr.indexOf(item);
if (index > -1) return arr.splice(index, 1);
}
// 绑定事件
export function on(el, type, func) {
el.addEventListener(type, func, {
capture: false, // 默认在**冒泡阶段**触发事件
passive: true, // 明确告诉浏览器:**此监听器绝不会调用 `event.preventDefault()`**
});
}
// 解绑事件
export function off(el, type, func) {
// 第三个参数是一个布尔值,指定需要移除的事件监听器是否为捕获监听器,默认false
el.removeEventListener(type, func, false);
}
2.3 throttle节流函数
- 第一次立即执行, lastRun = 0, Date.now() - lastRun是一个很大的数,肯定比delay大,所以第一次会立即执行
- 第二次注入一个定时器,等待delay秒后执行,此时timeout有值且是一个数字,
- 等待delay秒后执行runCallback函数,timeout被清空,lastRun被重新赋值为当前时间
- 事件在时间间隔为delay秒内触发,就会继续注入setTimeout,等待执行。。。如此一直循环
- 只要当没有setTimeout回调事件之后,最后会在elapsed >= delay条件下再执行一次runCallback
ini
export function throttle(action, delay) {
let timeout = null;
let lastRun = 0;
return function (...args) {
if (timeout) {
return;
}
const elapsed = Date.now() - lastRun;
const runCallback = () => {
lastRun = Date.now();
timeout = false;
action.apply(this, args);
};
// 第一次立即执行
if (elapsed >= delay) {
runCallback();
} else {
// 第二次注入一个定时器
timeout = setTimeout(runCallback, delay);
}
};
}
2.4 supportWebp & getDPR
| 函数 | 作用 | 典型用途 |
|---|---|---|
supportWebp() |
检测是否支持 WebP 图片格式 | 优先加载 WebP,节省带宽、提升加载速度 |
getDPR(scale) |
获取设备像素比(DPR) ,获取当前设备的物理像素与 CSS 像素的比率(DPR) | 为高清屏加载高分辨率图片,避免模糊 |
- inBrowser是判断条件是typeof window是否存在值
const inBrowser = 'undefined' != typeof window; - canvas.toDataURL('image/webp') 如果浏览器支持WebP格式的图片,就会返回'data:image/webp'...图片base64格式的字符串
如果不支持,就会降级为PNG, 返回"data:image/png..."
哇哦,知识面又拓宽了~
javascript
export function supportWebp() {
// 不是browser, 直接return
if (!inBrowser) return false;
let support = true;
try {
const elem = document.createElement('canvas');
// 检测canvas是否可用
if (elem.getContext && elem.getContext('2d')) {
// 尝试将canvas导出为WebP格式的数据URL
support = elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
} catch (err) {
support = false;
}
return support;
}
export const getDPR = (scale = 1) =>
inBrowser ? window.devicePixelRatio || scale : scale;
2.5 getBestSelectionFromSrcset
这个函数 getBestSelectionFromSrcset 的作用是:
根据图片容器的缩放后宽度(考虑设备像素比),从
<img>元素的data-srcset属性中智能选择最合适的一张图片源(URL) ,优先选择 WebP 格式,并适配高清屏。
ini
export function getBestSelectionFromSrcset(el, scale) {
// 判断是img标签 且 存在data-srcset属性
if (el.tagName !== 'IMG' || !el.getAttribute('data-srcset')) return;
// 注意不是标准 `srcset`,是自定义属性data-srcset
let options = el.getAttribute('data-srcset');
const container = el.parentNode;
// 计算父容器的宽度,考虑缩放比例
const containerWidth = container.offsetWidth * scale;
let spaceIndex;
let tmpSrc;
let tmpWidth;
// 标准的srcset格式image-400.jpg 400w, image-800.webp 800w, image-1200.jpg 1200w
options = options.trim().split(',');
const result = options.map((item) => {
item = item.trim();
spaceIndex = item.lastIndexOf(' ');
if (spaceIndex === -1) {
tmpSrc = item;
tmpWidth = 999998;// 无宽度描述,视为"兜底图"
} else {
tmpSrc = item.substr(0, spaceIndex);
// 提取 "800w" → 去掉末尾 'w'(通过 -2 截断)
tmpWidth = parseInt(
item.substr(spaceIndex + 1, item.length - spaceIndex - 2),
10,
);
}
return [tmpWidth, tmpSrc];
});
// 从大到小排序
result.sort((a, b) => {
if (a[0] < b[0]) { // a宽度小,放后面
return 1;
}
if (a[0] > b[0]) { // a宽度大,放前面
return -1;
}
if (a[0] === b[0]) {
// 宽度相同:WebP 优先
if (b[1].indexOf('.webp', b[1].length - 5) !== -1) {
return 1;
}
if (a[1].indexOf('.webp', a[1].length - 5) !== -1) {
return -1;
}
}
return 0;
});
let bestSelectedSrc = '';
let tmpOption;
/** 排序完成后的数据大概如下,目的是找到最契合容器宽度的图片src后返回
[
[1200, 'img-1200.webp'],
[800, 'img-800.jpg'],
[400, 'img-400.jpg']
] **/
for (let i = 0; i < result.length; i++) {
tmpOption = result[i];
bestSelectedSrc = tmpOption[1];
const next = result[i + 1];
// 如果下一个图片的宽度小于父级容器的宽度,则获取当前值
if (next && next[0] < containerWidth) {
bestSelectedSrc = tmpOption[1];
break;
} else if (!next) {
bestSelectedSrc = tmpOption[1];
break;
}
}
// 返回最合适的图片
return bestSelectedSrc;
}
2.6 hasIntersectionObserver & ImageCache
-
hasIntersectionObserver 是判断浏览器是否支持IntersectionObserver方法
IntersectionObserver提供了一种异步观察目标元素与其祖先元素或顶级文档[视口]交叉状态的方法。其祖先元素或视口被称为根。 通俗的讲就是某个目标元素出现在屏幕范围内,就能通过ntersectionObserver捕获到这个元素,从而对这个元素做一些操作。我们这里使用的目的是图片element元素出现在屏幕可视范围才加载图片的资源。 -
如何封装一个Cache类?
this.imageCache = new ImageCache({ max: 200 });
javascript
判断浏览器是否支持IntersectionObserver 方法
export const hasIntersectionObserver =
inBrowser &&
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype;
// 图片cache类
export class ImageCache {
constructor({ max }) {
this.options = {
max: max || 100,
};
this.caches = [];
}
has(key) {
return this.caches.indexOf(key) > -1;
}
add(key) {
if (this.has(key)) return;
this.caches.push(key);
if (this.caches.length > this.options.max) {
this.free();
}
}
free() {
this.caches.shift();
}
}
2.7 initEvent
这个 initEvent() 函数的作用是:在当前类实例(如 Lazy)上初始化一个简易的自定义事件系统(发布-订阅模式) ,提供 $on、$once、$off、$emit 等方法,用于组件内部或外部监听和触发特定事件(如图片加载中、加载完成、加载失败等
kotlin
initEvent() {
this.Event = {
listeners: {
loading: [],
loaded: [],
error: [],
},
};
// 绑定事件
this.$on = (event, func) => {
if (!this.Event.listeners[event]) this.Event.listeners[event] = [];
this.Event.listeners[event].push(func);
};
// 绑定一次性事件
this.$once = (event, func) => {
const on = (...args) => {
// 先调用$off解绑事件
this.$off(event, on);
// 执行回调方法
func.apply(this, args);
};
this.$on(event, on);
};
// 解绑事件
this.$off = (event, func) => {
// 如果不存在func
if (!func) {
// 如果event类名在Event没有注册过,直接return
if (!this.Event.listeners[event]) return;
// 注册过就直接清空数组中数据
this.Event.listeners[event].length = 0;
return;
}
// 存在func回调函数则从数组中移除
remove(this.Event.listeners[event], func);
};
// 触发注册过的event事件
this.$emit = (event, context, inCache) => {
if (!this.Event.listeners[event]) return;
this.Event.listeners[event].forEach((func) => func(context, inCache));
};
}
2.7 lazyLoadHandler
遍历所有被监听的元素(如图片),检查它们是否进入视口(可见区域),如果是,则触发加载;同时清理已从 DOM 中移除的无效监听器,防止内存泄漏。具体的listener是数据结构是什么样的,我们下篇讲。
scss
lazyLoadHandler() {
const freeList = [];
this.listeners.forEach((listener) => {
// 没有目标元素的监听器,放入待释放数组中
if (!listener.el || !listener.el.parentNode) {
freeList.push(listener);
}
// 检查图片是否已经进入了视口区域,如果是,则加载图片
const catIn = listener.checkInView();
if (!catIn) return;
listener.load();
});
// 释放无用的监听器,防止内存泄漏
freeList.forEach((item) => {
remove(this.listeners, item);
item.$destroy();
});
}
总结
- 本文深入解析了 LazyLoad 中常用的一些工具函数,涵盖了如何基于发布-订阅模式实现一个轻量级的事件收集与触发机制,回顾了节流(throttle)、
addEventListener的高级参数(如passive和capture)等核心概念,并探讨了 WebP 图片格式在性能优化中的重要意义。 - 我们梳理了源码中的关键知识点,下篇再见~