Skip to content

Commit 064cdf4

Browse files
fix: ensure all intervals/timeouts/observers are cleared when component is destroyed (#5362)
* chore: tests cleanup * Only stub components when really needed * Update button-toolbar.spec.js * Update carousel-slide.spec.js * Update form-checkbox-group.spec.js * Update form-radio-group.spec.js * Update tabs.spec.js * Update click-out.spec.js * Update focus-in.spec.js * Update dom.spec.js * fix: ensure all intervals/timeouts/observers are cleared * Unify variable name for non-reactive properties * Shave off some bytes * Update visible.js * Update mixin-tbody.js * Update modal.js * Update carousel.js Co-authored-by: Troy Morehouse <troymore@nbnet.nb.ca>
1 parent c3db758 commit 064cdf4

File tree

14 files changed

+137
-134
lines changed

14 files changed

+137
-134
lines changed

src/components/carousel/carousel.js

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,10 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
205205
},
206206
created() {
207207
// Create private non-reactive props
208-
this._intervalId = null
209-
this._animationTimeout = null
210-
this._touchTimeout = null
208+
this.$_interval = null
209+
this.$_animationTimeout = null
210+
this.$_touchTimeout = null
211+
this.$_observer = null
211212
// Set initial paused state
212213
this.isPaused = !(toInteger(this.interval, 0) > 0)
213214
},
@@ -217,22 +218,39 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
217218
// Get all slides
218219
this.updateSlides()
219220
// Observe child changes so we can update slide list
220-
observeDom(this.$refs.inner, this.updateSlides.bind(this), {
221-
subtree: false,
222-
childList: true,
223-
attributes: true,
224-
attributeFilter: ['id']
225-
})
221+
this.setObserver(true)
226222
},
227223
beforeDestroy() {
228-
clearTimeout(this._animationTimeout)
229-
clearTimeout(this._touchTimeout)
230-
clearInterval(this._intervalId)
231-
this._intervalId = null
232-
this._animationTimeout = null
233-
this._touchTimeout = null
224+
this.clearInterval()
225+
this.clearAnimationTimeout()
226+
this.clearTouchTimeout()
227+
this.setObserver(false)
234228
},
235229
methods: {
230+
clearInterval() {
231+
clearInterval(this.$_interval)
232+
this.$_interval = null
233+
},
234+
clearAnimationTimeout() {
235+
clearTimeout(this.$_animationTimeout)
236+
this.$_animationTimeout = null
237+
},
238+
clearTouchTimeout() {
239+
clearTimeout(this.$_touchTimeout)
240+
this.$_touchTimeout = null
241+
},
242+
setObserver(on = false) {
243+
this.$_observer && this.$_observer.disconnect()
244+
this.$_observer = null
245+
if (on) {
246+
this.$_observer = observeDom(this.$refs.inner, this.updateSlides.bind(this), {
247+
subtree: false,
248+
childList: true,
249+
attributes: true,
250+
attributeFilter: ['id']
251+
})
252+
}
253+
},
236254
// Set slide
237255
setSlide(slide, direction = null) {
238256
// Don't animate when page is not visible
@@ -286,24 +304,18 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
286304
if (!evt) {
287305
this.isPaused = true
288306
}
289-
if (this._intervalId) {
290-
clearInterval(this._intervalId)
291-
this._intervalId = null
292-
}
307+
this.clearInterval()
293308
},
294309
// Start auto rotate slides
295310
start(evt) {
296311
if (!evt) {
297312
this.isPaused = false
298313
}
299314
/* istanbul ignore next: most likely will never happen, but just in case */
300-
if (this._intervalId) {
301-
clearInterval(this._intervalId)
302-
this._intervalId = null
303-
}
315+
this.clearInterval()
304316
// Don't start if no interval, or less than 2 slides
305317
if (this.interval && this.numSlides > 1) {
306-
this._intervalId = setInterval(this.next, mathMax(1000, this.interval))
318+
this.$_interval = setInterval(this.next, mathMax(1000, this.interval))
307319
}
308320
},
309321
// Restart auto rotate slides when focus/hover leaves the carousel
@@ -362,7 +374,7 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
362374
eventOff(currentSlide, evt, onceTransEnd, EVENT_OPTIONS_NO_CAPTURE)
363375
)
364376
}
365-
this._animationTimeout = null
377+
this.clearAnimationTimeout()
366378
removeClass(nextSlide, dirClass)
367379
removeClass(nextSlide, overlayClass)
368380
addClass(nextSlide, 'active')
@@ -387,7 +399,7 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
387399
)
388400
}
389401
// Fallback to setTimeout()
390-
this._animationTimeout = setTimeout(onceTransEnd, TRANS_DURATION)
402+
this.$_animationTimeout = setTimeout(onceTransEnd, TRANS_DURATION)
391403
}
392404
if (isCycling) {
393405
this.start(false)
@@ -480,10 +492,8 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
480492
// is NOT fired) and after a timeout (to allow for mouse compatibility
481493
// events to fire) we explicitly restart cycling
482494
this.pause(false)
483-
if (this._touchTimeout) {
484-
clearTimeout(this._touchTimeout)
485-
}
486-
this._touchTimeout = setTimeout(
495+
this.clearTouchTimeout()
496+
this.$_touchTimeout = setTimeout(
487497
this.start,
488498
TOUCH_EVENT_COMPAT_WAIT + mathMax(1000, this.interval)
489499
)

src/components/form-spinbutton/form-spinbutton.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({
466466
resetTimers() {
467467
clearTimeout(this.$_autoDelayTimer)
468468
clearInterval(this.$_autoRepeatTimer)
469+
this.$_autoDelayTimer = null
470+
this.$_autoRepeatTimer = null
469471
},
470472
clearRepeat() {
471473
this.resetTimers()

src/components/modal/modal.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({
450450
},
451451
created() {
452452
// Define non-reactive properties
453-
this._observer = null
453+
this.$_observer = null
454454
},
455455
mounted() {
456456
// Set initial z-index as queried from the DOM
@@ -470,17 +470,25 @@ export const BModal = /*#__PURE__*/ Vue.extend({
470470
},
471471
beforeDestroy() {
472472
// Ensure everything is back to normal
473-
if (this._observer) {
474-
this._observer.disconnect()
475-
this._observer = null
476-
}
473+
this.setObserver(false)
477474
if (this.isVisible) {
478475
this.isVisible = false
479476
this.isShow = false
480477
this.isTransitioning = false
481478
}
482479
},
483480
methods: {
481+
setObserver(on = false) {
482+
this.$_observer && this.$_observer.disconnect()
483+
this.$_observer = null
484+
if (on) {
485+
this.$_observer = observeDom(
486+
this.$refs.content,
487+
this.checkModalOverflow.bind(this),
488+
OBSERVER_CONFIG
489+
)
490+
}
491+
},
484492
// Private method to update the v-model
485493
updateModel(val) {
486494
if (val !== this.visible) {
@@ -562,10 +570,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({
562570
return
563571
}
564572
// Stop observing for content changes
565-
if (this._observer) {
566-
this._observer.disconnect()
567-
this._observer = null
568-
}
573+
this.setObserver(false)
569574
// Trigger the hide transition
570575
this.isVisible = false
571576
// Update the v-model
@@ -615,13 +620,9 @@ export const BModal = /*#__PURE__*/ Vue.extend({
615620
// Update the v-model
616621
this.updateModel(true)
617622
this.$nextTick(() => {
618-
// In a nextTick in case modal content is lazy
619623
// Observe changes in modal content and adjust if necessary
620-
this._observer = observeDom(
621-
this.$refs.content,
622-
this.checkModalOverflow.bind(this),
623-
OBSERVER_CONFIG
624-
)
624+
// In a `$nextTick()` in case modal content is lazy
625+
this.setObserver(true)
625626
})
626627
})
627628
},

src/components/table/helpers/mixin-filtering.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ export default {
101101
// Watch for debounce being set to 0
102102
computedFilterDebounce(newVal) {
103103
if (!newVal && this.$_filterTimer) {
104-
clearTimeout(this.$_filterTimer)
105-
this.$_filterTimer = null
104+
this.clearFilterTimer()
106105
this.localFilter = this.filterSanitize(this.filter)
107106
}
108107
},
@@ -113,8 +112,7 @@ export default {
113112
deep: true,
114113
handler(newCriteria) {
115114
const timeout = this.computedFilterDebounce
116-
clearTimeout(this.$_filterTimer)
117-
this.$_filterTimer = null
115+
this.clearFilterTimer()
118116
if (timeout && timeout > 0) {
119117
// If we have a debounce time, delay the update of `localFilter`
120118
this.$_filterTimer = setTimeout(() => {
@@ -155,7 +153,7 @@ export default {
155153
}
156154
},
157155
created() {
158-
// Create non-reactive prop where we store the debounce timer id
156+
// Create private non-reactive props
159157
this.$_filterTimer = null
160158
// If filter is "pre-set", set the criteria
161159
// This will trigger any watchers/dependents
@@ -167,10 +165,13 @@ export default {
167165
})
168166
},
169167
beforeDestroy() /* istanbul ignore next */ {
170-
clearTimeout(this.$_filterTimer)
171-
this.$_filterTimer = null
168+
this.clearFilterTimer()
172169
},
173170
methods: {
171+
clearFilterTimer() {
172+
clearTimeout(this.$_filterTimer)
173+
this.$_filterTimer = null
174+
},
174175
filterSanitize(criteria) {
175176
// Sanitizes filter criteria based on internal or external filtering
176177
if (

src/components/table/helpers/mixin-tbody.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const props = {
1717
export default {
1818
mixins: [tbodyRowMixin],
1919
props,
20+
beforeDestroy() {
21+
this.$_bodyFieldSlotNameCache = null
22+
},
2023
methods: {
2124
// Helper methods
2225
getTbodyTrs() {

src/components/tabs/tabs.js

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
312312
}
313313
},
314314
created() {
315+
// Create private non-reactive props
316+
this.$_observer = null
315317
this.currentTab = toInteger(this.value, -1)
316-
this._bvObserver = null
317318
// For SSR and to make sure only a single tab is shown on mount
318319
// We wrap this in a `$nextTick()` to ensure the child tabs have been created
319320
this.$nextTick(() => {
@@ -362,11 +363,11 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
362363
unregisterTab(tab) {
363364
this.registeredTabs = this.registeredTabs.slice().filter(t => t !== tab)
364365
},
366+
// DOM observer is needed to detect changes in order of tabs
365367
setObserver(on) {
366-
// DOM observer is needed to detect changes in order of tabs
368+
this.$_observer && this.$_observer.disconnect()
369+
this.$_observer = null
367370
if (on) {
368-
// Make sure no existing observer running
369-
this.setObserver(false)
370371
const self = this
371372
/* istanbul ignore next: difficult to test mutation observer in JSDOM */
372373
const handler = () => {
@@ -379,18 +380,12 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
379380
})
380381
}
381382
// Watch for changes to <b-tab> sub components
382-
this._bvObserver = observeDom(this.$refs.tabsContainer, handler, {
383+
this.$_observer = observeDom(this.$refs.tabsContainer, handler, {
383384
childList: true,
384385
subtree: false,
385386
attributes: true,
386387
attributeFilter: ['id']
387388
})
388-
} else {
389-
/* istanbul ignore next */
390-
if (this._bvObserver && this._bvObserver.disconnect) {
391-
this._bvObserver.disconnect()
392-
}
393-
this._bvObserver = null
394389
}
395390
},
396391
getTabs() {

src/components/tooltip/helpers/bv-popper.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ export const BVPopper = /*#__PURE__*/ Vue.extend({
157157
updated() {
158158
// Update popper if needed
159159
// TODO: Should this be a watcher on `this.popperConfig` instead?
160-
this.popperUpdate()
160+
this.updatePopper()
161161
},
162162
beforeDestroy() {
163-
this.popperDestroy()
163+
this.destroyPopper()
164164
},
165165
destroyed() {
166166
// Make sure template is removed from DOM
@@ -198,16 +198,16 @@ export const BVPopper = /*#__PURE__*/ Vue.extend({
198198
return this.offset
199199
},
200200
popperCreate(el) {
201-
this.popperDestroy()
201+
this.destroyPopper()
202202
// We use `el` rather than `this.$el` just in case the original
203203
// mountpoint root element type was changed by the template
204204
this.$_popper = new Popper(this.target, el, this.popperConfig)
205205
},
206-
popperDestroy() {
206+
destroyPopper() {
207207
this.$_popper && this.$_popper.destroy()
208208
this.$_popper = null
209209
},
210-
popperUpdate() {
210+
updatePopper() {
211211
this.$_popper && this.$_popper.scheduleUpdate()
212212
},
213213
popperPlacementChange(data) {

src/components/tooltip/helpers/bv-tooltip.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
343343
this.clearActiveTriggers()
344344
this.localPlacementTarget = null
345345
try {
346-
this.$_tip && this.$_tip.$destroy()
346+
this.$_tip.$destroy()
347347
} catch {}
348348
this.$_tip = null
349349
this.removeAriaDescribedby()
@@ -552,16 +552,12 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
552552
return this.isDropdown() && target && select(DROPDOWN_OPEN_SELECTOR, target)
553553
},
554554
clearHoverTimeout() {
555-
if (this.$_hoverTimeout) {
556-
clearTimeout(this.$_hoverTimeout)
557-
this.$_hoverTimeout = null
558-
}
555+
clearTimeout(this.$_hoverTimeout)
556+
this.$_hoverTimeout = null
559557
},
560558
clearVisibilityInterval() {
561-
if (this.$_visibleInterval) {
562-
clearInterval(this.$_visibleInterval)
563-
this.$_visibleInterval = null
564-
}
559+
clearInterval(this.$_visibleInterval)
560+
this.$_visibleInterval = null
565561
},
566562
clearActiveTriggers() {
567563
for (const trigger in this.activeTrigger) {

0 commit comments

Comments
 (0)