Intersection Observer API
一种异步检测指标元素与先人元素或 viewport 相交状况变动的办法。
Intersection Observer API 会注册一个回调函数,每当被监督的元素进入或者退出另外一个元素时(或者 viewport ),或者两个元素的相交局部大小发生变化时,该回调办法会被触发执行,浏览器会自行优化元素相交治理。
实用场景
- 图片懒加载——当图片滚动到可见时才进行加载
- 内容有限滚动——也就是用户滚动到靠近内容底部时间接加载更多,而无需用户操作翻页,给用户一种网页能够有限滚动的错觉
- 检测广告的曝光状况——为了计算广告收益,须要晓得广告元素的曝光状况
- 在用户看见某个区域时执行工作或播放动画
代替办法
过来,相交检测通常要用到事件监听,并且须要频繁调用 Element.getBoundingClientRect()
办法以获取相干元素的边界信息。事件监听和调用 Element.getBoundingClientRect()
都是在主线程上运行,因而频繁触发、调用可能会造成性能问题。这种检测办法极其怪异且不优雅。
如果为了应用不同业务援用多个第三方库,外面可能都各自实现一套雷同的流程,这种状况下的性能是蹩脚并且无奈优化的
概念和用法
Intersection Observer API 容许你配置一个回调函数,当以下状况产生时会被调用
- 每当指标(target)元素与设施视窗或者其余指定元素产生交加的时候执行。设施视窗或者其余元素咱们称它为根元素或根(root)。
- Observer 第一次监听指标元素的时候
通常,您须要关注文档最靠近的可滚动先人元素的交加更改,如果元素不是可滚动元素的后辈,则默认为设施视窗。如果要察看绝对于根(root)元素的交加,请指定根(root)元素为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:
<ul> <li v-for="img in list"> <img v-lazy="img.src" > </li></ul>
use v-lazy-container
work with raw HTML
<div v-lazy-container="{ selector: 'img' }"> <img data-src="//domain.com/img1.jpg"> <img data-src="//domain.com/img2.jpg"> <img data-src="//domain.com/img3.jpg"> </div>
custom error
and loading
placeholder image
<div v-lazy-container="{ selector: 'img', error: 'xxx.jpg', loading: 'xxx.jpg' }"> <img data-src="//domain.com/img1.jpg"> <img data-src="//domain.com/img2.jpg"> <img data-src="//domain.com/img3.jpg"> </div><div v-lazy-container="{ selector: 'img' }"> <img data-src="//domain.com/img1.jpg" data-error="xxx.jpg"> <img data-src="//domain.com/img2.jpg" data-loading="xxx.jpg"> <img data-src="//domain.com/img3.jpg"> </div>
默认配置
在初始化的时候咱们能够传入一些配置参数
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 = [];// 充当察看实例IDthis.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<any>, 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<string>; 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<Tlistener> = [] 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) return this._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<IntersectionObserverEntry>) { 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 = 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 = 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<[tmpWidth: number, tmpSrc: string]> = [] // 父元素 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 = 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 bestSelectedSrc = ''; let tmpOption; 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;}
咱们能够间接看官网示例用法
<template> <div ref="container"> <img v-lazy="'img.400px.jpg'" data-srcset="img.400px.jpg 400w, img.800px.jpg 800w, img.1200px.jpg 1200w"> </div></template>
搜寻父滚动元素(scrollParent)
// 查找滚动元素的父元素const scrollParent = (el: HTMLElement) => { if (!inBrowser) return if (!(el instanceof Element)) { return window } let parent = el while (parent) { // body, html, 或者没有父元素就中断 if (parent === document.body || parent === document.documentElement || !parent.parentNode) break // 有设置overflow对应属性就返回父元素 if (/(scroll|auto)/.test(overflow(parent))) return parent // 递归让父元素判断 parent = parent.parentNode as HTMLElement } return window}
响应对象(ReactiveListener)
次要用来将懒加载的图片转成一个实例对象
根本属性
export default class ReactiveListener { constructor({ el, src, error, loading, bindType, $parent, options, cors, elRenderer, imageCache }) { this.el = el; this.src = src; this.error = error; this.loading = loading; // 指令传输的款式属性,例如background-image this.bindType = bindType; // 重连次数 this.attempt = 0; this.cors = cors; this.naturalHeight = 0; this.naturalWidth = 0; this.options = options; this.rect = {} as DOMRect; this.$parent = $parent; // 调用lazy的_elRenderer办法,设置src并且触发对应事件 this.elRenderer = elRenderer; this._imageCache = imageCache; // 计算工夫 this.performanceData = { loadStart: 0, loadEnd: 0 }; this.filter(); this.initState(); this.render('loading', false); }}
执行过滤器(filter)
/* * listener filter */filter() { // 执行过滤器操作 Object.keys(this.options.filter).forEach((key) => { this.options.filter[key](this, this.options); });}
初始化图片状态(initState)
/* * init listener state * @return */initState() { // HTMLElement.dataset 属性容许读写在 HTML或 DOM中的元素上设置的所有自定义数据属性(data-*)集。 if ('dataset' in this.el!) { this.el.dataset.src = this.src; } else { this.el!.setAttribute('data-src', this.src); } this.state = { loading: false, error: false, loaded: false, rendered: false, };}
更新状态(update)
如果传参换了须要从新更新全副流程
/* * update image listener data * @param {String} image uri * @param {String} loading image uri * @param {String} error image uri * @return */update(option: { src: string, loading: string, error: string }) { const oldSrc = this.src; // 更新地址 this.src = option.src; this.loading = option.loading; this.error = option.error; // 从新执行过滤器 this.filter(); // 新旧地址不同之后重置状态 if (oldSrc !== this.src) { this.attempt = 0; this.initState(); }}
检测视图(checkInView)
/* * get el node rect * @return */getRect() { this.rect = this.el!.getBoundingClientRect();}/* * check el is in view * @return {Boolean} el is in view */checkInView() { this.getRect(); return ( this.rect.top < window.innerHeight * this.options.preLoad! && this.rect.bottom > this.options.preLoadTop! && this.rect.left < window.innerWidth * this.options.preLoad! && this.rect.right > 0 );}
渲染函数(render)
/* * render image * @param {String} state to render // ['loading', 'src', 'error'] * @param {String} is form cache * @return */render(state: string, cache: boolean) { this.elRenderer(this, state, cache);}
这里调用的是lazy的_elRenderer办法,设置src并且触发对应事件
渲染Loading
/* * render loading first * @params cb:Function * @return */renderLoading(cb: Function) { this.state.loading = true; loadImageAsync( { src: this.loading, cors: this.cors, }, () => { this.render('loading', false); this.state.loading = false; cb(); }, () => { // handler `loading image` load failed cb(); this.state.loading = false; } );}
先加载loading图,胜利之后再持续往下加载真正图片
执行加载(load)
/* * try load image and render it * @return */load(onFinish = noop) { // 加载失败 if (this.attempt > this.options.attempt! - 1 && this.state.error) { onFinish(); return; } // 已加载中断 if (this.state.rendered && this.state.loaded) return; // 已缓存过中断 if (this._imageCache.has(this.src as string)) { this.state.loaded = true; this.render('loaded', true); this.state.rendered = true; return onFinish(); } // 省略......}
下面代码次要做了三个判断
- 间断失败的时候中断
- 已加载过的时候中断
- 已缓存过的时候中断
/* * try load image and render it * @return */load(onFinish = noop) { // 省略...... this.renderLoading(() => { this.attempt // 动静批改元素属性 this.options.adapter.beforeLoad && this.options.adapter.beforeLoad(this, this.options) // 记录开始工夫 this.record('loadStart'); loadImageAsync( { src: this.src, cors: this.cors, }, (data: { naturalHeight: number; naturalWidth: number src: string; }) => { // 记录尺寸 this.naturalHeight = data.naturalHeight; this.naturalWidth = data.naturalWidth; // 批改状态 this.state.loaded = true; this.state.error = false; // 记录加载工夫 this.record('loadEnd'); // 渲染视图 this.render('loaded', false); this.state.rendered = true; // 记录缓存 this._imageCache.add(this.src); onFinish(); }, (err: Error) => { this.state.error = true; this.state.loaded = false; this.render('error', false); } ); });}
加载Loading胜利之后,会批改状态之后记录缓存再执行回调
耗时记录(record,performance)
外部计算的函数
/* * record performance * @return */record(event: 'loadStart' | 'loadEnd') { this.performanceData[event] = Date.now();}
裸露给内部的查问函数,并且返回标准对象
/* * output performance data * @return {Object} performance data */performance() { let state = 'loading'; let time = 0; if (this.state.loaded) { state = 'loaded'; time = (this.performanceData.loadEnd - this.performanceData.loadStart) / 1000; } if (this.state.error) state = 'error'; return { src: this.src, state, time, };}
手动销毁实例($destroy)
/* * $destroy * @return */$destroy() { this.el = null; this.src = ''; this.error = null; this.loading = ''; this.bindType = null; this.attempt = 0;}
打印记录(performance)
/** * output listener's load performance * @return {Array} */performance() { const list: Array<VueReactiveListener> = [] this.ListenerQueue.map(item => list.push(item.performance())) return list}
设置图片地址和状态(_elRenderer)
/** * set element attribute with image'url and state * @param {object} lazyload listener object * @param {string} state will be rendered * @param {bool} inCache is rendered from cache * @return */_elRenderer(listener: ReactiveListener, state: TeventType, cache: boolean) { if (!listener.el) return; const { el, bindType } = listener; // 决定渲染状态地址 let src; switch (state) { case 'loading': src = listener.loading; break; case 'error': src = listener.error; break; default: src = listener.src break; } // 应用指令就间接设置背景 if (bindType) { el.style[bindType] = 'url("' + src + '")'; } else if (el.getAttribute('src') !== src) { // 应用属性就设置值 el.setAttribute('src', src); } // 批改状态属性值 el.setAttribute('lazy', state); // 公布对应事件 this.$emit(state, listener, cache); // 动静批改元素属性(配置传入) this.options.adapter[state] && this.options.adapter[state](listener, this.options); // 触发元素事件(配置传入true) if (this.options.dispatchEvent) { // 创立一个自定义事件 const event = new CustomEvent(state, { detail: listener, }); el.dispatchEvent(event); }}
次要流程:
- 依据状态决定渲染图
- 设置到款式或者属性
- 将以后状态记录到元素
- 公布对应的事件
- 执行动静批改函数(如果配置有)
- 是否触发自定义事件(如果配置选true)
能够从官网示例看动静批改函数的应用办法
Vue.use(vueLazy, { adapter: { loaded ({ bindType, el, naturalHeight, naturalWidth, $parent, src, loading, error, Init }) { // do something here // example for call LoadedHandler LoadedHandler(el) }, loading (listender, Init) { console.log('loading') }, error (listender, Init) { console.log('error') } }})
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)});
增加懒加载实例到队列(add)
代码比拟多,咱们逐渐拆解整个函数性能
判断是否已存在队列内和标准参数
/* * add image listener to queue * @param {DOM} el * @param {object} binding vue directive binding * @param {vnode} vnode vue directive vnode * @return */add(el: HTMLElement, binding: DirectiveBinding, vnode: VNode) { // 如果在监听队列中 if (this.ListenerQueue.some((item) => item.el === el)) { // 更新实例 this.update(el, binding); // 下次更新周期执行检测 return nextTick(this.lazyLoadHandler); } // 标准格局 let { src, loading, error, cors } = this._valueFormatter(binding.value) nextTick(() => { // 省略...... } }
增加懒加载实例(addLazyBox)
/* * add lazy component to queue * @param {Vue} vm lazy component instance * @return */addLazyBox(vm: Tlistener) { // 增加到监听队列 this.ListenerQueue.push(vm); if (inBrowser) { // 增加察看队列 this._addListenerTarget(window); // 如果有察看对象则退出察看 this._observer?.observe(vm.el); // 存在父元素也增加察看队列 if (vm.$el?.parentNode) { this._addListenerTarget(vm.$el.parentNode); } }}
增加的办法在上面
/* * add listener target * @param {DOM} el listener target * @return */_addListenerTarget(el: HTMLElement | Window) { if (!el) return; // 查找是否已存在指标 let target = this.TargetQueue.find((target) => target.el === el); if (!target) { // 初始化构造 target = { el, id: ++this.TargetIndex, childrenCount: 1, listened: true, }; // 应用事件模式则进行绑定 this.mode === modeType.event && this._initListen(target.el, true); this.TargetQueue.push(target); } else { // 子元素数量加1 target.childrenCount++; } // 返回以后指标元素索引值 return this.TargetIndex;}
之所以把每个懒加载实例的父滚动元素退出队列是为了防止屡次对同一元素执行初始化操作
记录子元素数量是为了确保移除懒加载实例的时候不会间接删除父元素导致其余实例受影响
查找容器元素和对应地址
咱们能够晓得指令属性都有哪些
参数 | 形容 |
---|---|
instance | 应用指令的组件实例 |
value | 传递给指令的值。例如,在 v-my-directive="1 + 1" 中,该值为 2 |
oldValue | 先前的值,仅在 beforeUpdate 和 updated 中可用。值是否已更改都可用 |
arg | 参数传递给指令 (如果有)。例如在 v-my-directive:foo 中,arg 为 "foo" |
modifiers | 蕴含修饰符 (如果有) 的对象。例如在 v-my-directive.foo.bar 中,修饰符对象为 {foo: true,bar: true} |
dir | 一个对象,在注册指令时作为参数传递。例如,在以下指令中 |
/* * add image listener to queue * @param {DOM} el * @param {object} binding vue directive binding * @param {vnode} vnode vue directive vnode * @return */add(el: HTMLElement, binding: DirectiveBinding, vnode: VNode) { // 省略...... nextTick(() => { // 失去对应响应式图片地址 src = getBestSelectionFromSrcset(el, this.options.scale as number) || src; // 存在则退出察看 this._observer?.observe(el); // 获取修饰符对象,如果有的话 const container: string = Object.keys(binding.modifiers)[0]; // 父滚动元素 let $parent: any; if (container) { $parent = binding.instance!.$refs[container] // if there is container passed in, try ref first, then fallback to getElementById to support the original usage $parent = $parent ? $parent.$el || $parent : document.getElementById(container); } // 查找父元素 if (!$parent) { $parent = scrollParent(el); } } }
其中对修饰符对象那一块可能会有疑难,从官网demo能够看到它的用法
<template> <div ref="container"> <!-- Customer scrollable element --> <img v-lazy.container ="imgUrl"/> <div v-lazy:background-image.container="img"></div> </div></template>
在指令前面附带一个dom的ref名或者dom ID,如果有多个默认只拿第一个,而后从应用指令的组件实例开始往下层查找
如果搜寻不到就间接从dom元素往上搜寻
生成响应对象退出队列并检测视图
/* * add image listener to queue * @param {DOM} el * @param {object} binding vue directive binding * @param {vnode} vnode vue directive vnode * @return */add(el: HTMLElement, binding: DirectiveBinding, vnode: VNode) { // 省略...... nextTick(() => { // 省略...... const newListener = new ReactiveListener({ el, src, error, loading, bindType: binding.arg!, $parent, options: this.options, cors, elRenderer: this._elRenderer.bind(this), imageCache: this._imageCache }); // 退出事件队列 this.ListenerQueue.push(newListener); // 退出察看队列 if (inBrowser) { this._addListenerTarget(window); this._addListenerTarget($parent); } nextTick(() => this.lazyLoadHandler()); } }
最初就做了三件事
- 生成响应对象退出监听队列
- 将window和父滚动元素退出察看队列,外面会过滤反复增加
- 在下一次Dom更新之后触发一次视图检测
更新实例地址(update)
/** * update image src * @param {DOM} el * @param {object} vue directive binding * @return */update(el: HTMLElement, binding: DirectiveBinding, vnode?: VNode) { let { src, loading, error } = this._valueFormatter(binding.value) // 获取对应的响应式图片地址 src = getBestSelectionFromSrcset(el, this.options.scale!) || src; const exist = this.ListenerQueue.find((item) => item.el === el); if (!exist) { // 不存在则增加进队列 this.add(el, binding, vnode!); } else { // 已存在就更新地址 exist.update({ src, error, loading }); } // 从新绑定察看 if (this._observer) { this._observer.unobserve(el); this._observer.observe(el); } // 在下次 DOM 更新循环完结之后执行提早回调进行检测 nextTick(() => this.lazyLoadHandler());}
次要流程
- 获取最终渲染地址
- 检测是否存在队列中,决定增加或者间接批改
- 从新执行察看
- 下次 DOM 更新循环完结之后执行提早回调进行检测
从监听队列移除实例(remove)
/** * remove listener form list * @param {DOM} el * @return */remove(el: HTMLElement) { // 不存在间接中断 if (!el) return; // 移除察看 this._observer?.unobserve(el); const existItem = this.ListenerQueue.find((item) => item.el === el); if (existItem) { // 缩小childrenCount数量,为0则移除对应事件和TargetQueue实例 this._removeListenerTarget(existItem.$parent); this._removeListenerTarget(window); // 从队列移除 remove(this.ListenerQueue, existItem); // 手动销毁 existItem.$destroy && existItem.$destroy() }}
其中_removeListenerTarget
上面再解析
从察看队列移除实例(_removeListenerTarget)
/* * remove listener target or reduce target childrenCount * @param {DOM} el or window * @return */_removeListenerTarget(el: HTMLElement | Window & typeof globalThis) { this.TargetQueue.forEach((target, index) => { // 如果匹配到队列数据 if (target!.el === el) { // 子数量-1 target.childrenCount--; // 曾经为0 if (!target.childrenCount) { // 移除事件 this._initListen(target.el, false); // 将指标从察看队列移除 this.TargetQueue.splice(index, 1); target = null; } } });}
移除组件(removeComponent)
/* * remove lazy components form list * @param {Vue} vm Vue instance * @return */removeComponent(vm: Tlistener) { if (!vm) return // 将指标从队列移除 remove(this.ListenerQueue, vm) // 如果有察看对象则移除察看 this._observer?.unobserve(vm.el); // 存在父元素节点也移除 if (vm.$parent && vm.$el.parentNode) { this._removeListenerTarget(vm.$el.parentNode) } this._removeListenerTarget(window)}
懒加载图片组件(lazy-image)
组件根本入参和属性
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 = 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 = options.src loadImageAsync({ src }, ({ src }: loadImageAsyncOption) => { renderSrc.value = src state.loaded = true }, () => { state.attempt++ renderSrc.value = 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 = 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<HTMLElement> { 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; }}
只有两个性能
- 更新的时候从新获取容器内所有对应标签,整顿参数之后传入
lzay
队列里,外面会判断是新增还是更新操作 - 组件清理的时候会在
lzay
懒加载队列里移除容器内所有对应标签的实例