🌐 AI搜索 & 代理 主页
Skip to content

Commit 7765d7c

Browse files
authored
chore(refactor): move away from lifecycle hook listeners (#6381)
* chore(refactor): move away from lifecycle hook listeners * Update listen-on-root.spec.js
1 parent f27a10d commit 7765d7c

File tree

9 files changed

+190
-121
lines changed

9 files changed

+190
-121
lines changed

src/components/form-tags/form-tags.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22
// Based loosely on https://adamwathan.me/renderless-components-in-vuejs/
33
import { Vue } from '../../vue'
44
import { NAME_FORM_TAGS } from '../../constants/components'
5-
import {
6-
EVENT_NAME_TAG_STATE,
7-
EVENT_OPTIONS_PASSIVE,
8-
HOOK_EVENT_NAME_BEFORE_DESTROY
9-
} from '../../constants/events'
5+
import { EVENT_NAME_TAG_STATE, EVENT_OPTIONS_PASSIVE } from '../../constants/events'
106
import { CODE_BACKSPACE, CODE_DELETE, CODE_ENTER } from '../../constants/key-codes'
117
import {
128
PROP_TYPE_ARRAY,
@@ -286,9 +282,12 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
286282
const $form = closest('form', this.$el)
287283
if ($form) {
288284
eventOn($form, 'reset', this.reset, EVENT_OPTIONS_PASSIVE)
289-
this.$on(HOOK_EVENT_NAME_BEFORE_DESTROY, () => {
290-
eventOff($form, 'reset', this.reset, EVENT_OPTIONS_PASSIVE)
291-
})
285+
}
286+
},
287+
beforeDestroy() {
288+
const $form = closest('form', this.$el)
289+
if ($form) {
290+
eventOff($form, 'reset', this.reset, EVENT_OPTIONS_PASSIVE)
292291
}
293292
},
294293
methods: {

src/components/modal/helpers/modal-manager.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import { Vue } from '../../../vue'
77
import { IS_BROWSER } from '../../../constants/env'
8-
import { HOOK_EVENT_NAME_BEFORE_DESTROY } from '../../../constants/events'
98
import {
109
addClass,
1110
getAttr,
@@ -82,11 +81,7 @@ const ModalManager = /*#__PURE__*/ Vue.extend({
8281
registerModal(modal) {
8382
// Register the modal if not already registered
8483
if (modal && this.modals.indexOf(modal) === -1) {
85-
// Add modal to modals array
8684
this.modals.push(modal)
87-
modal.$once(HOOK_EVENT_NAME_BEFORE_DESTROY, () => {
88-
this.unregisterModal(modal)
89-
})
9085
}
9186
},
9287
unregisterModal(modal) {
@@ -95,13 +90,13 @@ const ModalManager = /*#__PURE__*/ Vue.extend({
9590
// Remove modal from modals array
9691
this.modals.splice(index, 1)
9792
// Reset the modal's data
98-
if (!(modal._isBeingDestroyed || modal._isDestroyed)) {
93+
if (!modal._isBeingDestroyed && !modal._isDestroyed) {
9994
this.resetModal(modal)
10095
}
10196
}
10297
},
10398
getBaseZIndex() {
104-
if (isNull(this.baseZIndex) && IS_BROWSER) {
99+
if (IS_BROWSER && isNull(this.baseZIndex)) {
105100
// Create a temporary `div.modal-backdrop` to get computed z-index
106101
const div = document.createElement('div')
107102
addClass(div, 'modal-backdrop')
@@ -114,7 +109,7 @@ const ModalManager = /*#__PURE__*/ Vue.extend({
114109
return this.baseZIndex || DEFAULT_ZINDEX
115110
},
116111
getScrollbarWidth() {
117-
if (isNull(this.scrollbarWidth) && IS_BROWSER) {
112+
if (IS_BROWSER && isNull(this.scrollbarWidth)) {
118113
// Create a temporary `div.measure-scrollbar` to get computed z-index
119114
const div = document.createElement('div')
120115
addClass(div, 'modal-scrollbar-measure')

src/components/modal/modal.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({
378378
},
379379
beforeDestroy() {
380380
// Ensure everything is back to normal
381+
modalManager.unregisterModal(this)
381382
this.setObserver(false)
382383
if (this.isVisible) {
383384
this.isVisible = false

src/components/toast/toaster.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { PortalTarget, Wormhole } from 'portal-vue'
22
import { Vue } from '../../vue'
33
import { NAME_TOASTER } from '../../constants/components'
4-
import { EVENT_NAME_DESTROYED, HOOK_EVENT_NAME_BEFORE_DESTROY } from '../../constants/events'
4+
import { EVENT_NAME_DESTROYED } from '../../constants/events'
55
import { PROP_TYPE_STRING } from '../../constants/props'
66
import { removeClass, requestAF } from '../../utils/dom'
77
import { getRootEventName } from '../../utils/events'
@@ -85,11 +85,13 @@ export const BToaster = /*#__PURE__*/ Vue.extend({
8585
this.dead = true
8686
} else {
8787
this.doRender = true
88-
this.$once(HOOK_EVENT_NAME_BEFORE_DESTROY, () => {
89-
// Let toasts made with `this.$bvToast.toast()` know that this toaster
90-
// is being destroyed and should should also destroy/hide themselves
91-
this.emitOnRoot(getRootEventName(NAME_TOASTER, EVENT_NAME_DESTROYED), name)
92-
})
88+
}
89+
},
90+
beforeDestroy() {
91+
// Let toasts made with `this.$bvToast.toast()` know that this toaster
92+
// is being destroyed and should should also destroy/hide themselves
93+
if (this.doRender) {
94+
this.emitOnRoot(getRootEventName(NAME_TOASTER, EVENT_NAME_DESTROYED), this.name)
9395
}
9496
},
9597
destroyed() {

src/mixins/form-text.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import {
33
EVENT_NAME_BLUR,
44
EVENT_NAME_CHANGE,
55
EVENT_NAME_INPUT,
6-
EVENT_NAME_UPDATE,
7-
HOOK_EVENT_NAME_BEFORE_DESTROY
6+
EVENT_NAME_UPDATE
87
} from '../constants/events'
98
import {
109
PROP_TYPE_BOOLEAN,
@@ -117,10 +116,6 @@ export const formTextMixin = Vue.extend({
117116
// Create private non-reactive props
118117
this.$_inputDebounceTimer = null
119118
},
120-
mounted() {
121-
// Set up destroy handler
122-
this.$on(HOOK_EVENT_NAME_BEFORE_DESTROY, this.clearDebounce)
123-
},
124119
beforeDestroy() {
125120
this.clearDebounce()
126121
},

src/mixins/listen-on-document.js

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,64 @@
11
import { Vue } from '../vue'
22
import { IS_BROWSER } from '../constants/env'
3-
import { EVENT_OPTIONS_NO_CAPTURE, HOOK_EVENT_NAME_BEFORE_DESTROY } from '../constants/events'
3+
import { EVENT_OPTIONS_NO_CAPTURE } from '../constants/events'
44
import { arrayIncludes } from '../utils/array'
55
import { eventOn, eventOff } from '../utils/events'
6-
import { isString, isFunction } from '../utils/inspect'
76
import { keys } from '../utils/object'
87

98
// --- Constants ---
109

11-
const PROP = '$_bv_documentHandlers_'
10+
const PROP = '$_documentListeners'
1211

1312
// --- Mixin ---
1413

1514
// @vue/component
1615
export const listenOnDocumentMixin = Vue.extend({
1716
created() {
18-
/* istanbul ignore next */
19-
if (!IS_BROWSER) {
20-
return
21-
}
22-
// Declare non-reactive property
17+
// Define non-reactive property
2318
// Object of arrays, keyed by event name,
24-
// where value is an array of handlers
25-
// Prop will be defined on client only
19+
// where value is an array of callbacks
2620
this[PROP] = {}
27-
// Set up our beforeDestroy handler (client only)
28-
this.$once(HOOK_EVENT_NAME_BEFORE_DESTROY, () => {
29-
const items = this[PROP] || {}
30-
// Immediately delete this[PROP] to prevent the
31-
// listenOn/Off methods from running (which may occur
32-
// due to requestAnimationFrame/transition delays)
33-
delete this[PROP]
34-
// Remove all registered event handlers
35-
keys(items).forEach(eventName => {
36-
const handlers = items[eventName] || []
37-
handlers.forEach(handler =>
38-
eventOff(document, eventName, handler, EVENT_OPTIONS_NO_CAPTURE)
39-
)
21+
},
22+
beforeDestroy() {
23+
// Unregister all registered listeners
24+
keys(this[PROP] || {}).forEach(event => {
25+
this[PROP][event].forEach(callback => {
26+
this.listenOffDocument(event, callback)
4027
})
4128
})
29+
30+
this[PROP] = null
4231
},
4332
methods: {
44-
listenDocument(on, eventName, handler) {
45-
on ? this.listenOnDocument(eventName, handler) : this.listenOffDocument(eventName, handler)
46-
},
47-
listenOnDocument(eventName, handler) {
48-
if (this[PROP] && isString(eventName) && isFunction(handler)) {
49-
this[PROP][eventName] = this[PROP][eventName] || []
50-
if (!arrayIncludes(this[PROP][eventName], handler)) {
51-
this[PROP][eventName].push(handler)
52-
eventOn(document, eventName, handler, EVENT_OPTIONS_NO_CAPTURE)
33+
registerDocumentListener(event, callback) {
34+
if (this[PROP]) {
35+
this[PROP][event] = this[PROP][event] || []
36+
if (!arrayIncludes(this[PROP][event], callback)) {
37+
this[PROP][event].push(callback)
5338
}
5439
}
5540
},
56-
listenOffDocument(eventName, handler) {
57-
if (this[PROP] && isString(eventName) && isFunction(handler)) {
58-
eventOff(document, eventName, handler, EVENT_OPTIONS_NO_CAPTURE)
59-
this[PROP][eventName] = (this[PROP][eventName] || []).filter(h => h !== handler)
41+
unregisterDocumentListener(event, callback) {
42+
if (this[PROP] && this[PROP][event]) {
43+
this[PROP][event] = this[PROP][event].filter(cb => cb !== callback)
6044
}
45+
},
46+
47+
listenDocument(on, event, callback) {
48+
on ? this.listenOnDocument(event, callback) : this.listenOffDocument(event, callback)
49+
},
50+
listenOnDocument(event, callback) {
51+
if (IS_BROWSER) {
52+
eventOn(document, event, callback, EVENT_OPTIONS_NO_CAPTURE)
53+
this.registerDocumentListener(event, callback)
54+
}
55+
},
56+
listenOffDocument(event, callback) {
57+
if (IS_BROWSER) {
58+
eventOff(document, event, callback, EVENT_OPTIONS_NO_CAPTURE)
59+
}
60+
61+
this.unregisterDocumentListener(event, callback)
6162
}
6263
}
6364
})

src/mixins/listen-on-root.js

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,116 @@
11
import { Vue } from '../vue'
2-
import { HOOK_EVENT_NAME_BEFORE_DESTROY } from '../constants/events'
2+
import { arrayIncludes } from '../utils/array'
3+
import { keys } from '../utils/object'
4+
5+
// --- Constants ---
6+
7+
const PROP = '$_rootListeners'
8+
9+
// --- Mixin ---
310

411
// @vue/component
512
export const listenOnRootMixin = Vue.extend({
13+
created() {
14+
// Define non-reactive property
15+
// Object of arrays, keyed by event name,
16+
// where value is an array of callbacks
17+
this[PROP] = {}
18+
},
19+
beforeDestroy() {
20+
// Unregister all registered listeners
21+
keys(this[PROP] || {}).forEach(event => {
22+
this[PROP][event].forEach(callback => {
23+
this.listenOffRoot(event, callback)
24+
})
25+
})
26+
27+
this[PROP] = null
28+
},
629
methods: {
30+
registerRootListener(event, callback) {
31+
if (this[PROP]) {
32+
this[PROP][event] = this[PROP][event] || []
33+
if (!arrayIncludes(this[PROP][event], callback)) {
34+
this[PROP][event].push(callback)
35+
}
36+
}
37+
},
38+
unregisterRootListener(event, callback) {
39+
if (this[PROP] && this[PROP][event]) {
40+
this[PROP][event] = this[PROP][event].filter(cb => cb !== callback)
41+
}
42+
},
43+
744
/**
845
* Safely register event listeners on the root Vue node
946
* While Vue automatically removes listeners for individual components,
10-
* when a component registers a listener on root and is destroyed,
11-
* this orphans a callback because the node is gone,
12-
* but the root does not clear the callback
47+
* when a component registers a listener on `$root` and is destroyed,
48+
* this orphans a callback because the node is gone, but the `$root`
49+
* does not clear the callback
1350
*
14-
* When registering a `$root` listener, it also registers a listener on
15-
* the component's `beforeDestroy()` hook to automatically remove the
16-
* event listener from the `$root` instance
51+
* When registering a `$root` listener, it also registers the listener
52+
* to be removed in the component's `beforeDestroy()` hook
1753
*
1854
* @param {string} event
1955
* @param {function} callback
2056
*/
2157
listenOnRoot(event, callback) {
22-
this.$root.$on(event, callback)
23-
this.$on(HOOK_EVENT_NAME_BEFORE_DESTROY, () => {
24-
this.$root.$off(event, callback)
25-
})
58+
if (this.$root) {
59+
this.$root.$on(event, callback)
60+
this.registerRootListener(event, callback)
61+
}
2662
},
2763

2864
/**
2965
* Safely register a `$once()` event listener on the root Vue node
3066
* While Vue automatically removes listeners for individual components,
31-
* when a component registers a listener on root and is destroyed,
32-
* this orphans a callback because the node is gone,
33-
* but the root does not clear the callback
67+
* when a component registers a listener on `$root` and is destroyed,
68+
* this orphans a callback because the node is gone, but the `$root`
69+
* does not clear the callback
3470
*
35-
* When registering a $root listener, it also registers a listener on
36-
* the component's `beforeDestroy` hook to automatically remove the
37-
* event listener from the $root instance.
71+
* When registering a `$root` listener, it also registers the listener
72+
* to be removed in the component's `beforeDestroy()` hook
3873
*
3974
* @param {string} event
4075
* @param {function} callback
4176
*/
4277
listenOnRootOnce(event, callback) {
43-
this.$root.$once(event, callback)
44-
this.$on(HOOK_EVENT_NAME_BEFORE_DESTROY, () => {
78+
if (this.$root) {
79+
const _callback = (...args) => {
80+
this.unregisterRootListener(_callback)
81+
// eslint-disable-next-line node/no-callback-literal
82+
callback(...args)
83+
}
84+
85+
this.$root.$once(event, _callback)
86+
this.registerRootListener(event, _callback)
87+
}
88+
},
89+
90+
/**
91+
* Safely unregister event listeners from the root Vue node
92+
*
93+
* @param {string} event
94+
* @param {function} callback
95+
*/
96+
listenOffRoot(event, callback) {
97+
this.unregisterRootListener(event, callback)
98+
99+
if (this.$root) {
45100
this.$root.$off(event, callback)
46-
})
101+
}
47102
},
48103

49104
/**
50-
* Convenience method for calling `vm.$emit()` on `vm.$root`
105+
* Convenience method for calling `vm.$emit()` on `$root`
51106
*
52107
* @param {string} event
53108
* @param {*} args
54109
*/
55110
emitOnRoot(event, ...args) {
56-
this.$root.$emit(event, ...args)
111+
if (this.$root) {
112+
this.$root.$emit(event, ...args)
113+
}
57114
}
58115
}
59116
})

0 commit comments

Comments
 (0)