Repository: vuejs/vue
Pull request: fix(component): clean up memory leak after loading async component completes (fix #8740)

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**


Changed files:
filename: src/core/vdom/helpers/resolve-async-component.js
patch: @@ -61,10 +61,14 @@ export function resolveAsyncComponent (
     const contexts = factory.contexts = [context]
     let sync = true
 
-    const forceRender = () => {
+    const forceRender = (renderCompleted: boolean) => {
       for (let i = 0, l = contexts.length; i < l; i++) {
         contexts[i].$forceUpdate()
       }
+
+      if (renderCompleted) {
+        contexts.length = 0
+      }
     }
 
     const resolve = once((res: Object | Class<Component>) => {
@@ -73,7 +77,7 @@ export function resolveAsyncComponent (
       // invoke callbacks only if this is not a synchronous resolve
       // (async resolves are shimmed as synchronous during SSR)
       if (!sync) {
-        forceRender()
+        forceRender(true)
       }
     })
 
@@ -84,7 +88,7 @@ export function resolveAsyncComponent (
       )
       if (isDef(factory.errorComp)) {
         factory.error = true
-        forceRender()
+        forceRender(true)
       }
     })
 
@@ -111,7 +115,7 @@ export function resolveAsyncComponent (
             setTimeout(() => {
               if (isUndef(factory.resolved) && isUndef(factory.error)) {
                 factory.loading = true
-                forceRender()
+                forceRender(false)
               }
             }, res.delay || 200)
           }

filename: test/unit/features/transition/transition-mode.spec.js
patch: @@ -457,7 +457,7 @@ if (!isIE9) {
             setTimeout(() => {
               resolve({ template: '<div><h1>component B</h1></div>' })
               Vue.nextTick(next)
-            }, (duration + buffer) * 1.5)
+            }, (duration + buffer) * 1.7)
           }
         },
         data: {

filename: test/unit/modules/vdom/create-component.spec.js
patch: @@ -58,6 +58,7 @@ describe('create-component', () => {
       vnode = createComponent(async, data, vm, vm)
       expect(vnode.isComment).toBe(true) // not to be loaded yet.
       expect(vnode.asyncFactory).toBe(async)
+      expect(vnode.asyncFactory.contexts.length).toEqual(1)
     }
     function loaded () {
       vnode = createComponent(async, data, vm, vm)
@@ -68,6 +69,7 @@ describe('create-component', () => {
       expect(vnode.elm).toBeUndefined()
       expect(vnode.ns).toBeUndefined()
       expect(vnode.context).toEqual(vm)
+      expect(vnode.asyncFactory.contexts.length).toEqual(0)
       expect(vm.$forceUpdate).toHaveBeenCalled()
       done()
     }