From 4e833a6930bd433e23361ebbc4172e2786e9eae5 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 5 Mar 2019 03:37:38 -0400 Subject: [PATCH 01/13] fix(router): remove app from $router.apps array once app destroyed (Fixes #2639) Prevent destroyed apps from causing memory leak in `$router.apps` array. Fixes #2639 --- src/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index a1a7b8507..baf6db972 100644 --- a/src/index.js +++ b/src/index.js @@ -91,7 +91,12 @@ export default class VueRouter { // main app already initialized. if (this.app) { - return + app.$once('hook:destroyed', () { + // Clean out app from this.apps array once destroyed + const index = this.apps.indexOf(app) + if (index > -1) this.apps.splice(index, 1) + }) + return } this.app = app From 02eca65394e72d68a53dfc99f74bc02bc8adde17 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 5 Mar 2019 03:42:12 -0400 Subject: [PATCH 02/13] lint --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index baf6db972..388919009 100644 --- a/src/index.js +++ b/src/index.js @@ -91,7 +91,7 @@ export default class VueRouter { // main app already initialized. if (this.app) { - app.$once('hook:destroyed', () { + app.$once('hook:destroyed', () => { // Clean out app from this.apps array once destroyed const index = this.apps.indexOf(app) if (index > -1) this.apps.splice(index, 1) From 646c294ebeae37c7584fc530457cd942ab0777cd Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 5 Mar 2019 03:44:32 -0400 Subject: [PATCH 03/13] lint --- src/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index 388919009..12449bfc5 100644 --- a/src/index.js +++ b/src/index.js @@ -91,12 +91,12 @@ export default class VueRouter { // main app already initialized. if (this.app) { - app.$once('hook:destroyed', () => { - // Clean out app from this.apps array once destroyed - const index = this.apps.indexOf(app) - if (index > -1) this.apps.splice(index, 1) - }) - return + app.$once('hook:destroyed', () => { + // Clean out app from this.apps array once destroyed + const index = this.apps.indexOf(app) + if (index > -1) this.apps.splice(index, 1) + }) + return } this.app = app From 614713a9b9b689085f2eec9b3e84b3d3da0f0d00 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 29 Mar 2019 23:59:55 -0300 Subject: [PATCH 04/13] Update index.js --- src/index.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 12449bfc5..2c698403c 100644 --- a/src/index.js +++ b/src/index.js @@ -89,13 +89,16 @@ export default class VueRouter { this.apps.push(app) + // Clean out app from this.apps array once destroyed + app.$once('hook:destroyed', () => { + const index = this.apps.indexOf(app) + if (index > -1) this.apps.splice(index, 1) + // Ensure we still have a main app + if (this.apps[0]) this.app = this.apps[0] + }) + // main app already initialized. if (this.app) { - app.$once('hook:destroyed', () => { - // Clean out app from this.apps array once destroyed - const index = this.apps.indexOf(app) - if (index > -1) this.apps.splice(index, 1) - }) return } From c0fd03b762d4e9d1dc0bd7fa934ef9874dfd7a44 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 01:08:59 -0300 Subject: [PATCH 05/13] better handling of last app destroyed Prevents history listeners from being set up multiple times --- src/index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 2c698403c..6e9bf33a1 100644 --- a/src/index.js +++ b/src/index.js @@ -89,16 +89,22 @@ export default class VueRouter { this.apps.push(app) - // Clean out app from this.apps array once destroyed + // set up app destroyed handler app.$once('hook:destroyed', () => { + // clean out app from this.apps array once destroyed const index = this.apps.indexOf(app) if (index > -1) this.apps.splice(index, 1) - // Ensure we still have a main app - if (this.apps[0]) this.app = this.apps[0] + // ensure we still have a main app, unless this is the last app left + if (this.apps.length > 0) this.app = this.apps[0] }) - // main app already initialized. + // main app previously initialized if (this.app) { + if (this.app !== this.apps[0]) { + // main app had been destroyed, so replace it with this new app + this.app = this.apps[0] + } + // return as we don't need to set up new history listener return } From 06d0b00750003b432bb8a4efcc1f7c837bc4632d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 02:06:14 -0300 Subject: [PATCH 06/13] add app destroy testing --- test/unit/specs/api.spec.js | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index 57e912cd9..2c62d08ef 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -1,4 +1,5 @@ import Router from '../../../src/index' +import Vue from 'vue' describe('router.onReady', () => { it('should work', done => { @@ -145,3 +146,67 @@ describe('router.push/replace callbacks', () => { }) }) }) + +describe('router app destroy handling', () => { + it('should remove destroyed apps from this.apps', () => { + const router = new Router({ + mode: 'abstract', + routes: [ + { path: '/', component: { name: 'A' }} + ] + }) + + const options = { + router, + render(h) { return h('div') } + } + + expect(router.apps.length).toBe(0) + expect(router.app).toBe(null) + + // Add main app + const app1 = new Vue(options) + expect(router.app).toBe(app1) + expect(router.apps.length).toBe(1) + expect(router.app[0]).toBe(app1) + + // Add 2nd app + const app2 = new Vue(options) + expect(router.app).toBe(app1) + expect(router.apps.length).toBe(2) + expect(router.app[0]).toBe(app1) + expect(router.app[1]).toBe(app2) + + // Add 3rd app + const app3 = new Vue(options) + expect(router.app).toBe(app1) + expect(router.apps.length).toBe(3) + expect(router.app[0]).toBe(app1) + expect(router.app[1]).toBe(app2) + expect(router.app[2]).toBe(app3) + + // Destroy second app + app2.destroy() + expect(router.app).toBe(app1) + expect(router.apps.length).toBe(2) + expect(router.app[0]).toBe(app1) + expect(router.app[1]).toBe(app3) + + // Destroy 1st app + app1.destroy() + expect(router.app).toBe(app3) + expect(router.apps.length).toBe(1) + expect(router.app[0]).toBe(app3) + + // Destroy 3rd app + app3.destroy() + expect(router.app).toBe(app3) + expect(router.apps.length).toBe(0) + + // Add 4th app (should be the only app) + const app4 = new Vue(options) + expect(router.app).toBe(app4) + expect(router.apps.length).toBe(1) + expect(router.app[0]).toBe(app4) + }) +}) From 743e0ff84eb31cd04c46add9a49f2d71271990ec Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 02:13:19 -0300 Subject: [PATCH 07/13] Update api.spec.js --- test/unit/specs/api.spec.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index 2c62d08ef..ae4468215 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -156,29 +156,33 @@ describe('router app destroy handling', () => { ] }) - const options = { - router, - render(h) { return h('div') } - } - expect(router.apps.length).toBe(0) expect(router.app).toBe(null) // Add main app - const app1 = new Vue(options) + const app1 = new Vue({ + router, + render (h) { return h('div') } + }) expect(router.app).toBe(app1) expect(router.apps.length).toBe(1) expect(router.app[0]).toBe(app1) // Add 2nd app - const app2 = new Vue(options) + const app2 = new Vue({ + router, + render (h) { return h('div') } + }) expect(router.app).toBe(app1) expect(router.apps.length).toBe(2) expect(router.app[0]).toBe(app1) expect(router.app[1]).toBe(app2) // Add 3rd app - const app3 = new Vue(options) + const app3 = new Vue({ + router, + render (h) { return h('div') } + }) expect(router.app).toBe(app1) expect(router.apps.length).toBe(3) expect(router.app[0]).toBe(app1) @@ -186,25 +190,28 @@ describe('router app destroy handling', () => { expect(router.app[2]).toBe(app3) // Destroy second app - app2.destroy() + app2.$destroy() expect(router.app).toBe(app1) expect(router.apps.length).toBe(2) expect(router.app[0]).toBe(app1) expect(router.app[1]).toBe(app3) // Destroy 1st app - app1.destroy() + app1.$destroy() expect(router.app).toBe(app3) expect(router.apps.length).toBe(1) expect(router.app[0]).toBe(app3) // Destroy 3rd app - app3.destroy() + app3.$destroy() expect(router.app).toBe(app3) expect(router.apps.length).toBe(0) // Add 4th app (should be the only app) - const app4 = new Vue(options) + const app4 = new Vue({ + router, + render (h) { return h('div') } + }) expect(router.app).toBe(app4) expect(router.apps.length).toBe(1) expect(router.app[0]).toBe(app4) From a90b3f12e68a1442214b58b6e5c1b31c114909bd Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 02:24:07 -0300 Subject: [PATCH 08/13] Update api.spec.js --- test/unit/specs/api.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index ae4468215..4566a9da8 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -149,6 +149,8 @@ describe('router.push/replace callbacks', () => { describe('router app destroy handling', () => { it('should remove destroyed apps from this.apps', () => { + Vue.use(Router) + const router = new Router({ mode: 'abstract', routes: [ From 2fe68ca16bb11e0d589d2f2618c6aa28fd8f2938 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 02:46:10 -0300 Subject: [PATCH 09/13] Update api.spec.js --- test/unit/specs/api.spec.js | 119 ++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index 4566a9da8..9973f1be1 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -148,74 +148,87 @@ describe('router.push/replace callbacks', () => { }) describe('router app destroy handling', () => { - it('should remove destroyed apps from this.apps', () => { - Vue.use(Router) + Vue.use(Router) - const router = new Router({ - mode: 'abstract', - routes: [ - { path: '/', component: { name: 'A' }} - ] - }) + const router = new Router({ + mode: 'abstract', + routes: [ + { path: '/', component: { name: 'A' }} + ] + }) - expect(router.apps.length).toBe(0) - expect(router.app).toBe(null) + // Add main app + const app1 = new Vue({ + router, + render (h) { return h('div') } + }) - // Add main app - const app1 = new Vue({ - router, - render (h) { return h('div') } - }) - expect(router.app).toBe(app1) - expect(router.apps.length).toBe(1) - expect(router.app[0]).toBe(app1) + // Add 2nd app + const app2 = new Vue({ + router, + render (h) { return h('div') } + }) - // Add 2nd app - const app2 = new Vue({ - router, - render (h) { return h('div') } - }) - expect(router.app).toBe(app1) - expect(router.apps.length).toBe(2) - expect(router.app[0]).toBe(app1) - expect(router.app[1]).toBe(app2) + // Add 3rd app + const app3 = new Vue({ + router, + render (h) { return h('div') } + }) - // Add 3rd app - const app3 = new Vue({ - router, - render (h) { return h('div') } - }) - expect(router.app).toBe(app1) - expect(router.apps.length).toBe(3) - expect(router.app[0]).toBe(app1) - expect(router.app[1]).toBe(app2) - expect(router.app[2]).toBe(app3) + it('router and apps should be defined', async () => { + expect(router).toBeDefined() + expect(router).toBeInstanceOf(Router) + expect(app1).toBeDefined() + expect(app1).toBeInstanceOf(Vue) + expect(app2).toBeDefined() + expect(app2).toBeInstanceOf(Vue) + expect(app3).toBeDefined() + expect(app3).toBeInstanceOf(Vue) + expect(app1.$router.apps).toBe(app2.$router.apps) + expect(app2.$router.apps).toBe(app3.$router.apps) + }) + + it('should have 3 registered apps', async () => { + expect(app1.$router).toBeDefined() + expect(app1.$router.app).toBe(app1) + expect(app1.$router.apps.length).toBe(3) + expect(app1.$router.apps[0]).toBe(app1) + expect(app1.$router.apps[1]).toBe(app2) + expect(app1.$router.apps[2]).toBe(app3) + }) - // Destroy second app + it('should remove 2nd destroyed app from this.apps', async () => { app2.$destroy() - expect(router.app).toBe(app1) - expect(router.apps.length).toBe(2) - expect(router.app[0]).toBe(app1) - expect(router.app[1]).toBe(app3) + await Vue.nextTick() + expect(app1.$router.app).toBe(app1) + expect(app1.$router.apps.length).toBe(2) + expect(app1.$router.app[0]).toBe(app1) + expect(app1.$router.app[1]).toBe(app3) + }) - // Destroy 1st app + it('should remove 1st destroyed app from this.apps and replace this.app', async () => { app1.$destroy() - expect(router.app).toBe(app3) - expect(router.apps.length).toBe(1) - expect(router.app[0]).toBe(app3) + await Vue.nextTick() + expect(app3.$router.app).toBe(app3) + expect(app3.$router.apps.length).toBe(1) + expect(app3.$router.app[0]).toBe(app3) + }) - // Destroy 3rd app + it('should remove last destroyed app from this.apps', async () => { app3.$destroy() - expect(router.app).toBe(app3) - expect(router.apps.length).toBe(0) + await Vue.nextTick() + expect(app3.$router.app).toBe(app3) + expect(app3.$router.apps.length).toBe(0) + }) - // Add 4th app (should be the only app) + it('should replace app with new app', async () => { const app4 = new Vue({ router, render (h) { return h('div') } }) - expect(router.app).toBe(app4) - expect(router.apps.length).toBe(1) - expect(router.app[0]).toBe(app4) + await Vue.nextTick() + expect(app4.$router.app).toBe(app4) + expect(app4.$router.apps.length).toBe(1) + expect(app4.$router.app[0]).toBe(app4) }) }) From 44dbc00a950c826e31da59ed01932d092d51cc07 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 02:52:54 -0300 Subject: [PATCH 10/13] Update api.spec.js --- test/unit/specs/api.spec.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index 9973f1be1..e74f88e27 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -175,7 +175,7 @@ describe('router app destroy handling', () => { render (h) { return h('div') } }) - it('router and apps should be defined', async () => { + it('router and apps should be defined', () => { expect(router).toBeDefined() expect(router).toBeInstanceOf(Router) expect(app1).toBeDefined() @@ -188,7 +188,7 @@ describe('router app destroy handling', () => { expect(app2.$router.apps).toBe(app3.$router.apps) }) - it('should have 3 registered apps', async () => { + it('should have 3 registered apps', () => { expect(app1.$router).toBeDefined() expect(app1.$router.app).toBe(app1) expect(app1.$router.apps.length).toBe(3) @@ -197,36 +197,32 @@ describe('router app destroy handling', () => { expect(app1.$router.apps[2]).toBe(app3) }) - it('should remove 2nd destroyed app from this.apps', async () => { + it('should remove 2nd destroyed app from this.apps', () => { app2.$destroy() - await Vue.nextTick() expect(app1.$router.app).toBe(app1) expect(app1.$router.apps.length).toBe(2) expect(app1.$router.app[0]).toBe(app1) expect(app1.$router.app[1]).toBe(app3) }) - it('should remove 1st destroyed app from this.apps and replace this.app', async () => { + it('should remove 1st destroyed app from this.apps and replace this.app', () => { app1.$destroy() - await Vue.nextTick() expect(app3.$router.app).toBe(app3) expect(app3.$router.apps.length).toBe(1) expect(app3.$router.app[0]).toBe(app3) }) - it('should remove last destroyed app from this.apps', async () => { + it('should remove last destroyed app from this.apps', () => { app3.$destroy() - await Vue.nextTick() expect(app3.$router.app).toBe(app3) expect(app3.$router.apps.length).toBe(0) }) - it('should replace app with new app', async () => { + it('should replace app with new app', () => { const app4 = new Vue({ router, render (h) { return h('div') } }) - await Vue.nextTick() expect(app4.$router.app).toBe(app4) expect(app4.$router.apps.length).toBe(1) expect(app4.$router.app[0]).toBe(app4) From 31dad30a94ec013705d6767bf9451f63b236a200 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 02:59:37 -0300 Subject: [PATCH 11/13] Update api.spec.js --- test/unit/specs/api.spec.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index e74f88e27..a066bace6 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -177,15 +177,17 @@ describe('router app destroy handling', () => { it('router and apps should be defined', () => { expect(router).toBeDefined() - expect(router).toBeInstanceOf(Router) + expect(router istanceof Router).toBe(true) expect(app1).toBeDefined() - expect(app1).toBeInstanceOf(Vue) + expect(app1 instanceof Vue).toBe(true) expect(app2).toBeDefined() - expect(app2).toBeInstanceOf(Vue) + expect(app2 instanceof Vue).toBe(true) expect(app3).toBeDefined() - expect(app3).toBeInstanceOf(Vue) + expect(app3 instanceof Vue).toBe(true) expect(app1.$router.apps).toBe(app2.$router.apps) expect(app2.$router.apps).toBe(app3.$router.apps) + expect(app1.$router.app).toBe(app2.$router.app) + expect(app2.$router.app).toBe(app3.$router.app) }) it('should have 3 registered apps', () => { From f9d556d492271fbb65478c6fcfeb8a241a1d7726 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 03:02:16 -0300 Subject: [PATCH 12/13] Update api.spec.js --- test/unit/specs/api.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index a066bace6..822253395 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -177,7 +177,7 @@ describe('router app destroy handling', () => { it('router and apps should be defined', () => { expect(router).toBeDefined() - expect(router istanceof Router).toBe(true) + expect(router instanceof Router).toBe(true) expect(app1).toBeDefined() expect(app1 instanceof Vue).toBe(true) expect(app2).toBeDefined() From e724261d584e0d371232e700bc7e6241e36eba26 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 30 Mar 2019 03:06:26 -0300 Subject: [PATCH 13/13] Update api.spec.js --- test/unit/specs/api.spec.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index 822253395..340aab6e8 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -201,17 +201,19 @@ describe('router app destroy handling', () => { it('should remove 2nd destroyed app from this.apps', () => { app2.$destroy() + expect(app1.$router).toBeDefined() + expect(app1.$router.app).toBeDefined() expect(app1.$router.app).toBe(app1) expect(app1.$router.apps.length).toBe(2) - expect(app1.$router.app[0]).toBe(app1) - expect(app1.$router.app[1]).toBe(app3) + expect(app1.$router.apps[0]).toBe(app1) + expect(app1.$router.apps[1]).toBe(app3) }) it('should remove 1st destroyed app from this.apps and replace this.app', () => { app1.$destroy() expect(app3.$router.app).toBe(app3) expect(app3.$router.apps.length).toBe(1) - expect(app3.$router.app[0]).toBe(app3) + expect(app3.$router.apps[0]).toBe(app3) }) it('should remove last destroyed app from this.apps', () => { @@ -227,6 +229,6 @@ describe('router app destroy handling', () => { }) expect(app4.$router.app).toBe(app4) expect(app4.$router.apps.length).toBe(1) - expect(app4.$router.app[0]).toBe(app4) + expect(app4.$router.apps[0]).toBe(app4) }) })