diff --git a/packages/vant/src/lazyload/test/index.spec.js b/packages/vant/src/lazyload/test/index.spec.js
new file mode 100644
index 00000000000..7179ab517f3
--- /dev/null
+++ b/packages/vant/src/lazyload/test/index.spec.js
@@ -0,0 +1,101 @@
+import { h, isReactive, nextTick } from 'vue';
+import { mount } from '../../../test';
+import createLazyComponent from '../vue-lazyload/lazy-component';
+import createLazyImage from '../vue-lazyload/lazy-image';
+
+const createLazyManager = (overrides = {}) => ({
+ options: {
+ attempt: 3,
+ preLoad: 1,
+ silent: true,
+ },
+ addLazyBox: rs.fn(),
+ removeComponent: rs.fn(),
+ lazyLoadHandler: rs.fn(),
+ valueFormatter: (src) => ({
+ src,
+ loading: `loading-${src}`,
+ error: `error-${src}`,
+ }),
+ ...overrides,
+});
+
+test('LazyComponent should preserve the public component instance', async () => {
+ const lazy = createLazyManager();
+ const wrapper = mount(createLazyComponent(lazy), {
+ slots: {
+ default: () => h('span', 'content'),
+ },
+ });
+ const vm = wrapper.vm;
+ const lazyBox = lazy.addLazyBox.mock.calls[0][0];
+
+ expect(lazy.addLazyBox).toHaveBeenCalledTimes(1);
+ expect(lazyBox.$el).toBe(wrapper.element);
+ expect(lazyBox.el).toBe(wrapper.element);
+ expect(lazyBox.load).toBe(vm.load);
+ expect(vm.el).toBe(wrapper.element);
+ expect(vm.show).toBe(false);
+ expect(vm.state).toEqual({ loaded: false });
+ expect(vm.checkInView).toBeTypeOf('function');
+ expect(vm.load).toBeTypeOf('function');
+ expect(vm.destroy).toBeTypeOf('function');
+
+ vm.load();
+ await nextTick();
+
+ expect(vm.show).toBe(true);
+ expect(vm.state.loaded).toBe(true);
+
+ const payload = wrapper.emitted('show')[0][0];
+ expect(payload.show).toBe(true);
+ expect(payload.state.loaded).toBe(true);
+ expect(payload.load).toBe(vm.load);
+ expect(payload.checkInView).toBe(vm.checkInView);
+ expect(wrapper.html()).toBe('
content
');
+
+ wrapper.unmount();
+ expect(lazy.removeComponent.mock.calls[0][0]).toBe(lazyBox);
+});
+
+test('LazyImage should preserve public state and re-observe on src change', async () => {
+ const observer = {
+ observe: rs.fn(),
+ };
+ const lazy = createLazyManager({ observer });
+ const wrapper = mount(createLazyImage(lazy), {
+ props: {
+ src: 'a.png',
+ },
+ });
+ const vm = wrapper.vm;
+ const lazyBox = lazy.addLazyBox.mock.calls[0][0];
+
+ expect(lazy.addLazyBox).toHaveBeenCalledTimes(1);
+ expect(lazyBox.$el).toBe(wrapper.element);
+ expect(lazyBox.el).toBe(wrapper.element);
+ expect(lazyBox.load).toBe(vm.load);
+ expect(vm.el).toBe(wrapper.element);
+ expect(vm.src).toBe('a.png');
+ expect(vm.renderSrc).toBe('loading-a.png');
+ expect(vm.options.src).toBe('a.png');
+ expect(isReactive(vm.options)).toBe(true);
+ expect(isReactive(vm.state)).toBe(true);
+ expect(vm.init).toBeTypeOf('function');
+ expect(vm.checkInView).toBeTypeOf('function');
+ expect(vm.load).toBeTypeOf('function');
+
+ await wrapper.setProps({ src: 'b.png' });
+
+ expect(lazy.addLazyBox).toHaveBeenCalledTimes(1);
+ expect(lazy.lazyLoadHandler).toHaveBeenCalledTimes(2);
+ expect(observer.observe).toHaveBeenCalledTimes(1);
+ expect(observer.observe).toHaveBeenCalledWith(wrapper.element);
+ expect(vm.src).toBe('b.png');
+ expect(vm.renderSrc).toBe('loading-b.png');
+ expect(vm.options.src).toBe('b.png');
+ expect(wrapper.attributes('src')).toBe('loading-b.png');
+
+ wrapper.unmount();
+ expect(lazy.removeComponent.mock.calls[0][0]).toBe(lazyBox);
+});
diff --git a/packages/vant/src/lazyload/vue-lazyload/lazy-component.js b/packages/vant/src/lazyload/vue-lazyload/lazy-component.js
index 9768a74095f..6d8765c9406 100644
--- a/packages/vant/src/lazyload/vue-lazyload/lazy-component.js
+++ b/packages/vant/src/lazyload/vue-lazyload/lazy-component.js
@@ -3,66 +3,79 @@
* license at https://github.com/hilongjw/vue-lazyload/blob/master/LICENSE
*/
-import { h } from 'vue';
+import {
+ h,
+ ref,
+ reactive,
+ defineComponent,
+ getCurrentInstance,
+ onMounted,
+ onBeforeUnmount,
+} from 'vue';
import { inBrowser, useRect } from '@vant/use';
-export default (lazy) => ({
- props: {
- tag: {
- type: String,
- default: 'div',
+export default (lazy) =>
+ defineComponent({
+ props: {
+ tag: {
+ type: String,
+ default: 'div',
+ },
},
- },
-
- emits: ['show'],
- render() {
- return h(
- this.tag,
- this.show && this.$slots.default ? this.$slots.default() : null,
- );
- },
+ emits: ['show'],
- data() {
- return {
- el: null,
- state: {
+ setup(_, { emit }) {
+ const instance = getCurrentInstance();
+ const el = ref(null);
+ const show = ref(false);
+ const state = reactive({
loaded: false,
- },
- show: false,
- };
- },
+ });
- mounted() {
- this.el = this.$el;
- lazy.addLazyBox(this);
- lazy.lazyLoadHandler();
- },
+ const checkInView = () => {
+ const rect = useRect(instance.proxy.$el);
+ return (
+ inBrowser &&
+ rect.top < window.innerHeight * lazy.options.preLoad &&
+ rect.bottom > 0 &&
+ rect.left < window.innerWidth * lazy.options.preLoad &&
+ rect.right > 0
+ );
+ };
- beforeUnmount() {
- lazy.removeComponent(this);
- },
+ const load = () => {
+ show.value = true;
+ state.loaded = true;
+ emit('show', instance.proxy);
+ };
- methods: {
- checkInView() {
- const rect = useRect(this.$el);
- return (
- inBrowser &&
- rect.top < window.innerHeight * lazy.options.preLoad &&
- rect.bottom > 0 &&
- rect.left < window.innerWidth * lazy.options.preLoad &&
- rect.right > 0
- );
- },
+ const destroy = () => instance.proxy.$destroy;
+
+ onMounted(() => {
+ el.value = instance.proxy.$el;
+ lazy.addLazyBox(instance.proxy);
+ lazy.lazyLoadHandler();
+ });
- load() {
- this.show = true;
- this.state.loaded = true;
- this.$emit('show', this);
+ onBeforeUnmount(() => {
+ lazy.removeComponent(instance.proxy);
+ });
+
+ return {
+ el,
+ show,
+ state,
+ checkInView,
+ load,
+ destroy,
+ };
},
- destroy() {
- return this.$destroy;
+ render() {
+ return h(
+ this.tag,
+ this.show && this.$slots.default ? this.$slots.default() : null,
+ );
},
- },
-});
+ });
diff --git a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js
index 5cbdb9c5050..3e1712ee37f 100644
--- a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js
+++ b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js
@@ -6,105 +6,125 @@
import { useRect } from '@vant/use';
import { loadImageAsync } from './util';
import { noop } from '../../utils';
-import { h } from 'vue';
+import {
+ h,
+ ref,
+ reactive,
+ defineComponent,
+ getCurrentInstance,
+ onMounted,
+ onBeforeUnmount,
+ watch,
+} from 'vue';
-export default (lazyManager) => ({
- props: {
- src: [String, Object],
- tag: {
- type: String,
- default: 'img',
- },
- },
- render() {
- return h(
- this.tag,
- {
- src: this.renderSrc,
+export default (lazyManager) =>
+ defineComponent({
+ props: {
+ src: [String, Object],
+ tag: {
+ type: String,
+ default: 'img',
},
- this.$slots.default?.(),
- );
- },
- data() {
- return {
- el: null,
- options: {
+ },
+
+ setup(props) {
+ const instance = getCurrentInstance();
+ const el = ref(null);
+ const renderSrc = ref('');
+ const options = reactive({
src: '',
error: '',
loading: '',
attempt: lazyManager.options.attempt,
- },
- state: {
+ });
+ const state = reactive({
loaded: false,
error: false,
attempt: 0,
- },
- renderSrc: '',
- };
- },
- watch: {
- src() {
- this.init();
- lazyManager.addLazyBox(this);
- lazyManager.lazyLoadHandler();
- },
- },
- created() {
- this.init();
- },
- mounted() {
- this.el = this.$el;
- lazyManager.addLazyBox(this);
- lazyManager.lazyLoadHandler();
- },
- beforeUnmount() {
- lazyManager.removeComponent(this);
- },
- methods: {
- init() {
- const { src, loading, error } = lazyManager.valueFormatter(this.src);
- this.state.loaded = false;
- this.options.src = src;
- this.options.error = error;
- this.options.loading = loading;
- this.renderSrc = this.options.loading;
- },
- checkInView() {
- const rect = useRect(this.$el);
- return (
- rect.top < window.innerHeight * lazyManager.options.preLoad &&
- rect.bottom > 0 &&
- rect.left < window.innerWidth * lazyManager.options.preLoad &&
- rect.right > 0
- );
- },
- load(onFinish = noop) {
- if (this.state.attempt > this.options.attempt - 1 && this.state.error) {
- if (
- process.env.NODE_ENV !== 'production' &&
- !lazyManager.options.silent
- ) {
- console.log(
- `[@vant/lazyload] ${this.options.src} tried too more than ${this.options.attempt} times`,
- );
+ });
+
+ const init = () => {
+ const { src, loading, error } = lazyManager.valueFormatter(props.src);
+ state.loaded = false;
+ options.src = src;
+ options.error = error;
+ options.loading = loading;
+ renderSrc.value = options.loading;
+ };
+
+ const checkInView = () => {
+ const rect = useRect(instance.proxy.$el);
+ return (
+ rect.top < window.innerHeight * lazyManager.options.preLoad &&
+ rect.bottom > 0 &&
+ rect.left < window.innerWidth * lazyManager.options.preLoad &&
+ rect.right > 0
+ );
+ };
+
+ const load = (onFinish = noop) => {
+ if (state.attempt > options.attempt - 1 && state.error) {
+ if (
+ process.env.NODE_ENV !== 'production' &&
+ !lazyManager.options.silent
+ ) {
+ console.log(
+ `[@vant/lazyload] ${options.src} tried too more than ${options.attempt} times`,
+ );
+ }
+ onFinish();
+ return;
}
+ const { src } = options;
+ loadImageAsync(
+ { src },
+ ({ src }) => {
+ renderSrc.value = src;
+ state.loaded = true;
+ },
+ () => {
+ state.attempt++;
+ renderSrc.value = options.error;
+ state.error = true;
+ },
+ );
+ };
- onFinish();
- return;
- }
- const { src } = this.options;
- loadImageAsync(
- { src },
- ({ src }) => {
- this.renderSrc = src;
- this.state.loaded = true;
- },
+ init();
+
+ watch(
+ () => props.src,
() => {
- this.state.attempt++;
- this.renderSrc = this.options.error;
- this.state.error = true;
+ init();
+ lazyManager.lazyLoadHandler();
+ if (lazyManager.observer && el.value) {
+ lazyManager.observer.observe(el.value);
+ }
},
);
+
+ onMounted(() => {
+ el.value = instance.proxy.$el;
+ lazyManager.addLazyBox(instance.proxy);
+ lazyManager.lazyLoadHandler();
+ });
+
+ onBeforeUnmount(() => {
+ lazyManager.removeComponent(instance.proxy);
+ });
+
+ return {
+ el,
+ renderSrc,
+ state,
+ options,
+ init,
+ checkInView,
+ load,
+ };
+ },
+
+ render() {
+ return h(this.tag, { src: this.renderSrc }, this.$slots.default?.());
},
- },
-});
+ });