Skip to content

Commit 4c81be8

Browse files
authored
feat(history): Remove event listeners when all apps are destroyed. (#3172)
* feat(history): Remove event listeners when all apps are destroyed. * fix(tests): Adding test assertions * fix(feedback): addressing @posva's feedback * fix(index): posva's feedback * feat(tests): adding multi-app test * fix(feedback): posva's feedback * fix(feedback): unmounting apps with buttons outside of the app Close #3152 Close #2341
1 parent db39ae1 commit 4c81be8

File tree

15 files changed

+305
-38
lines changed

15 files changed

+305
-38
lines changed

examples/basic/app.js

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
import Vue from 'vue'
22
import VueRouter from 'vue-router'
33

4+
// track number of popstate listeners
5+
let numPopstateListeners = 0
6+
const listenerCountDiv = document.createElement('div')
7+
listenerCountDiv.id = 'popstate-count'
8+
listenerCountDiv.textContent = numPopstateListeners + ' popstate listeners'
9+
document.body.appendChild(listenerCountDiv)
10+
11+
const originalAddEventListener = window.addEventListener
12+
const originalRemoveEventListener = window.removeEventListener
13+
window.addEventListener = function (name, handler) {
14+
if (name === 'popstate') {
15+
listenerCountDiv.textContent =
16+
++numPopstateListeners + ' popstate listeners'
17+
}
18+
return originalAddEventListener.apply(this, arguments)
19+
}
20+
window.removeEventListener = function (name, handler) {
21+
if (name === 'popstate') {
22+
listenerCountDiv.textContent =
23+
--numPopstateListeners + ' popstate listeners'
24+
}
25+
return originalRemoveEventListener.apply(this, arguments)
26+
}
27+
428
// 1. Use plugin.
529
// This installs <router-view> and <router-link>,
630
// and injects $router and $route to all router-enabled child components
@@ -27,7 +51,7 @@ const router = new VueRouter({
2751
// 4. Create and mount root instance.
2852
// Make sure to inject the router.
2953
// Route components will be rendered inside <router-view>.
30-
new Vue({
54+
const vueInstance = new Vue({
3155
router,
3256
data: () => ({ n: 0 }),
3357
template: `
@@ -69,3 +93,8 @@ new Vue({
6993
}
7094
}
7195
}).$mount('#app')
96+
97+
document.getElementById('unmount').addEventListener('click', () => {
98+
vueInstance.$destroy()
99+
vueInstance.$el.innerHTML = ''
100+
})

examples/basic/index.html

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<link rel="stylesheet" href="/global.css">
33
<a href="/">&larr; Examples index</a>
4+
<button id="unmount">Unmount</button>
5+
<hr />
46
<div id="app"></div>
57
<script src="/__build__/shared.chunk.js"></script>
68
<script src="/__build__/basic.js"></script>

examples/hash-mode/app.js

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
import Vue from 'vue'
22
import VueRouter from 'vue-router'
33

4+
// track number of popstate listeners
5+
let numPopstateListeners = 0
6+
const listenerCountDiv = document.createElement('div')
7+
listenerCountDiv.id = 'popstate-count'
8+
listenerCountDiv.textContent = numPopstateListeners + ' popstate listeners'
9+
document.body.appendChild(listenerCountDiv)
10+
11+
const originalAddEventListener = window.addEventListener
12+
const originalRemoveEventListener = window.removeEventListener
13+
window.addEventListener = function (name, handler) {
14+
if (name === 'popstate') {
15+
listenerCountDiv.textContent =
16+
++numPopstateListeners + ' popstate listeners'
17+
}
18+
return originalAddEventListener.apply(this, arguments)
19+
}
20+
window.removeEventListener = function (name, handler) {
21+
if (name === 'popstate') {
22+
listenerCountDiv.textContent =
23+
--numPopstateListeners + ' popstate listeners'
24+
}
25+
return originalRemoveEventListener.apply(this, arguments)
26+
}
27+
428
// 1. Use plugin.
529
// This installs <router-view> and <router-link>,
630
// and injects $router and $route to all router-enabled child components
@@ -28,7 +52,7 @@ const router = new VueRouter({
2852
// 4. Create and mount root instance.
2953
// Make sure to inject the router.
3054
// Route components will be rendered inside <router-view>.
31-
new Vue({
55+
const vueInstance = new Vue({
3256
router,
3357
template: `
3458
<div id="app">
@@ -47,5 +71,12 @@ new Vue({
4771
<pre id="hash">{{ $route.hash }}</pre>
4872
<router-view class="view"></router-view>
4973
</div>
50-
`
74+
`,
75+
methods: {
76+
}
5177
}).$mount('#app')
78+
79+
document.getElementById('unmount').addEventListener('click', () => {
80+
vueInstance.$destroy()
81+
vueInstance.$el.innerHTML = ''
82+
})

examples/hash-mode/index.html

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE html>
22
<link rel="stylesheet" href="/global.css">
33
<a href="/">&larr; Examples index</a>
4+
<button id="unmount">Unmount</button>
5+
<hr />
46
<div id="app"></div>
57
<script src="/__build__/shared.chunk.js"></script>
68
<script src="/__build__/hash-mode.js"></script>

examples/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ <h1>Vue Router Examples</h1>
2727
<li><a href="discrete-components">Discrete Components</a></li>
2828
<li><a href="nested-router">Nested Routers</a></li>
2929
<li><a href="keepalive-view">Keepalive View</a></li>
30+
<li><a href="multi-app">Multiple Apps</a></li>
3031
</ul>
3132
</body>
3233
</html>

examples/multi-app/app.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Vue from 'vue'
2+
import VueRouter from 'vue-router'
3+
4+
// track number of popstate listeners
5+
let numPopstateListeners = 0
6+
const listenerCountDiv = document.getElementById('popcount')
7+
listenerCountDiv.textContent = 0
8+
9+
const originalAddEventListener = window.addEventListener
10+
const originalRemoveEventListener = window.removeEventListener
11+
window.addEventListener = function (name, handler) {
12+
if (name === 'popstate') {
13+
listenerCountDiv.textContent =
14+
++numPopstateListeners
15+
}
16+
return originalAddEventListener.apply(this, arguments)
17+
}
18+
window.removeEventListener = function (name, handler) {
19+
if (name === 'popstate') {
20+
listenerCountDiv.textContent =
21+
--numPopstateListeners
22+
}
23+
return originalRemoveEventListener.apply(this, arguments)
24+
}
25+
26+
// 1. Use plugin.
27+
// This installs <router-view> and <router-link>,
28+
// and injects $router and $route to all router-enabled child components
29+
Vue.use(VueRouter)
30+
31+
const looper = [1, 2, 3]
32+
33+
looper.forEach((n) => {
34+
let vueInstance
35+
const mountEl = document.getElementById('mount' + n)
36+
const unmountEl = document.getElementById('unmount' + n)
37+
38+
mountEl.addEventListener('click', () => {
39+
// 2. Define route components
40+
const Home = { template: '<div>home</div>' }
41+
const Foo = { template: '<div>foo</div>' }
42+
43+
// 3. Create the router
44+
const router = new VueRouter({
45+
mode: 'history',
46+
base: __dirname,
47+
routes: [
48+
{ path: '/', component: Home },
49+
{ path: '/foo', component: Foo }
50+
]
51+
})
52+
53+
// 4. Create and mount root instance.
54+
// Make sure to inject the router.
55+
// Route components will be rendered inside <router-view>.
56+
vueInstance = new Vue({
57+
router,
58+
template: `
59+
<div id="app-${n}">
60+
<h1>Basic</h1>
61+
<ul>
62+
<li><router-link to="/">/</router-link></li>
63+
<li><router-link to="/foo">/foo</router-link></li>
64+
</ul>
65+
<router-view class="view"></router-view>
66+
</div>
67+
`
68+
}).$mount('#app-' + n)
69+
})
70+
71+
unmountEl.addEventListener('click', () => {
72+
vueInstance.$destroy()
73+
vueInstance.$el.innerHTML = ''
74+
})
75+
})

examples/multi-app/index.html

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<link rel="stylesheet" href="/global.css">
3+
<a href="/">&larr; Examples index</a>
4+
5+
<button id="mount1">Mount App 1</button>
6+
<button id="mount2">Mount App 2</button>
7+
<button id="mount3">Mount App 3</button>
8+
9+
<hr />
10+
11+
<button id="unmount1">Unmount App 1</button>
12+
<button id="unmount2">Unmount App 2</button>
13+
<button id="unmount3">Unmount App 3</button>
14+
15+
<hr />
16+
17+
popstate count: <span id="popcount"></span>
18+
19+
<div id="app-1"></div>
20+
<div id="app-2"></div>
21+
<div id="app-3"></div>
22+
23+
<script src="/__build__/shared.chunk.js"></script>
24+
<script src="/__build__/multi-app.js"></script>

src/history/base.js

+15
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@ export class History {
2323
readyCbs: Array<Function>
2424
readyErrorCbs: Array<Function>
2525
errorCbs: Array<Function>
26+
listeners: Array<Function>
27+
cleanupListeners: Function
2628

2729
// implemented by sub-classes
2830
+go: (n: number) => void
2931
+push: (loc: RawLocation) => void
3032
+replace: (loc: RawLocation) => void
3133
+ensureURL: (push?: boolean) => void
3234
+getCurrentLocation: () => string
35+
+setupListeners: Function
3336

3437
constructor (router: Router, base: ?string) {
3538
this.router = router
@@ -41,6 +44,7 @@ export class History {
4144
this.readyCbs = []
4245
this.readyErrorCbs = []
4346
this.errorCbs = []
47+
this.listeners = []
4448
}
4549

4650
listen (cb: Function) {
@@ -208,6 +212,17 @@ export class History {
208212
hook && hook(route, prev)
209213
})
210214
}
215+
216+
setupListeners () {
217+
// Default implementation is empty
218+
}
219+
220+
teardownListeners () {
221+
this.listeners.forEach(cleanupListener => {
222+
cleanupListener()
223+
})
224+
this.listeners = []
225+
}
211226
}
212227

213228
function normalizeBase (base: ?string): string {

src/history/hash.js

+25-16
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,40 @@ export class HashHistory extends History {
2020
// this is delayed until the app mounts
2121
// to avoid the hashchange listener being fired too early
2222
setupListeners () {
23+
if (this.listeners.length > 0) {
24+
return
25+
}
26+
2327
const router = this.router
2428
const expectScroll = router.options.scrollBehavior
2529
const supportsScroll = supportsPushState && expectScroll
2630

2731
if (supportsScroll) {
28-
setupScroll()
32+
this.listeners.push(setupScroll())
2933
}
3034

31-
window.addEventListener(
32-
supportsPushState ? 'popstate' : 'hashchange',
33-
() => {
34-
const current = this.current
35-
if (!ensureSlash()) {
36-
return
37-
}
38-
this.transitionTo(getHash(), route => {
39-
if (supportsScroll) {
40-
handleScroll(this.router, route, current, true)
41-
}
42-
if (!supportsPushState) {
43-
replaceHash(route.fullPath)
44-
}
45-
})
35+
const handleRoutingEvent = () => {
36+
const current = this.current
37+
if (!ensureSlash()) {
38+
return
4639
}
40+
this.transitionTo(getHash(), route => {
41+
if (supportsScroll) {
42+
handleScroll(this.router, route, current, true)
43+
}
44+
if (!supportsPushState) {
45+
replaceHash(route.fullPath)
46+
}
47+
})
48+
}
49+
const eventType = supportsPushState ? 'popstate' : 'hashchange'
50+
window.addEventListener(
51+
eventType,
52+
handleRoutingEvent
4753
)
54+
this.listeners.push(() => {
55+
window.removeEventListener(eventType, handleRoutingEvent)
56+
})
4857
}
4958

5059
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {

src/history/html5.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,34 @@ import { setupScroll, handleScroll } from '../util/scroll'
88
import { pushState, replaceState, supportsPushState } from '../util/push-state'
99

1010
export class HTML5History extends History {
11+
_startLocation: string
12+
1113
constructor (router: Router, base: ?string) {
1214
super(router, base)
1315

16+
this._startLocation = getLocation(this.base)
17+
}
18+
19+
setupListeners () {
20+
if (this.listeners.length > 0) {
21+
return
22+
}
23+
24+
const router = this.router
1425
const expectScroll = router.options.scrollBehavior
1526
const supportsScroll = supportsPushState && expectScroll
1627

1728
if (supportsScroll) {
18-
setupScroll()
29+
this.listeners.push(setupScroll())
1930
}
2031

21-
const initLocation = getLocation(this.base)
22-
window.addEventListener('popstate', e => {
32+
const handleRoutingEvent = () => {
2333
const current = this.current
2434

2535
// Avoiding first `popstate` event dispatched in some browsers but first
2636
// history route not updated since async guard at the same time.
2737
const location = getLocation(this.base)
28-
if (this.current === START && location === initLocation) {
38+
if (this.current === START && location === this._startLocation) {
2939
return
3040
}
3141

@@ -34,6 +44,10 @@ export class HTML5History extends History {
3444
handleScroll(router, route, current, true)
3545
}
3646
})
47+
}
48+
window.addEventListener('popstate', handleRoutingEvent)
49+
this.listeners.push(() => {
50+
window.removeEventListener('popstate', handleRoutingEvent)
3751
})
3852
}
3953

0 commit comments

Comments
 (0)