Vue3中vue-intersect搭配v-if失效问题的原因与解决方法
问题分析与解决方案
问题重现
使用如下代码时,当activated为true,<intersect>组件对应的DOM元素出现,但滚动到该元素时,@enter绑定的addImages()完全不触发;移除v-if后,大屏幕下页面加载时组件会多次触发激活。
<intersect v-if="activated == true" @enter="addImages();"> <div class="h-1 w-full border"></div> </intersect>
失效原因
结合你提供的vue-intersect组件代码,问题出在抽象组件的挂载时机与DOM元素的绑定逻辑:
- 该
intersect是抽象组件(abstract: true),本身不会生成DOM节点,直接渲染插槽内的子元素。 - 组件在
mounted钩子的$nextTick中尝试获取插槽子元素的真实DOM(this.$slots.default[0].elm)并绑定IntersectionObserver。但当用v-if控制组件挂载时,mounted执行时,插槽内的子元素可能尚未完全插入到DOM树中,导致Observer没有成功绑定到目标元素上,后续滚动自然无法触发事件。 - 移除
v-if后多次触发的问题,是因为组件默认threshold为[0, 0.2]——当元素可见度达到0%和20%时,都会触发isIntersecting为true,进而多次触发@enter。
解决方案
方案1:控制插槽子元素而非intersect组件
把v-if移到插槽内的子元素上,同时修改intersect组件,使其能在子元素动态插入后重新绑定Observer:
<!-- 父组件代码 --> <intersect @enter="addImages()"> <div v-if="activated" class="h-1 w-full border"></div> </intersect>
修改intersect组件代码,添加updated钩子和状态跟踪已观察的元素:
// intersect组件修改后代码 import Vue from 'vue' const warn = (msg) => { if (!Vue.config.silent) { console.warn(msg) } } export default { name: 'intersect', abstract: true, data() { return { observedElm: null // 跟踪已观察的DOM元素 } }, props: { threshold: { type: Array, required: false, default: () => [0, 0.2] }, root: { type: typeof HTMLElement !== 'undefined' ? HTMLElement : Object, required: false, default: () => null }, rootMargin: { type: String, required: false, default: () => '0px 0px 0px 0px' } }, mounted () { this.observer = new IntersectionObserver((entries) => { if (!entries[0].isIntersecting) { this.$emit('leave', [entries[0]]) } else { this.$emit('enter', [entries[0]]) } this.$emit('change', [entries[0]]) }, { threshold: this.threshold, root: this.root, rootMargin: this.rootMargin }) }, updated () { this.$nextTick(() => { if (this.$slots.default && this.$slots.default.length > 1) { warn('[VueIntersect] You may only wrap one element in a <intersect> component.') } else if (!this.$slots.default || this.$slots.default.length < 1) { warn('[VueIntersect] You must have one child inside a <intersect> component.') return } const currentElm = this.$slots.default[0].elm // 元素变化时,取消旧观察,绑定新元素 if (this.observedElm !== currentElm) { if (this.observedElm) { this.observer.unobserve(this.observedElm) } this.observedElm = currentElm this.observer.observe(currentElm) } }) }, destroyed () { this.$emit('destroyed') this.observer.disconnect() }, render () { return this.$slots.default ? this.$slots.default[0] : null } }
方案2:调整intersect组件的观察时机
把原mounted中的观察逻辑移到updated钩子,确保DOM元素已完全插入后再绑定Observer:
// intersect组件修改后核心部分 mounted () { this.observer = new IntersectionObserver((entries) => { if (!entries[0].isIntersecting) { this.$emit('leave', [entries[0]]) } else { this.$emit('enter', [entries[0]]) } this.$emit('change', [entries[0]]) }, { threshold: this.threshold, root: this.root, rootMargin: this.rootMargin }) }, updated () { this.$nextTick(() => { if (this.$slots.default && this.$slots.default.length > 1) { warn('[VueIntersect] You may only wrap one element in a <intersect> component.') } else if (!this.$slots.default || this.$slots.default.length < 1) { warn('[VueIntersect] You must have one child inside a <intersect> component.') return } const currentElm = this.$slots.default[0].elm if (this.observedElm !== currentElm) { if (this.observedElm) { this.observer.unobserve(this.observedElm) } this.observedElm = currentElm this.observer.observe(currentElm) } }) }, data() { return { observedElm: null } }
父组件仍保留v-if控制intersect组件即可:
<intersect v-if="activated" @enter="addImages();"> <div class="h-1 w-full border"></div> </intersect>
方案3:用v-show替代v-if(简化方案)
如果业务允许,用v-show控制组件显示(元素始终存在于DOM中),同时调整threshold为[0]避免多次触发:
<intersect v-show="activated" :threshold="[0]" @enter="addImages();"> <div class="h-1 w-full border"></div> </intersect>
此方案无需修改组件代码,但注意v-show是通过display: none控制显示,元素始终存在于DOM中,若元素初始在视口内,仍会触发一次@enter。
内容的提问来源于stack exchange,提问作者user1664377
相关产品推荐
相关产品推荐

