Vue3-Lazyload源码解析
Intersection Observer API
一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。Intersection Observer API 会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时(或者 viewport ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行,浏览器会自行优化元素相交管理。
文章图片
适用场景
- 图片懒加载——当图片滚动到可见时才进行加载
- 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
- 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
- 在用户看见某个区域时执行任务或播放动画
Element.getBoundingClientRect()
方法以获取相关元素的边界信息。事件监听和调用 Element.getBoundingClientRect()
都是在主线程上运行,因此频繁触发、调用可能会造成性能问题。这种检测方法极其怪异且不优雅。如果为了使用不同业务引用多个第三方库,里面可能都各自实现一套相同的流程,这种情况下的性能是糟糕并且无法优化的
概念和用法 Intersection Observer API 允许你配置一个回调函数,当以下情况发生时会被调用
- 每当目标(target)元素与设备视窗或者其他指定元素发生交集的时候执行。设备视窗或者其他元素我们称它为根元素或根(root)。
- Observer 第一次监听目标元素的时候
null
。目标(target)元素与根(root)元素之间的交叉度是交叉比(intersection ratio)。这是目标(target)元素相对于根(root)的交集百分比的表示,它的取值在0.0和1.0之间。
创建一个 intersection observer
创建一个 IntersectionObserver 对象,并传入相应参数和回调函数,该回调函数将会在目标(target)元素和根(root)元素的交集大小超过阈值(threshold)规定的大小时候被执行。
options
参数 | 描述 |
---|---|
root | 指定根(root)元素,用于检查目标的可见性。必须是目标元素的父级元素。如果未指定或者为null,则默认为浏览器视窗。 |
rootMargin | 根(root)元素的外边距。如果有指定 root 参数,则 rootMargin 也可以使用百分比来取值。该属性值是用作 root 元素和 target 发生交集时候的计算交集的区域范围,使用该属性可以控制 root 元素每一边的收缩或者扩张。默认值为0。 |
threshold | 可以是单一的 number 也可以是 number 数组,target 元素和 root 元素相交程度达到该值的时候 IntersectionObserver 注册的回调函数将会被执行。如果你只是想要探测当 target 元素的在 root 元素中的可见性超过50%的时候,你可以指定该属性值为0.5。如果你想要 target 元素在 root 元素的可见程度每多25%就执行一次回调,那么你可以指定一个数组 [0, 0.25, 0.5, 0.75, 1] 。默认值是0 (意味着只要有一个 target 像素出现在 root 元素中,回调函数将会被执行)。该值为1.0含义是当 target 完全出现在 root 元素中时候回调才会被执行。 |
const options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}const callback =(entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
const observer = new IntersectionObserver(callback, options);
请留意,你注册的回调函数将会在主线程中被执行。所以该函数执行速度要尽可能的快。如果有一些耗时的操作需要执行,建议使用
Window.requestIdleCallback()
方法。创建一个 observer 后需要给定一个目标元素进行观察。
const target = document.querySelector('#listItem');
observer.observe(target);
文章图片
交集的计算 容器元素和偏移值
所有区域均被 Intersection Observer API 当做一个矩形看待。如果元素是不规则的图形也将会被看成一个包含元素所有区域的最小矩形,相似的,如果元素发生的交集部分不是一个矩形,那么也会被看作是一个包含他所有交集区域的最小矩形。
容器 (root) 元素既可以是 target 元素祖先元素也可以是指定 null 则使用浏览器视口做为容器(root)。来对目标元素进行相交检测的矩形,它的大小有以下几种情况:
- 如果隐含 root (值为null) , 就是视窗的矩形大小。
- 如果有溢出部分, 则是 root 元素的内容 (content) 区域.
- 否则就是容器元素的矩形边界 (getBoundingClientRect() 方法获取).
rootMargin
的属性值将会做为 margin 偏移值添加到容器 (root) 元素的对应的 margin 位置,并最终形成 root 元素的矩形边界阈值 IntersectionObserver API 并不会每次在元素的交集发生变化的时候都会执行回调。相反它使用了 thresholds 参数。当你创建一个 observer 的时候,你可以提供一个或者多个 number 类型的数值用来表示 target 元素在 root 元素的可见程序的百分比,然后,API的回调函数只会在元素达到 thresholds 规定的阈值时才会执行。
- 第一个盒子的 thresholds 包含每个可视百分比
- 第二个盒子只有唯一的值 [0.5]。
- 第三个盒子的 thresholds 按10%从0递增(0%, 10%, 20%, etc.)。
- 最后一个盒子为 [0, 0.25, 0.5, 0.75, 1.0]。
文章图片
requestIdleCallback
window.requestIdleCallback()
方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout
,则有可能为了在超时前执行函数而打乱执行顺序。你可以在空闲回调函数中调用
**requestIdleCallback()**
,以便在下一次通过事件循环之前调度另一个回调。文章图片
注意:
requestAnimationFrame
会请求浏览器在下一次重新渲染之前执行回调函数requestIdleCallback
在浏览器空闲时期被调用
自定义事件 CustomEvent 创建一个新的 CustomEvent 对象。
$$ event = new CustomEvent(typeArg, customEventInit); $$
- typeArg:一个表示 event 名字的字符串
- customEventInit:
| 参数 | 描述 |
| ---------- | ----------------------------------------------------------- |
| detail | 可选的默认值是 null 的任意类型数据,是一个与 event 相关的值 |
| bubbles | 一个布尔值,表示该事件能否冒泡 |
| cancelable | 一个布尔值,表示该事件是否可以取消 |
文章图片
dispatchEvent 向一个指定的事件目标派发一个事件, 并以合适的顺序同步调用目标元素相关的事件处理函数。标准事件处理规则(包括事件捕获和可选的冒泡过程)同样适用于通过手动的使用dispatchEvent()方法派发的事件。
$$ cancelled = !target.dispatchEvent(event) $$
参数:
event
是要被派发的事件对象。target
被用来初始化 事件 和 决定将会触发 目标.
- 当该事件是可取消的(cancelable为true)并且至少一个该事件的 事件处理方法 调用了Event.preventDefault(),则返回值为false;否则返回true。
文章图片
如果该被派发的事件的事件类型(event's type)在方法调用之前没有被经过初始化被指定,就会抛出一个
UNSPECIFIED_EVENT_TYPE_ERR
异常,或者如果事件类型是null
或一个空字符串. event handler 就会抛出未捕获的异常; 这些 event handlers 运行在一个嵌套的调用栈中: 他们会阻塞调用直到他们处理完毕,但是异常不会冒泡。注意
与浏览器原生事件不同,原生事件是由DOM派发的,并通过
event loop
异步调用事件处理程序,而dispatchEvent()
则是同步调用事件处理程序。在调用dispatchEvent()
后,所有监听该事件的事件处理程序将在代码继续前执行并返回。dispatchEvent()
是create-init-dispatch过程的最后一步,用于将事件调度到实现的事件模型中。可以使用Event
构造函数来创建事件。懒加载流程
文章图片
这是比较常规的实现方式
Vue-lazyload源码解析 入口文件
export const Lazyload = {
/*
* install function
* @param{App} app
* @param{object} options lazyload options
*/
install(app: App, options: VueLazyloadOptions = {}) {
const lazy = new Lazy(options)
const lazyContainer = new LazyContainer(lazy)// 暴露给组件实例
app.config.globalProperties.$Lazyload = lazy;
// 组件注册
if (options.lazyComponent) {
app.component('lazy-component', LazyComponent(lazy));
}if (options.lazyImage) {
app.component('lazy-image', LazyImage(lazy));
}// 指令注册
app.directive('lazy', {
// 保持指向
beforeMount: lazy.add.bind(lazy),
beforeUpdate: lazy.update.bind(lazy),
updated: lazy.lazyLoadHandler.bind(lazy),
unmounted: lazy.remove.bind(lazy)
});
app.directive('lazy-container', {
beforeMount: lazyContainer.bind.bind(lazyContainer),
updated: lazyContainer.update.bind(lazyContainer),
unmounted: lazyContainer.unbind.bind(lazyContainer),
});
}
}
lzay
就是懒加载的核心实现,需要把他暴露给Vue实例的实例上,这点很重要app.config.globalProperties.$Lazyload = lazy;
使用方式:两个组件和两种指令 首先注册组件
if (options.lazyComponent) {
app.component('lazy-component', LazyComponent(lazy));
}if (options.lazyImage) {
app.component('lazy-image', LazyImage(lazy));
}
在不同的指令钩子需要调用
lazy
的方法app.directive('lazy', {
// 保持指向
beforeMount: lazy.add.bind(lazy),
beforeUpdate: lazy.update.bind(lazy),
updated: lazy.lazyLoadHandler.bind(lazy),
unmounted: lazy.remove.bind(lazy)
});
app.directive('lazy-container', {
beforeMount: lazyContainer.bind.bind(lazyContainer),
updated: lazyContainer.update.bind(lazyContainer),
unmounted: lazyContainer.unbind.bind(lazyContainer),
});
使用方式 template:
-
文章图片
文章图片
文章图片
custom
error
and loading
placeholder image
文章图片
文章图片
文章图片
文章图片
文章图片
文章图片
默认配置 在初始化的时候我们可以传入一些配置参数
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1,
// the default is ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend']
listenEvents: [ 'scroll' ]
})
内部它的配置参数是这么处理的
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 || {},
// 是否使用IntersectionObserver
observer: !!observer || true,
observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS,
};
监听实现方式 只要有两种
// 实现懒加载的两种方案
export const modeType = {
event: 'event',
observer: 'observer',
};
其中需要判断是否兼容
observer
方式const inBrowser = typeof window !== 'undefined' && window !== nullexport const hasIntersectionObserver = checkIntersectionObserver()function checkIntersectionObserver(): boolean {
if (inBrowser &&
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
// Minimal polyfill for Edge 15's lack of `isIntersecting`
// See: https://github.com/w3c/IntersectionObserver/issues/211
if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
Object.defineProperty(window.IntersectionObserverEntry.prototype,
'isIntersecting', {
get: function () {
return this.intersectionRatio > 0
}
})
}
return true
}
return false
}
Lazy构造函数
class Lazy {
constructor({
preLoad,
error,
throttleWait,
preLoadTop,
dispatchEvent,
loading,
attempt,
silent = true,
scale,
listenEvents,
filter,
adapter,
observer,
observerOptions
}: VueLazyloadOptions) {
this.lazyContainerMananger = null;
this.mode = modeType.event;
// 监听队列,各个图片实例
this.ListenerQueue = [];
// 充当观察实例ID
this.TargetIndex = 0;
// 观察队列,window或其他父元素实例
this.TargetQueue = [];
this.options = {
上文省略...
};
// 初始化事件
this._initEvent();
// 缓存
this._imageCache = new ImageCache(200);
// 视图检测
this.lazyLoadHandler = throttle(
this._lazyLoadHandler.bind(this),
this.options.throttleWait!
);
// 选择懒加载方式
this.setMode(this.options.observer ? modeType.observer : modeType.event);
}
}
我们分步骤解析他们都有什么功能,首先里面主要有两个维护队列
// 监听队列,各个图片实例
this.ListenerQueue = [];
// 充当观察实例ID
this.TargetIndex = 0;
// 观察队列,window或其他父元素实例
this.TargetQueue = [];
ListenerQueue:用于保存懒加载图片的实例
TargetQueue:用于保存懒加载容器的实例
发布订阅事件(_initEvent) 默认提供三个图片加载的事件:
- loading
- loaded
- error
// 提供发布订阅事件
_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.$off = (event, func) => {
// 不传方法的情况
if (!func) {
// 不含事件直接中断
if (!this.Event.listeners[event]) return;
// 否则直接清空事件队列
this.Event.listeners[event].length = 0;
return;
}
// 只清除指定函数
remove(this.Event.listeners[event], func);
};
this.$once = (event, func) => {
const on = () => {
// 一次触发立马移除事件
this.$off(event, on);
func.apply(this, arguments);
};
this.$on(event, on);
};
this.$emit = (event, context, inCache) => {
if (!this.Event.listeners[event]) return;
// 遍历事件所有监听方法触发
this.Event.listeners[event].forEach((func) => func(context, inCache));
};
}
基本代码都比较简单,其中有一个
remove
函数,他主要作用就是从队列移除实例function remove(arr: Array, item: any) {
if (!arr.length) return;
const index = arr.indexOf(item);
if (index > -1) return arr.splice(index, 1);
}
图片缓存 初始化的时候默认做了缓存处理
this._imageCache = new ImageCache(200);
实现也比较简单
class ImageCache {
max: number;
_caches: Array;
constructor(max: number) {
this.max = max || 100
this._caches = []
}has(key: string): boolean {
return this._caches.indexOf(key) > -1;
}// 需要唯一索引值
add(key: string) {
// 阻止重复|无效添加
if (!key || this.has(key)) return;
this._caches.push(key);
// 超过限制移除最旧图片
if (this._caches.length > this.max) {
this.free();
}
}// 先进先出
free() {
this._caches.shift();
}
}
视图检测(lazyLoadHandler) 初始化的时候已经自动加了节流降低触发频率,默认200
// 视图检测
this.lazyLoadHandler = throttle(
this._lazyLoadHandler.bind(this),
this.options.throttleWait!
);
主要实现功能有
- 检测是否在视图内
- 是否触发图片加载逻辑
- 清理队列无用实例
/**
* find nodes which in viewport and trigger load
* @return
*/
_lazyLoadHandler() {
// 需要被清理的节点
const freeList: Array = []this.ListenerQueue.forEach((listener) => {
// 不存在DOM节点 || 不存在父节点DOM || 已加载过
if (!listener.el || !listener.el.parentNode || listener.state.loaded) {
freeList.push(listener)
}
// 检测是否在可视视图范围内
const catIn = listener.checkInView();
if (!catIn) return;
// 如果是在视图内并未加载完
if (!listener.state.loaded) listener.load()
});
// 无用节点实例移除
freeList.forEach((item) => {
remove(this.ListenerQueue, item);
// 手动销毁vm实例与DOM之间的关联
item.$destroy && item.$destroy()
});
}
选择懒加载方式(setMode) 初始化调用函数
// 选择懒加载方式
this.setMode(this.options.observer ? modeType.observer : modeType.event);
主要实现功能:
- 使用
observer
模式的时候需要有优雅降级处理 - 如果使用
observer
就移除事件逻辑并进行实例化 - 如果使用
event
就移除观察并进行事件绑定
setMode(mode: string) {
// 不兼容降级方案
if (!hasIntersectionObserver && mode === modeType.observer) {
mode = modeType.event;
}this.mode = mode;
// event or observer
if (mode === modeType.event) {
if (this._observer) {
// 移除事件队列所有观察
this.ListenerQueue.forEach((listener) => {
this._observer!.unobserve(listener.el);
});
// 移除观察对象
this._observer = null;
}
// 添加事件
this.TargetQueue.forEach((target) => {
this._initListen(target.el, true);
});
} else {
// 移除事件队列
this.TargetQueue.forEach((target) => {
this._initListen(target.el, false);
});
// IntersectionObserver实例化
this._initIntersectionObserver();
}
}
事件绑定模式(_initListen) 默认的事件有
const DEFAULT_EVENTS = [
'scroll',
'wheel',
'mousewheel',
'resize',
'animationend',
'transitionend',
'touchmove',
];
下面懂得都懂
/*
* add or remove eventlistener
* @param{DOM} el DOM or Window
* @param{boolean} start flag
* @return
*/
_initListen(el: HTMLElement, start: boolean) {
this.options.listenEvents!.forEach((evt) => _[start ? 'on' : 'off'](el, evt, this.lazyLoadHandler))
}
const _ = {
on(el: Element, type: string, func: () => void, capture = false) {
el.addEventListener(type, func, {
capture: capture,
passive: true
})
},
off(el: Element, type: string, func: () => void, capture = false) {
el.removeEventListener(type, func, capture)
}
}
其中
passive
的作用是这么描述的passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。根据规范,passive 选项的默认值始终为false。但是,这引入了处理某些触摸事件(以及其他)的事件监听器在尝试处理滚动时阻止浏览器的主线程的可能性,从而导致滚动处理期间性能可能大大降低。
为防止出现此问题,某些浏览器(特别是Chrome和Firefox)已将文档级节点 Window,Document和Document.body的touchstart (en-US)和touchmove (en-US)事件的passive选项的默认值更改为true。这可以防止调用事件监听器,因此在用户滚动时无法阻止页面呈现。
Observer初始化(_initIntersectionObserver) 如果不传参的情况有默认参数
const DEFAULT_OBSERVER_OPTIONS = {
rootMargin: '0px',
threshold: 0,
};
下面进行实例化,然后把所有懒加载图片实例加入观察
/**
* init IntersectionObserver
* set mode to observer
* @return
*/
_initIntersectionObserver() {
if (!hasIntersectionObserver) returnthis._observer = new IntersectionObserver(
// callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。
this._observerHandler.bind(this),
this.options.observerOptions
);
// 加载队列所有数据都放入观察
if (this.ListenerQueue.length) {
// 列表所有元素加入观察
this.ListenerQueue.forEach((listener) => {
// 开始观察元素
this._observer!.observe(listener.ell as Element);
});
}
}
回调函数做的操作
/**
* init IntersectionObserver
* 遍历对比触发元素和监听元素,如果已加载完成移除观察,否则开始加载
* @return
*/
_observerHandler(entries: Array) {
entries.forEach((entry) => {
// target 元素在 root 元素中的可见性是否发生变化
// 如果 isIntersecting 为真,target 元素的至少已经达到 thresholds 属性值当中规定的其中一个阈值,如果为假,则 target 元素不在给定的阈值范围内可见。
if (entry.isIntersecting) {
this.ListenerQueue.forEach((listener) => {
// 容器元素内触发元素跟队列元素匹配上
if (listener.el === entry.target) {
// 已完成加载则移除
if (listener.state.loaded) return this._observer!.unobserve(listener.el as Element);
// 进行加载
listener.load();
}
});
}
});
}
回调获取到的数据大概如下
文章图片
图片地址规范函数(_valueFormatter)
/**
* generate loading loaded error image url
* @param {string} image's src
* @return {object} image's loading, loaded, error url
*/
_valueFormatter(value) {
let src = https://www.it610.com/article/value;
// 加载/错误时的图片,没有就取默认
let { loading, error, cors } = this.options;
// value is object
if (isObject(value)) {
if (!value.src && !this.options.silent) console.error('Vue Lazyload Next warning: miss src with ' + value)
src = https://www.it610.com/article/value.src;
loading = value.loading || this.options.loading;
error = value.error || this.options.error;
}return {
src,
loading,
error,
cors
};
}
搜索对应图片地址(getBestSelectionFromSrcset) 过滤无效实例,进行参数整合
// 筛选最终替换图片地址
function getBestSelectionFromSrcset(el: Element, scale: number): string {
// 非IMG标签或者不含响应式属性
if (el.tagName !== 'IMG' || !el.getAttribute('data-srcset')) return '';
// 例如"img.400px.jpg 400w, img.800px.jpg" => ['img.400px.jpg 400w', ' img.800px.jpg']
let options = el.getAttribute('data-srcset')!.trim().split(',');
const result: Array = []
// 父元素
const container = el.parentNode as HTMLElement;
const containerWidth = container.offsetWidth * scale;
let spaceIndex: number;
let tmpSrc: string;
let tmpWidth: number;
// ......
}
转换出地址和宽度
// 筛选最终替换图片地址
function getBestSelectionFromSrcset(el: Element, scale: number): string {
// ......
options.forEach((item) => {
item = item.trim();
spaceIndex = item.lastIndexOf(' ');
// 没指定宽度就给默认99999
if (spaceIndex === -1) {
tmpSrc = https://www.it610.com/article/item;
tmpWidth = 99999;
} else {
tmpSrc = item.substr(0, spaceIndex);
tmpWidth = parseInt(
item.substr(spaceIndex + 1, item.length - spaceIndex - 2),
10
);
}
return [tmpWidth, tmpSrc];
});
}
得到如下
/*
得出
[
[400, 'img.400px.jpg''],
[99999, 'img.800px.jpg']
]
*/
进行排序
// 筛选最终替换图片地址
function getBestSelectionFromSrcset(el: Element, scale: number): string {
// ......
// 宽度优先,webp后者优先
result.sort(function (a, b) {
if (a[0] < b[0]) return 1;
if (a[0] > b[0]) return -1;
if (a[0] === b[0]) {
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;
});
}
得出最终地址
// 筛选最终替换图片地址
function getBestSelectionFromSrcset(el: Element, scale: number): string {
// ......
let bestSelectedSrchttps://www.it610.com/article/= '';
let tmpOption;
for (let i = 0;
i < result.length;
i++) {
tmpOption = result[i];
bestSelectedSrc = https://www.it610.com/article/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;
}
我们可以直接看官方示例用法
文章图片
组件基本入参和属性
export default (lazy: Lazy) => {
return defineComponent({
props: {
src: [String, Object],
tag: {
type: String,
default: 'img'
}
},setup(props, { slots }) {
const el: Ref = ref(null)
// 配置
const options = reactive({
src: '',
error: '',
loading: '',
attempt: lazy.options.attempt
})
// 状态
const state = reactive({
loaded: false,
error: false,
attempt: 0
})
const renderSrc: Ref = ref('')
const { rect, checkInView } = useCheckInView(el, lazy.options.preLoad!)// 生成标准化实例对象
const vm = computed(() => {
return {
el: el.value,
rect,
checkInView,
load,
state,
}
})// 初始化各种状态下对应图片地址
const init = () => {
const { src, loading, error } = lazy._valueFormatter(props.src)
state.loaded = false
options.src = https://www.it610.com/article/src
options.error = error!
options.loading = loading!
renderSrc.value = options.loading
}
init()return () => createVNode(
props.tag,
{
src: renderSrc.value,
ref: el
},
[slots.default?.()]
)
}
})
}
加载函数
export default (lazy: Lazy) => {
return defineComponent({
setup(props, { slots }) {
// 省略......
const load = (onFinish = noop) => {
// 失败重试次数
if ((state.attempt > options.attempt! - 1) && state.error) {
onFinish()
return
}
const src = https://www.it610.com/article/options.src
loadImageAsync({ src }, ({ src }: loadImageAsyncOption) => {
renderSrc.value = https://www.it610.com/article/src
state.loaded = true
}, () => {
state.attempt++
renderSrc.value = https://www.it610.com/article/options.error
state.error = true
})
}
}
})
}
触发事件
export default (lazy: Lazy) => {
return defineComponent({
setup(props, { slots }) {
// 省略......// 地址修改重新执行流程
watch(
() => props.src,
() => {
init()
lazy.addLazyBox(vm.value)
lazy.lazyLoadHandler()
}
)
onMounted(() => {
// 保存到事件队列
lazy.addLazyBox(vm.value)
// 立马执行一次视图检测
lazy.lazyLoadHandler()
})
onUnmounted(() => {
lazy.removeComponent(vm.value)
})
}
})
}
懒加载组件(lazy-component)
文章图片
export default (lazy: Lazy) => {
return defineComponent({
props: {
tag: {
type: String,
default: 'div'
}
},
emits: ['show'],
setup(props, { emit, slots }) {
const el: Ref = ref(null)
const state = reactive({
loaded: false,
error: false,
attempt: 0
})
const show = ref(false)
const { rect, checkInView } = useCheckInView(el, lazy.options.preLoad!)// 通知父组件
const load = () => {
show.value = https://www.it610.com/article/true
state.loaded = true
emit('show', show.value)
}// 标准化实例对象
const vm = computed(() => {
return {
el: el.value,
rect,
checkInView,
load,
state,
}
})onMounted(() => {
// 保存到事件队列
lazy.addLazyBox(vm.value)
// 立马执行一次视图检测
lazy.lazyLoadHandler()
})onUnmounted(() => {
lazy.removeComponent(vm.value)
})return () => createVNode(
props.tag,
{
ref: el
},
[show.value && slots.default?.()]
)
}
})
}
跟图片组件的主要区别在于加载函数直接通知到父元素,本身只记录状态
懒加载容器指令(lazy-container)
文章图片
我们看回入口跟这个有关系的代码
export const Lazyload = {
/*
* install function
* @param{App} app
* @param{object} options lazyload options
*/
install(app: App, options: VueLazyloadOptions = {}) {
// 省略...
const lazyContainer = new LazyContainer(lazy)app.directive('lazy-container', {
beforeMount: lazyContainer.bind.bind(lazyContainer),
updated: lazyContainer.update.bind(lazyContainer),
unmounted: lazyContainer.unbind.bind(lazyContainer),
});
}
}
生成实例之后会在lazy-container
指令的钩子函数里调用对应方法
lazy-container实现
// 懒加载容器管理
export default class LazyContainerManager {
constructor(lazy: Lazy) {
// 保存lazy指向
this.lazy = lazy;
// 保存管理指向
lazy.lazyContainerMananger = this
// 维护队列
this._queue = [];
}bind(el: HTMLElement, binding: DirectiveBinding, vnode: VNode) {
const container = new LazyContainer(
el,
binding,
vnode,
this.lazy,
);
// 保存懒加载容器实例
this._queue.push(container);
}update(el: HTMLElement, binding: DirectiveBinding, vnode?: VNode) {
const container = this._queue.find((item) => item.el === el);
if (!container) return;
container.update(el, binding);
}unbind(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) {
const container = this._queue.find((item) => item.el === el);
if (!container) return;
// 清空状态
container.clear();
// 移除实例
remove(this._queue, container);
}
}
这是全局统一的容器实例管理组件,只有三个功能
- 挂载之前,将容器组件保存为
LazyContainer
实例,保存在队列维护
- 更新之后,更新队列实例属性
- 销毁之前,清空状态移除实例
LazyContainer类
class LazyContainer {
constructor(el: HTMLElement, binding: DirectiveBinding, vnode: VNode, lazy: Lazy) {
this.el = null;
this.vnode = vnode;
this.binding = binding;
this.options = {} as DefaultOptions;
this.lazy = lazy;
this._queue = [];
this.update(el, binding);
}update(el: HTMLElement, binding: DirectiveBinding) {
this.el = el;
this.options = Object.assign({}, defaultOptions, binding.value);
// 组件下所有图片添加进懒加载队列
const imgs = this.getImgs();
imgs.forEach((el: HTMLElement) => {
this.lazy!.add(
el,
Object.assign({}, this.binding, {
value: {
src: el.getAttribute('data-src') || el.dataset.src,
error: el.getAttribute('data-error') || el.dataset.error || this.options.error,
loading: el.getAttribute('data-loading') || el.dataset.loading || this.options.loading
},
}),
this.vnode as VNode
);
});
}getImgs(): Array {
return Array.from(this.el!.querySelectorAll(this.options.selector));
}clear() {
const imgs = this.getImgs();
imgs.forEach((el) => this.lazy!.remove(el));
this.vnode = null;
this.binding = null;
this.lazy = null;
}
}
【Vue3-Lazyload源码解析】只有两个功能
- 更新的时候重新获取容器内所有对应标签,整理参数之后传入
lzay
队列里,里面会判断是新增还是更新操作
- 组件清理的时候会在
lzay
懒加载队列里移除容器内所有对应标签的实例
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Android事件传递源码分析
- Quartz|Quartz 源码解析(四) —— QuartzScheduler和Listener事件监听
- Java内存泄漏分析系列之二(jstack生成的Thread|Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析)
- [源码解析]|[源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)
- ffmpeg源码分析01(结构体)
- Android系统启动之init.rc文件解析过程
- Java程序员阅读源码的小技巧,原来大牛都是这样读的,赶紧看看!
- 小程序有哪些低成本获客手段——案例解析
- Vue源码分析—响应式原理(二)
- SwiftUI|SwiftUI iOS 瀑布流组件之仿CollectionView不规则图文混合(教程含源码)