diff --git a/packages/varlet-vue2-ui/src/lazy/__tests__/__snapshots__/index.spec.js.snap b/packages/varlet-vue2-ui/src/lazy/__tests__/__snapshots__/index.spec.js.snap
new file mode 100644
index 0000000..6c50cc2
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/__tests__/__snapshots__/index.spec.js.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`test lazy background-image 1`] = `""`;
+
+exports[`test lazy background-image 2`] = `""`;
+
+exports[`test lazy error with attempt 1`] = `""`;
+
+exports[`test lazy error with attempt 2`] = `""`;
+
+exports[`test lazy error with attempt 3`] = `""`;
+
+exports[`test lazy error with attempt 4`] = `""`;
+
+exports[`test lazy example 1`] = `
+"
"
+`;
+
+exports[`test lazy load 1`] = `""`;
+
+exports[`test lazy load 2`] = `""`;
+
+exports[`test lazy updated 1`] = `""`;
+
+exports[`test lazy updated 2`] = `""`;
+
+exports[`test lazy updated 3`] = `""`;
diff --git a/packages/varlet-vue2-ui/src/lazy/__tests__/index.spec.js b/packages/varlet-vue2-ui/src/lazy/__tests__/index.spec.js
new file mode 100644
index 0000000..56b8415
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/__tests__/index.spec.js
@@ -0,0 +1,111 @@
+import example from '../example'
+import Lazy, { imageCache } from '..'
+import { mount } from '@vue/test-utils'
+import Vue from 'vue'
+import { delay, mockDoubleRaf, trigger } from '../../utils/jest'
+
+test('test lazy example', () => {
+ const wrapper = mount(example)
+ expect(wrapper.html()).toMatchSnapshot()
+ wrapper.destroy()
+})
+
+test('test lazy plugin', () => {
+ Vue.use(Lazy)
+ expect(Vue.directive('lazy')).toBeTruthy()
+})
+
+const Wrapper = {
+ directives: { Lazy },
+ data: () => ({
+ src: 'https://varlet.gitee.io/varlet-ui/cat.jpg',
+ error: 'https://varlet.gitee.io/varlet-ui/error.jpg',
+ }),
+ template: `
+
+ `,
+}
+
+test('test lazy load', async () => {
+ const { mockRestore } = mockDoubleRaf()
+ const wrapper = mount(Wrapper)
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ await trigger(wrapper.element._lazy.preloadImage, 'load')
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ wrapper.destroy()
+ imageCache.clear()
+ mockRestore()
+})
+
+test('test lazy error with attempt', async () => {
+ const { mockRestore } = mockDoubleRaf()
+ const wrapper = mount(Wrapper)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ await delay(80)
+
+ await trigger(wrapper.element._lazy.preloadImage, 'error')
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ await trigger(wrapper.element._lazy.preloadImage, 'error')
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ await trigger(wrapper.element._lazy.preloadImage, 'error')
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ wrapper.destroy()
+ imageCache.clear()
+ mockRestore()
+})
+
+test('test lazy updated', async () => {
+ const { mockRestore } = mockDoubleRaf()
+ const wrapper = mount(Wrapper)
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ await trigger(wrapper.element._lazy.preloadImage, 'load')
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ await wrapper.setData({ src: 'https://varlet.gitee.io/varlet-ui/dog.jpg' })
+ await delay(80)
+
+ await trigger(wrapper.element._lazy.preloadImage, 'load')
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ wrapper.destroy()
+ imageCache.clear()
+ mockRestore()
+})
+
+test('test lazy background-image', async () => {
+ const { mockRestore } = mockDoubleRaf()
+ const wrapper = mount({
+ directives: { Lazy },
+ template: `
+
+ `,
+ })
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ await trigger(wrapper.element._lazy.preloadImage, 'load')
+ await delay(80)
+ expect(wrapper.html()).toMatchSnapshot()
+
+ wrapper.destroy()
+ imageCache.clear()
+ mockRestore()
+})
diff --git a/packages/varlet-vue2-ui/src/lazy/docs/en-US.md b/packages/varlet-vue2-ui/src/lazy/docs/en-US.md
new file mode 100644
index 0000000..8a3910e
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/docs/en-US.md
@@ -0,0 +1,80 @@
+# Lazy
+
+### Intro
+
+Load when the image is visible
+
+### Install
+
+```js
+import Vue from 'vue'
+import { Lazy } from '@varlet-vue2/ui'
+
+Vue.use(Lazy)
+```
+
+### Basic Use
+
+```html
+
+```
+
+### Background Image Lazy Load
+```html
+
+```
+
+### Inline Attributes
+You can modify the `loading`, `error` image, and `reload attempts` by using inline properties.
+
+```html
+
+```
+
+### Plugin
+
+The option to set the default `Lazy` load option is provided, which is passed in at plugin registration.
+
+```js
+import Vue from 'vue'
+import { Lazy } from '@varlet-vue2/ui'
+
+Vue.use(Lazy, {
+ loading: 'https://xxx.cn/loading.png',
+ error: 'https://xxx.cn/error.png',
+ attempt: 3,
+ throttleWait: 300,
+ events: [
+ 'scroll',
+ 'wheel',
+ 'mousewheel',
+ 'resize',
+ 'animationend',
+ 'transitionend',
+ 'touchmove'
+ ],
+ filter(lazy) {
+ // All properties of the context can be accessed, and some property interceptions can be performed.
+ // Such as simply modifying all image addresses http-> https
+ lazy.src.replace('http://', 'https://')
+ }
+})
+```
+
+## API
+
+### Plugin Options
+
+| Option | Description | Type | Default |
+| --- | --- | --- | --- |
+| `loading` | Loading images, if possible, select images that load quickly | _string_ | `Pixel transparent picture` |
+| `error` | Load failed to display the picture | _string_ | `Pixel transparent picture` |
+| `attempt` | The number of times a load failed to reload | _number_ | `3` |
+| `throttleWait` | throttle wait time, The higher the value, the lower the trigger frequency | _number_ | `300` |
+| `events` | A list of events that trigger visibility detection registration | _string[]_ | `['scroll'...]` |
+| `filter` | Attribute interceptor function | _(lazy: Lazy) => void_ | `() => void` |
\ No newline at end of file
diff --git a/packages/varlet-vue2-ui/src/lazy/docs/zh-CN.md b/packages/varlet-vue2-ui/src/lazy/docs/zh-CN.md
new file mode 100644
index 0000000..7326b9c
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/docs/zh-CN.md
@@ -0,0 +1,80 @@
+# 图片懒加载
+
+### 介绍
+
+在图片可见时进行加载
+
+### 引入
+
+```js
+import Vue from 'vue'
+import { Lazy } from '@varlet-vue2/ui'
+
+Vue.use(Lazy)
+```
+
+### 基本用法
+
+```html
+
+```
+
+### 背景图懒加载
+```html
+
+```
+
+### 内联属性
+可以通过内联属性修改 `loading`、 `error` 图片和`加载失败时尝试重新加载的次数`。
+
+```html
+
+```
+
+### 插件
+
+`Lazy` 提供了在插件注册时传入的选项,可以设置默认的懒加载选项。
+
+```js
+import Vue from 'vue'
+import { Lazy } from '@varlet-vue2/ui'
+
+Vue.use(Lazy, {
+ loading: 'https://xxx.cn/loading.png',
+ error: 'https://xxx.cn/error.png',
+ attempt: 3,
+ throttleWait: 300,
+ events: [
+ 'scroll',
+ 'wheel',
+ 'mousewheel',
+ 'resize',
+ 'animationend',
+ 'transitionend',
+ 'touchmove'
+ ],
+ filter(lazy) {
+ // 可以访问 lazy 上下文的所有属性,执行一些属性拦截,
+ // 比如简单修改所有的图片地址 http -> https
+ lazy.src.replace('http://', 'https://')
+ }
+})
+```
+
+## API
+
+### 插件选项
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| `loading` | 加载中的图片,尽可能选择加载速度很快的图片 | _string_ | `1像素透明图片` |
+| `error` | 加载失败显示的图片 | _string_ | `1像素透明图片` |
+| `attempt` | 加载失败时尝试重新加载的次数 | _number_ | `3` |
+| `throttleWait` | 节流时间,数值越大事件触发频率越低 | _number_ | `300` |
+| `events` | 触发可见性检测注册的事件列表 | _string[]_ | `['scroll'...]` |
+| `filter` | 属性拦截函数 | _(lazy: Lazy) => void_ | `() => void` |
diff --git a/packages/varlet-vue2-ui/src/lazy/example/index.vue b/packages/varlet-vue2-ui/src/lazy/example/index.vue
new file mode 100644
index 0000000..93c8ff3
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/example/index.vue
@@ -0,0 +1,47 @@
+
+
+
{{ pack.basicUsage }}
+
+
+
+
+
+
+
{{ pack.backgroundImageLazyLoad }}
+
+
+
+
+
+
+
diff --git a/packages/varlet-vue2-ui/src/lazy/example/locale/en-US.ts b/packages/varlet-vue2-ui/src/lazy/example/locale/en-US.ts
new file mode 100644
index 0000000..13582cc
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/example/locale/en-US.ts
@@ -0,0 +1,4 @@
+export default {
+ basicUsage: 'Basic Usage',
+ backgroundImageLazyLoad: 'Background Image Lazy Load',
+}
diff --git a/packages/varlet-vue2-ui/src/lazy/example/locale/index.ts b/packages/varlet-vue2-ui/src/lazy/example/locale/index.ts
new file mode 100644
index 0000000..d2e375e
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/example/locale/index.ts
@@ -0,0 +1,23 @@
+// lib
+import _zhCN from '../../../locale/zh-CN'
+import _enCN from '../../../locale/en-US'
+// mobile example doc
+import zhCN from './zh-CN'
+import enUS from './en-US'
+import { useLocale, add as _add, use as _use } from '../../../locale'
+
+const { add, use: exampleUse, pack, packs, merge } = useLocale()
+
+const use = (lang: string) => {
+ _use(lang)
+ exampleUse(lang)
+}
+
+export { add, pack, packs, merge, use }
+
+// lib
+_add('zh-CN', _zhCN)
+_add('en-US', _enCN)
+// mobile example doc
+add('zh-CN', zhCN as any)
+add('en-US', enUS as any)
diff --git a/packages/varlet-vue2-ui/src/lazy/example/locale/zh-CN.ts b/packages/varlet-vue2-ui/src/lazy/example/locale/zh-CN.ts
new file mode 100644
index 0000000..5b0210b
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/example/locale/zh-CN.ts
@@ -0,0 +1,4 @@
+export default {
+ basicUsage: '基本使用',
+ backgroundImageLazyLoad: '背景图懒加载',
+}
diff --git a/packages/varlet-vue2-ui/src/lazy/index.ts b/packages/varlet-vue2-ui/src/lazy/index.ts
new file mode 100644
index 0000000..ebfa6a6
--- /dev/null
+++ b/packages/varlet-vue2-ui/src/lazy/index.ts
@@ -0,0 +1,226 @@
+import { getAllParentScroller, inViewport } from '../utils/elements'
+import { createCache, removeItem, throttle } from '../utils/shared'
+import type { VueConstructor, DirectiveOptions, PluginObject } from 'vue'
+import type { DirectiveBinding } from 'vue/types/options'
+import type { CacheInstance } from '../utils/shared'
+
+interface LazyOptions {
+ loading?: string;
+ error?: string;
+ attempt?: number;
+ throttleWait?: number;
+ filter?: (lazy: Lazy) => void;
+ events?: string[];
+}
+
+type LazyState = 'pending' | 'success' | 'error'
+
+type Lazy = LazyOptions & {
+ src: string;
+ arg: string | undefined;
+ currentAttempt: number;
+ attemptLock: boolean;
+ state: LazyState;
+ preloadImage?: HTMLImageElement;
+}
+
+export interface LazyHTMLElement extends HTMLElement {
+ _lazy?: Lazy;
+}
+type ListenTarget = Window | HTMLElement
+
+const BACKGROUND_IMAGE_ARG_NAME = 'background-image'
+const LAZY_LOADING = 'lazy-loading'
+const LAZY_ERROR = 'lazy-error'
+const LAZY_ATTEMPT = 'lazy-attempt'
+const EVENTS = ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove']
+export const PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
+const lazyElements: LazyHTMLElement[] = []
+const listenTargets: ListenTarget[] = []
+
+export const imageCache: CacheInstance = createCache(100)
+
+export const defaultLazyOptions: LazyOptions = {
+ loading: PIXEL,
+ error: PIXEL,
+ attempt: 3,
+ throttleWait: 300,
+ events: EVENTS,
+}
+
+let checkAllWithThrottle = throttle(checkAll, defaultLazyOptions.throttleWait)
+
+function setSRC(el: LazyHTMLElement, src: string) {
+ if (el._lazy?.arg === BACKGROUND_IMAGE_ARG_NAME) {
+ el.style.backgroundImage = `url(${src})`
+ } else {
+ el.setAttribute('src', src)
+ }
+}
+
+function setLoading(el: LazyHTMLElement) {
+ el._lazy?.loading && setSRC(el, el._lazy.loading)
+
+ checkAll()
+}
+
+function setError(el: LazyHTMLElement) {
+ el._lazy?.error && setSRC(el, el._lazy.error)
+ el._lazy!.state = 'error'
+
+ clear(el)
+ checkAll()
+}
+
+function setSuccess(el: LazyHTMLElement, attemptSRC: string) {
+ setSRC(el, attemptSRC)
+ el._lazy!.state = 'success'
+
+ clear(el)
+ checkAll()
+}
+
+function bindEvents(listenTarget: ListenTarget) {
+ if (listenTargets.includes(listenTarget)) {
+ return
+ }
+ listenTargets.push(listenTarget)
+
+ defaultLazyOptions.events?.forEach((event: string) => {
+ listenTarget.addEventListener(event, checkAllWithThrottle, { passive: true })
+ })
+}
+
+function unbindEvents() {
+ listenTargets.forEach((listenTarget: ListenTarget) => {
+ defaultLazyOptions.events?.forEach((event: string) => {
+ listenTarget.removeEventListener(event, checkAllWithThrottle)
+ })
+ })
+
+ listenTargets.length = 0
+}
+
+function createLazy(el: LazyHTMLElement, binding: DirectiveBinding) {
+ const lazyOptions: LazyOptions = {
+ loading: el.getAttribute(LAZY_LOADING) ?? defaultLazyOptions.loading,
+ error: el.getAttribute(LAZY_ERROR) ?? defaultLazyOptions.error,
+ attempt: el.getAttribute(LAZY_ATTEMPT) ? Number(el.getAttribute(LAZY_ATTEMPT)) : defaultLazyOptions.attempt,
+ }
+
+ el._lazy = {
+ src: binding.value,
+ arg: binding.arg,
+ currentAttempt: 0,
+ state: 'pending',
+ attemptLock: false,
+ ...lazyOptions,
+ }
+
+ setSRC(el, PIXEL)
+
+ defaultLazyOptions.filter?.(el._lazy)
+}
+
+function createImage(el: LazyHTMLElement, attemptSRC: string) {
+ const image: HTMLImageElement = new Image()
+ image.src = attemptSRC
+ el._lazy!.preloadImage = image
+
+ image.addEventListener('load', () => {
+ el._lazy!.attemptLock = false
+
+ imageCache.add(attemptSRC)
+ setSuccess(el, attemptSRC)
+ })
+ image.addEventListener('error', () => {
+ el._lazy!.attemptLock = false
+ ;(el._lazy!.currentAttempt as number) >= (el._lazy!.attempt as number) ? setError(el) : attemptLoad(el)
+ })
+}
+
+function attemptLoad(el: LazyHTMLElement) {
+ if (el._lazy!.attemptLock) {
+ return
+ }
+ el._lazy!.attemptLock = true
+ el._lazy!.currentAttempt++
+
+ const { src: attemptSRC }: Lazy = el._lazy!
+
+ if (imageCache.has(attemptSRC)) {
+ setSuccess(el, attemptSRC)
+ el._lazy!.attemptLock = false
+ return
+ }
+
+ setLoading(el)
+ createImage(el, attemptSRC)
+}
+
+async function check(el: LazyHTMLElement) {
+ ;(await inViewport(el)) && attemptLoad(el)
+}
+
+function checkAll() {
+ lazyElements.forEach((el: LazyHTMLElement) => check(el))
+}
+
+async function add(el: LazyHTMLElement) {
+ !lazyElements.includes(el) && lazyElements.push(el)
+ getAllParentScroller(el).forEach(bindEvents)
+ await check(el)
+}
+
+function clear(el: LazyHTMLElement) {
+ removeItem(lazyElements, el)
+ lazyElements.length === 0 && unbindEvents()
+}
+
+function diff(el: LazyHTMLElement, binding: DirectiveBinding): boolean {
+ const { src, arg } = el._lazy!
+
+ return src !== binding.value || arg !== binding.arg
+}
+
+async function mounted(el: LazyHTMLElement, binding: DirectiveBinding) {
+ createLazy(el, binding)
+ await add(el)
+}
+
+async function updated(el: LazyHTMLElement, binding: DirectiveBinding) {
+ if (!diff(el, binding)) {
+ lazyElements.includes(el) && (await check(el))
+ return
+ }
+
+ await mounted(el, binding)
+}
+
+function mergeLazyOptions(lazyOptions: LazyOptions = {}) {
+ const { events, loading, error, attempt, throttleWait, filter } = lazyOptions
+
+ defaultLazyOptions.events = events ?? defaultLazyOptions.events
+ defaultLazyOptions.loading = loading ?? defaultLazyOptions.loading
+ defaultLazyOptions.error = error ?? defaultLazyOptions.error
+ defaultLazyOptions.attempt = attempt ?? defaultLazyOptions.attempt
+ defaultLazyOptions.throttleWait = throttleWait ?? defaultLazyOptions.throttleWait
+ defaultLazyOptions.filter = filter
+}
+
+const Lazy: DirectiveOptions & PluginObject = {
+ inserted: mounted,
+ unbind: clear,
+ updated,
+ install(app: VueConstructor, lazyOptions?: LazyOptions) {
+ mergeLazyOptions(lazyOptions)
+
+ checkAllWithThrottle = throttle(checkAll, defaultLazyOptions.throttleWait)
+
+ app.directive('lazy', this)
+ },
+}
+
+export const _LazyComponent = Lazy
+
+export default Lazy
diff --git a/packages/varlet-vue2-ui/types/global.d.ts b/packages/varlet-vue2-ui/types/global.d.ts
index ce75fc3..5e81c13 100644
--- a/packages/varlet-vue2-ui/types/global.d.ts
+++ b/packages/varlet-vue2-ui/types/global.d.ts
@@ -1,6 +1,7 @@
declare module 'vue' {
export interface GlobalComponents {
VarButton: typeof import('@varlet-vue2/ui')['_ButtonComponent']
+ VarLazy: typeof import('@varlet-vue2/ui')['_LazyComponent']
VarLocale: typeof import('@varlet-vue2/ui')['_LocaleComponent']
VarSkeleton: typeof import('@varlet-vue2/ui')['_SkeletonComponent']
}
diff --git a/packages/varlet-vue2-ui/types/index.d.ts b/packages/varlet-vue2-ui/types/index.d.ts
index 70d8e71..c0645ba 100644
--- a/packages/varlet-vue2-ui/types/index.d.ts
+++ b/packages/varlet-vue2-ui/types/index.d.ts
@@ -6,6 +6,7 @@ export * from './button'
export * from './locale'
export * from './skeleton'
export * from './cell'
+export * from './lazy'
export * from './progress'
export * from './varComponent'
export * from './varDirective'
diff --git a/packages/varlet-vue2-ui/types/lazy.d.ts b/packages/varlet-vue2-ui/types/lazy.d.ts
new file mode 100644
index 0000000..2255ce0
--- /dev/null
+++ b/packages/varlet-vue2-ui/types/lazy.d.ts
@@ -0,0 +1,5 @@
+import { VarDirective } from './varDirective'
+
+export class Lazy extends VarDirective {}
+
+export class _LazyComponent extends Lazy {}