Skip to content

Commit

Permalink
feat(history): Remove event listeners when all apps are destroyed. (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
joeldenning authored May 19, 2020
1 parent db39ae1 commit 4c81be8
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 38 deletions.
31 changes: 30 additions & 1 deletion examples/basic/app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import Vue from 'vue'
import VueRouter from 'vue-router'

// track number of popstate listeners
let numPopstateListeners = 0
const listenerCountDiv = document.createElement('div')
listenerCountDiv.id = 'popstate-count'
listenerCountDiv.textContent = numPopstateListeners + ' popstate listeners'
document.body.appendChild(listenerCountDiv)

const originalAddEventListener = window.addEventListener
const originalRemoveEventListener = window.removeEventListener
window.addEventListener = function (name, handler) {
if (name === 'popstate') {
listenerCountDiv.textContent =
++numPopstateListeners + ' popstate listeners'
}
return originalAddEventListener.apply(this, arguments)
}
window.removeEventListener = function (name, handler) {
if (name === 'popstate') {
listenerCountDiv.textContent =
--numPopstateListeners + ' popstate listeners'
}
return originalRemoveEventListener.apply(this, arguments)
}

// 1. Use plugin.
// This installs <router-view> and <router-link>,
// and injects $router and $route to all router-enabled child components
Expand All @@ -27,7 +51,7 @@ const router = new VueRouter({
// 4. Create and mount root instance.
// Make sure to inject the router.
// Route components will be rendered inside <router-view>.
new Vue({
const vueInstance = new Vue({
router,
data: () => ({ n: 0 }),
template: `
Expand Down Expand Up @@ -69,3 +93,8 @@ new Vue({
}
}
}).$mount('#app')

document.getElementById('unmount').addEventListener('click', () => {
vueInstance.$destroy()
vueInstance.$el.innerHTML = ''
})
2 changes: 2 additions & 0 deletions examples/basic/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/global.css">
<a href="/">&larr; Examples index</a>
<button id="unmount">Unmount</button>
<hr />
<div id="app"></div>
<script src="/__build__/shared.chunk.js"></script>
<script src="/__build__/basic.js"></script>
35 changes: 33 additions & 2 deletions examples/hash-mode/app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import Vue from 'vue'
import VueRouter from 'vue-router'

// track number of popstate listeners
let numPopstateListeners = 0
const listenerCountDiv = document.createElement('div')
listenerCountDiv.id = 'popstate-count'
listenerCountDiv.textContent = numPopstateListeners + ' popstate listeners'
document.body.appendChild(listenerCountDiv)

const originalAddEventListener = window.addEventListener
const originalRemoveEventListener = window.removeEventListener
window.addEventListener = function (name, handler) {
if (name === 'popstate') {
listenerCountDiv.textContent =
++numPopstateListeners + ' popstate listeners'
}
return originalAddEventListener.apply(this, arguments)
}
window.removeEventListener = function (name, handler) {
if (name === 'popstate') {
listenerCountDiv.textContent =
--numPopstateListeners + ' popstate listeners'
}
return originalRemoveEventListener.apply(this, arguments)
}

// 1. Use plugin.
// This installs <router-view> and <router-link>,
// and injects $router and $route to all router-enabled child components
Expand Down Expand Up @@ -28,7 +52,7 @@ const router = new VueRouter({
// 4. Create and mount root instance.
// Make sure to inject the router.
// Route components will be rendered inside <router-view>.
new Vue({
const vueInstance = new Vue({
router,
template: `
<div id="app">
Expand All @@ -47,5 +71,12 @@ new Vue({
<pre id="hash">{{ $route.hash }}</pre>
<router-view class="view"></router-view>
</div>
`
`,
methods: {
}
}).$mount('#app')

document.getElementById('unmount').addEventListener('click', () => {
vueInstance.$destroy()
vueInstance.$el.innerHTML = ''
})
2 changes: 2 additions & 0 deletions examples/hash-mode/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/global.css">
<a href="/">&larr; Examples index</a>
<button id="unmount">Unmount</button>
<hr />
<div id="app"></div>
<script src="/__build__/shared.chunk.js"></script>
<script src="/__build__/hash-mode.js"></script>
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ <h1>Vue Router Examples</h1>
<li><a href="discrete-components">Discrete Components</a></li>
<li><a href="nested-router">Nested Routers</a></li>
<li><a href="keepalive-view">Keepalive View</a></li>
<li><a href="multi-app">Multiple Apps</a></li>
</ul>
</body>
</html>
75 changes: 75 additions & 0 deletions examples/multi-app/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Vue from 'vue'
import VueRouter from 'vue-router'

// track number of popstate listeners
let numPopstateListeners = 0
const listenerCountDiv = document.getElementById('popcount')
listenerCountDiv.textContent = 0

const originalAddEventListener = window.addEventListener
const originalRemoveEventListener = window.removeEventListener
window.addEventListener = function (name, handler) {
if (name === 'popstate') {
listenerCountDiv.textContent =
++numPopstateListeners
}
return originalAddEventListener.apply(this, arguments)
}
window.removeEventListener = function (name, handler) {
if (name === 'popstate') {
listenerCountDiv.textContent =
--numPopstateListeners
}
return originalRemoveEventListener.apply(this, arguments)
}

// 1. Use plugin.
// This installs <router-view> and <router-link>,
// and injects $router and $route to all router-enabled child components
Vue.use(VueRouter)

const looper = [1, 2, 3]

looper.forEach((n) => {
let vueInstance
const mountEl = document.getElementById('mount' + n)
const unmountEl = document.getElementById('unmount' + n)

mountEl.addEventListener('click', () => {
// 2. Define route components
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }

// 3. Create the router
const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo }
]
})

// 4. Create and mount root instance.
// Make sure to inject the router.
// Route components will be rendered inside <router-view>.
vueInstance = new Vue({
router,
template: `
<div id="app-${n}">
<h1>Basic</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app-' + n)
})

unmountEl.addEventListener('click', () => {
vueInstance.$destroy()
vueInstance.$el.innerHTML = ''
})
})
24 changes: 24 additions & 0 deletions examples/multi-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/global.css">
<a href="/">&larr; Examples index</a>

<button id="mount1">Mount App 1</button>
<button id="mount2">Mount App 2</button>
<button id="mount3">Mount App 3</button>

<hr />

<button id="unmount1">Unmount App 1</button>
<button id="unmount2">Unmount App 2</button>
<button id="unmount3">Unmount App 3</button>

<hr />

popstate count: <span id="popcount"></span>

<div id="app-1"></div>
<div id="app-2"></div>
<div id="app-3"></div>

<script src="/__build__/shared.chunk.js"></script>
<script src="/__build__/multi-app.js"></script>
15 changes: 15 additions & 0 deletions src/history/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ export class History {
readyCbs: Array<Function>
readyErrorCbs: Array<Function>
errorCbs: Array<Function>
listeners: Array<Function>
cleanupListeners: Function

// implemented by sub-classes
+go: (n: number) => void
+push: (loc: RawLocation) => void
+replace: (loc: RawLocation) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string
+setupListeners: Function

constructor (router: Router, base: ?string) {
this.router = router
Expand All @@ -41,6 +44,7 @@ export class History {
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
this.listeners = []
}

listen (cb: Function) {
Expand Down Expand Up @@ -208,6 +212,17 @@ export class History {
hook && hook(route, prev)
})
}

setupListeners () {
// Default implementation is empty
}

teardownListeners () {
this.listeners.forEach(cleanupListener => {
cleanupListener()
})
this.listeners = []
}
}

function normalizeBase (base: ?string): string {
Expand Down
41 changes: 25 additions & 16 deletions src/history/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,40 @@ export class HashHistory extends History {
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
setupListeners () {
if (this.listeners.length > 0) {
return
}

const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll

if (supportsScroll) {
setupScroll()
this.listeners.push(setupScroll())
}

window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
() => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
Expand Down
22 changes: 18 additions & 4 deletions src/history/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,34 @@ import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'

export class HTML5History extends History {
_startLocation: string

constructor (router: Router, base: ?string) {
super(router, base)

this._startLocation = getLocation(this.base)
}

setupListeners () {
if (this.listeners.length > 0) {
return
}

const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll

if (supportsScroll) {
setupScroll()
this.listeners.push(setupScroll())
}

const initLocation = getLocation(this.base)
window.addEventListener('popstate', e => {
const handleRoutingEvent = () => {
const current = this.current

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

Expand All @@ -34,6 +44,10 @@ export class HTML5History extends History {
handleScroll(router, route, current, true)
}
})
}
window.addEventListener('popstate', handleRoutingEvent)
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}

Expand Down
Loading

0 comments on commit 4c81be8

Please sign in to comment.