|
1 | 1 | 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 --- |
3 | 10 |
|
4 | 11 | // @vue/component |
5 | 12 | 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 | + }, |
6 | 29 | 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 | + |
7 | 44 | /** |
8 | 45 | * Safely register event listeners on the root Vue node |
9 | 46 | * 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 |
13 | 50 | * |
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 |
17 | 53 | * |
18 | 54 | * @param {string} event |
19 | 55 | * @param {function} callback |
20 | 56 | */ |
21 | 57 | 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 | + } |
26 | 62 | }, |
27 | 63 |
|
28 | 64 | /** |
29 | 65 | * Safely register a `$once()` event listener on the root Vue node |
30 | 66 | * 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 |
34 | 70 | * |
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 |
38 | 73 | * |
39 | 74 | * @param {string} event |
40 | 75 | * @param {function} callback |
41 | 76 | */ |
42 | 77 | 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) { |
45 | 100 | this.$root.$off(event, callback) |
46 | | - }) |
| 101 | + } |
47 | 102 | }, |
48 | 103 |
|
49 | 104 | /** |
50 | | - * Convenience method for calling `vm.$emit()` on `vm.$root` |
| 105 | + * Convenience method for calling `vm.$emit()` on `$root` |
51 | 106 | * |
52 | 107 | * @param {string} event |
53 | 108 | * @param {*} args |
54 | 109 | */ |
55 | 110 | emitOnRoot(event, ...args) { |
56 | | - this.$root.$emit(event, ...args) |
| 111 | + if (this.$root) { |
| 112 | + this.$root.$emit(event, ...args) |
| 113 | + } |
57 | 114 | } |
58 | 115 | } |
59 | 116 | }) |
0 commit comments