From fd533d444100676577d2da64701d5f10fc95a5e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 06:38:25 +0000 Subject: [PATCH 1/7] Initial plan From c3b1eeed824daeeeac818c69c73710e29b0471e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 06:44:30 +0000 Subject: [PATCH 2/7] Migrate lazy-image.js and lazy-component.js from Options API to Composition API Co-authored-by: yoyo837 <6134547+yoyo837@users.noreply.github.com> --- .../lazyload/vue-lazyload/lazy-component.js | 114 ++++++----- .../src/lazyload/vue-lazyload/lazy-image.js | 189 ++++++++++-------- 2 files changed, 164 insertions(+), 139 deletions(-) diff --git a/packages/vant/src/lazyload/vue-lazyload/lazy-component.js b/packages/vant/src/lazyload/vue-lazyload/lazy-component.js index 9768a74095f..4cbe06f748f 100644 --- a/packages/vant/src/lazyload/vue-lazyload/lazy-component.js +++ b/packages/vant/src/lazyload/vue-lazyload/lazy-component.js @@ -3,66 +3,76 @@ * 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(props, { slots, emit }) { + const instance = getCurrentInstance(); + const show = ref(false); + const state = reactive({ loaded: false, - }, - show: false, - }; - }, + }); - mounted() { - this.el = this.$el; - lazy.addLazyBox(this); - lazy.lazyLoadHandler(); - }, + const lazyBox = { + get el() { + return instance.proxy.$el; + }, + get $el() { + return instance.proxy.$el; + }, + get $parent() { + return instance.proxy.$parent; + }, + state, + 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 + ); + }, + load() { + show.value = true; + state.loaded = true; + emit('show', instance.proxy); + }, + destroy() { + return undefined; + }, + }; - beforeUnmount() { - lazy.removeComponent(this); - }, + onMounted(() => { + lazy.addLazyBox(lazyBox); + lazy.lazyLoadHandler(); + }); - 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 - ); - }, - - load() { - this.show = true; - this.state.loaded = true; - this.$emit('show', this); - }, + onBeforeUnmount(() => { + lazy.removeComponent(lazyBox); + }); - destroy() { - return this.$destroy; + return () => + h(props.tag, show.value && slots.default ? 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..6529f5126b7 100644 --- a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js +++ b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js @@ -6,105 +6,120 @@ 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, { slots }) { + const instance = getCurrentInstance(); + const renderSrc = ref(''); + const options = { 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`, - ); - } + }); - onFinish(); - return; - } - const { src } = this.options; - loadImageAsync( - { src }, - ({ src }) => { - this.renderSrc = src; - this.state.loaded = true; + 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 lazyBox = { + get el() { + return instance.proxy.$el; + }, + get $el() { + return instance.proxy.$el; }, + get $parent() { + return instance.proxy.$parent; + }, + state, + options, + 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 + ); + }, + 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; + }, + ); + }, + }; + + init(); + + watch( + () => props.src, () => { - this.state.attempt++; - this.renderSrc = this.options.error; - this.state.error = true; + init(); + lazyManager.addLazyBox(lazyBox); + lazyManager.lazyLoadHandler(); }, ); + + onMounted(() => { + lazyManager.addLazyBox(lazyBox); + lazyManager.lazyLoadHandler(); + }); + + onBeforeUnmount(() => { + lazyManager.removeComponent(lazyBox); + }); + + return () => h(props.tag, { src: renderSrc.value }, slots.default?.()); }, - }, -}); + }); From ff4071f0151a11697abd6beb8f3557f42d0545f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 07:03:48 +0000 Subject: [PATCH 3/7] fix(lazyload): remove duplicate addLazyBox call in watch callback Co-authored-by: yoyo837 <6134547+yoyo837@users.noreply.github.com> --- packages/vant/src/lazyload/vue-lazyload/lazy-image.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js index 6529f5126b7..638ed887c6b 100644 --- a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js +++ b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js @@ -106,7 +106,6 @@ export default (lazyManager) => () => props.src, () => { init(); - lazyManager.addLazyBox(lazyBox); lazyManager.lazyLoadHandler(); }, ); From 9e9c459fa63afd3f6975edbc6cb1b9a9fb073e8b Mon Sep 17 00:00:00 2001 From: Amumu Date: Wed, 18 Mar 2026 15:39:28 +0800 Subject: [PATCH 4/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/vant/src/lazyload/vue-lazyload/lazy-image.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js index 638ed887c6b..eb1c6140550 100644 --- a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js +++ b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js @@ -107,6 +107,9 @@ export default (lazyManager) => () => { init(); lazyManager.lazyLoadHandler(); + if (lazyManager.observer && lazyBox.el) { + lazyManager.observer.observe(lazyBox.el); + } }, ); From 79d0f12992de6320d4506284f464139556cf4a83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 02:47:39 +0000 Subject: [PATCH 5/7] chore: sync upstream and tighten CI token permissions --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e5bb58e754..387d7109f95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,9 @@ on: workflow_dispatch: +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest From ed77b94df2b32d46ef63598e8e2213310c44734f Mon Sep 17 00:00:00 2001 From: yoyo837 Date: Mon, 3 Aug 2026 09:41:17 +0800 Subject: [PATCH 6/7] fix(lazyload): expose internal state via setup() to preserve instance access Composition API refactor made setup()'s local state/methods unreachable via instance.proxy, breaking consumers relying on ref access to show/state/load (e.g. via the 'show' event payload or template refs). Use expose() to keep the previous public surface. --- .../vant/src/lazyload/vue-lazyload/lazy-component.js | 10 +++++++++- packages/vant/src/lazyload/vue-lazyload/lazy-image.js | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/vant/src/lazyload/vue-lazyload/lazy-component.js b/packages/vant/src/lazyload/vue-lazyload/lazy-component.js index 4cbe06f748f..604088009ea 100644 --- a/packages/vant/src/lazyload/vue-lazyload/lazy-component.js +++ b/packages/vant/src/lazyload/vue-lazyload/lazy-component.js @@ -25,7 +25,7 @@ export default (lazy) => emits: ['show'], - setup(props, { slots, emit }) { + setup(props, { slots, emit, expose }) { const instance = getCurrentInstance(); const show = ref(false); const state = reactive({ @@ -72,6 +72,14 @@ export default (lazy) => lazy.removeComponent(lazyBox); }); + expose({ + show, + state, + checkInView: lazyBox.checkInView, + load: lazyBox.load, + destroy: lazyBox.destroy, + }); + return () => h(props.tag, show.value && slots.default ? 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 eb1c6140550..73deb6a9a7c 100644 --- a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js +++ b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js @@ -26,7 +26,7 @@ export default (lazyManager) => default: 'img', }, }, - setup(props, { slots }) { + setup(props, { slots, expose }) { const instance = getCurrentInstance(); const renderSrc = ref(''); const options = { @@ -122,6 +122,13 @@ export default (lazyManager) => lazyManager.removeComponent(lazyBox); }); + expose({ + state, + options, + checkInView: lazyBox.checkInView, + load: lazyBox.load, + }); + return () => h(props.tag, { src: renderSrc.value }, slots.default?.()); }, }); From bd93256914d761aadb9cba8a20abcf9d5608fd14 Mon Sep 17 00:00:00 2001 From: yoyo837 Date: Mon, 3 Aug 2026 10:14:24 +0800 Subject: [PATCH 7/7] fix(lazyload): preserve component public instances Return Composition API bindings from setup so component refs, lazy manager listeners, and show event payloads retain the Options API public surface. Keep LazyImage options reactive and add regression tests for public instance access and observer re-registration. --- packages/vant/src/lazyload/test/index.spec.js | 101 ++++++++++++++++ .../lazyload/vue-lazyload/lazy-component.js | 73 ++++++----- .../src/lazyload/vue-lazyload/lazy-image.js | 114 +++++++++--------- 3 files changed, 190 insertions(+), 98 deletions(-) create mode 100644 packages/vant/src/lazyload/test/index.spec.js 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 604088009ea..6d8765c9406 100644 --- a/packages/vant/src/lazyload/vue-lazyload/lazy-component.js +++ b/packages/vant/src/lazyload/vue-lazyload/lazy-component.js @@ -25,62 +25,57 @@ export default (lazy) => emits: ['show'], - setup(props, { slots, emit, expose }) { + setup(_, { emit }) { const instance = getCurrentInstance(); + const el = ref(null); const show = ref(false); const state = reactive({ loaded: false, }); - const lazyBox = { - get el() { - return instance.proxy.$el; - }, - get $el() { - return instance.proxy.$el; - }, - get $parent() { - return instance.proxy.$parent; - }, - state, - 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 - ); - }, - load() { - show.value = true; - state.loaded = true; - emit('show', instance.proxy); - }, - destroy() { - return undefined; - }, + 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 + ); + }; + + const load = () => { + show.value = true; + state.loaded = true; + emit('show', instance.proxy); }; + const destroy = () => instance.proxy.$destroy; + onMounted(() => { - lazy.addLazyBox(lazyBox); + el.value = instance.proxy.$el; + lazy.addLazyBox(instance.proxy); lazy.lazyLoadHandler(); }); onBeforeUnmount(() => { - lazy.removeComponent(lazyBox); + lazy.removeComponent(instance.proxy); }); - expose({ + return { + el, show, state, - checkInView: lazyBox.checkInView, - load: lazyBox.load, - destroy: lazyBox.destroy, - }); + checkInView, + load, + destroy, + }; + }, - return () => - h(props.tag, show.value && slots.default ? slots.default() : null); + 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 73deb6a9a7c..3e1712ee37f 100644 --- a/packages/vant/src/lazyload/vue-lazyload/lazy-image.js +++ b/packages/vant/src/lazyload/vue-lazyload/lazy-image.js @@ -26,15 +26,17 @@ export default (lazyManager) => default: 'img', }, }, - setup(props, { slots, expose }) { + + setup(props) { const instance = getCurrentInstance(); + const el = ref(null); const renderSrc = ref(''); - const options = { + const options = reactive({ src: '', error: '', loading: '', attempt: lazyManager.options.attempt, - }; + }); const state = reactive({ loaded: false, error: false, @@ -50,54 +52,42 @@ export default (lazyManager) => renderSrc.value = options.loading; }; - const lazyBox = { - get el() { - return instance.proxy.$el; - }, - get $el() { - return instance.proxy.$el; - }, - get $parent() { - return instance.proxy.$parent; - }, - state, - options, - 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 - ); - }, - 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 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`, + ); } - 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 } = options; + loadImageAsync( + { src }, + ({ src }) => { + renderSrc.value = src; + state.loaded = true; + }, + () => { + state.attempt++; + renderSrc.value = options.error; + state.error = true; + }, + ); }; init(); @@ -107,28 +97,34 @@ export default (lazyManager) => () => { init(); lazyManager.lazyLoadHandler(); - if (lazyManager.observer && lazyBox.el) { - lazyManager.observer.observe(lazyBox.el); + if (lazyManager.observer && el.value) { + lazyManager.observer.observe(el.value); } }, ); onMounted(() => { - lazyManager.addLazyBox(lazyBox); + el.value = instance.proxy.$el; + lazyManager.addLazyBox(instance.proxy); lazyManager.lazyLoadHandler(); }); onBeforeUnmount(() => { - lazyManager.removeComponent(lazyBox); + lazyManager.removeComponent(instance.proxy); }); - expose({ + return { + el, + renderSrc, state, options, - checkInView: lazyBox.checkInView, - load: lazyBox.load, - }); + init, + checkInView, + load, + }; + }, - return () => h(props.tag, { src: renderSrc.value }, slots.default?.()); + render() { + return h(this.tag, { src: this.renderSrc }, this.$slots.default?.()); }, });