Skip to content

Commit 8056105

Browse files
authored
fix: prevent memory leaks by removing app references (#2706)
Fix #2639
1 parent 627027f commit 8056105

File tree

2 files changed

+108
-1
lines changed

2 files changed

+108
-1
lines changed

src/index.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,19 @@ export default class VueRouter {
8989

9090
this.apps.push(app)
9191

92-
// main app already initialized.
92+
// set up app destroyed handler
93+
// https://github.com/vuejs/vue-router/issues/2639
94+
app.$once('hook:destroyed', () => {
95+
// clean out app from this.apps array once destroyed
96+
const index = this.apps.indexOf(app)
97+
if (index > -1) this.apps.splice(index, 1)
98+
// ensure we still have a main app or null if no apps
99+
// we do not release the router so it can be reused
100+
if (this.app === app) this.app = this.apps[0] || null
101+
})
102+
103+
// main app previously initialized
104+
// return as we don't need to set up new history listener
93105
if (this.app) {
94106
return
95107
}

test/unit/specs/api.spec.js

+95
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Router from '../../../src/index'
2+
import Vue from 'vue'
23

34
describe('router.onReady', () => {
45
it('should work', done => {
@@ -185,3 +186,97 @@ describe('router.push/replace callbacks', () => {
185186
})
186187
})
187188
})
189+
190+
describe('router app destroy handling', () => {
191+
Vue.use(Router)
192+
193+
let router, app1, app2, app3
194+
195+
beforeEach(() => {
196+
router = new Router({
197+
mode: 'abstract',
198+
routes: [
199+
{ path: '/', component: { name: 'A' }}
200+
]
201+
})
202+
203+
// Add main app
204+
app1 = new Vue({
205+
router,
206+
render (h) { return h('div') }
207+
})
208+
209+
// Add 2nd app
210+
app2 = new Vue({
211+
router,
212+
render (h) { return h('div') }
213+
})
214+
215+
// Add 3rd app
216+
app3 = new Vue({
217+
router,
218+
render (h) { return h('div') }
219+
})
220+
})
221+
222+
it('all apps point to the same router instance', () => {
223+
expect(app1.$router).toBe(app2.$router)
224+
expect(app2.$router).toBe(app3.$router)
225+
})
226+
227+
it('should have all 3 registered apps', () => {
228+
expect(app1.$router.app).toBe(app1)
229+
expect(app1.$router.apps.length).toBe(3)
230+
expect(app1.$router.apps[0]).toBe(app1)
231+
expect(app1.$router.apps[1]).toBe(app2)
232+
expect(app1.$router.apps[2]).toBe(app3)
233+
})
234+
235+
it('should remove 2nd destroyed app from this.apps', () => {
236+
app2.$destroy()
237+
expect(app1.$router.app).toBe(app1)
238+
expect(app1.$router.apps.length).toBe(2)
239+
expect(app1.$router.apps[0]).toBe(app1)
240+
expect(app1.$router.apps[1]).toBe(app3)
241+
})
242+
243+
it('should remove 1st destroyed app and replace current app', () => {
244+
app1.$destroy()
245+
expect(app3.$router.app).toBe(app2)
246+
expect(app3.$router.apps.length).toBe(2)
247+
expect(app3.$router.apps[0]).toBe(app2)
248+
expect(app1.$router.apps[1]).toBe(app3)
249+
})
250+
251+
it('should remove all apps', () => {
252+
app1.$destroy()
253+
app3.$destroy()
254+
app2.$destroy()
255+
expect(app3.$router.app).toBe(null)
256+
expect(app3.$router.apps.length).toBe(0)
257+
})
258+
259+
it('should keep current app if already defined', () => {
260+
const app4 = new Vue({
261+
router,
262+
render (h) { return h('div') }
263+
})
264+
expect(app4.$router.app).toBe(app1)
265+
expect(app4.$router.apps.length).toBe(4)
266+
expect(app4.$router.apps[3]).toBe(app4)
267+
})
268+
269+
it('should replace current app if none is assigned when creating the app', () => {
270+
app1.$destroy()
271+
app3.$destroy()
272+
app2.$destroy()
273+
const app4 = new Vue({
274+
router,
275+
render (h) { return h('div') }
276+
})
277+
expect(router.app).toBe(app4)
278+
expect(app4.$router).toBe(router)
279+
expect(app4.$router.apps.length).toBe(1)
280+
expect(app4.$router.apps[0]).toBe(app4)
281+
})
282+
})

0 commit comments

Comments
 (0)