Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(history): Remove event listeners when all apps are destroyed. #3172

Merged
merged 7 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
joeldenning marked this conversation as resolved.
Show resolved Hide resolved
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 () {
posva marked this conversation as resolved.
Show resolved Hide resolved
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)
posva marked this conversation as resolved.
Show resolved Hide resolved
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