Repository: vuejs/vue
Pull request: fix(v-on): add removing all dom event listeners when vnode destroyed

Add removing all dom event listeners when vnode destroyed
fixes #10083
fixes #10004 

<!--
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
- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [ ] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [ ] 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/platforms/web/runtime/modules/events.js
patch: @@ -5,6 +5,7 @@ import { updateListeners } from 'core/vdom/helpers/index'
 import { isIE, isFF, supportsPassive, isUsingMicroTask } from 'core/util/index'
 import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'
 import { currentFlushTimestamp } from 'core/observer/scheduler'
+import { emptyNode } from 'core/vdom/patch'
 
 // normalize v-model event tokens that can only be determined at runtime.
 // it's important to place the event as the first in the array because
@@ -108,13 +109,16 @@ function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
   }
   const on = vnode.data.on || {}
   const oldOn = oldVnode.data.on || {}
-  target = vnode.elm
+  // vnode is empty when removing all listeners,
+  // and use old vnode dom element
+  target = vnode.elm || oldVnode.elm
   normalizeEvents(on)
   updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
   target = undefined
 }
 
 export default {
   create: updateDOMListeners,
-  update: updateDOMListeners
+  update: updateDOMListeners,
+  destroy: (vnode: VNodeWithData) => updateDOMListeners(vnode, emptyNode)
 }

filename: test/unit/features/component/component-keep-alive.spec.js
patch: @@ -1249,5 +1249,37 @@ describe('Component keep-alive', () => {
         }).then(done)
       }
     })
+
+    // #10083
+    it('should not attach event handler repeatedly', done => {
+      const vm = new Vue({
+        template: `
+          <keep-alive>
+            <btn v-if="showBtn" @click.native="add" />
+          </keep-alive>
+        `,
+        data: { showBtn: true, n: 0 },
+        methods: {
+          add () {
+            this.n++
+          }
+        },
+        components: {
+          btn: { template: '<button>add 1</button>' }
+        }
+      }).$mount()
+
+      const btn = vm.$el
+      expect(vm.n).toBe(0)
+      btn.click()
+      expect(vm.n).toBe(1)
+      vm.showBtn = false
+      waitForUpdate(() => {
+        vm.showBtn = true
+      }).then(() => {
+        btn.click()
+        expect(vm.n).toBe(2)
+      }).then(done)
+    })
   }
 })