Vue: @click would trigger event other vnode @click event.

Created on 11 Sep 2017  ·  4Comments  ·  Source: vuejs/vue

Version

2.4.2

Reproduction link

https://jsbin.com/qejofexedo/edit?html,js,output

Steps to reproduce

see reproduction link.

What is expected?

When I click Expand is True, then expand to become false. And only countA changed.

What is actually happening?

When I click Expand is Ture, nothing happened.
The countA and countB changed.
I guess when I click, expand changed to false, but immediate the click event triggered. It executes another vnode click event. Then expand changed to true.

And More

  • If I rename the second div to another tag name, such as p, section, no errors occur.
  • If I move click event from i tag to parent div tag in the first div, no errors occur
bug improvement

Most helpful comment

So, this happens because:

  • The inner click event on <i> fires, triggering a 1st update on nextTick (microtask)
  • The microtask is processed before the event bubbles to the outer div. During the update, a click listener is added to the outer div.
  • Because the DOM structure is the same, both the outer div and the inner element are reused.
  • The event finally reaches outer div, triggers the listener added by the 1st update, in turn triggering a 2nd update.

This is quite tricky in fix, and other libs that leverages microtask for update queueing also have this problem (e.g. Preact). React doesn't seem to have this problem because they use a synthetic event system (probably due to edge cases like this).

To work around it, you can simply give the two outer divs different keys to force them to be replaced during updates. This would prevent the bubbled event to be picked up:

<div class="header" v-if="expand" key="1"> // block 1
  <i @click="expand = false, countA++">Expand is True</i> // element 1
</div>
<div class="expand" v-if="!expand" @click="expand = true, countB++" key="2"> // block 2
  <i>Expand is False</i> // element 2
</div>

All 4 comments

qq20170911-185025
seems normal

Your repro is working as intended...

@Kingwl @yyx990803 Sorry about that. I test others case and forgot to change back.

The important code is

<div class="header" v-if="expand"> // block 1
  <i @click="expand = false, countA++">Expand is True</i> // element 1
</div>
<div class="expand" v-if="!expand" @click="expand = true, countB++"> // block 2
  <i>Expand is False</i> // element 2
</div>

There is four case:

  • click event listen on block 1 and block2, works well
  • click event listen on element 1 and element 2, works well
  • click event listen on block 1 and element 2, change expand to true is ok. But cannot change back.
  • click event listen on element 1 and block 2, cannot change expand to false. But can change expand to true.

So, this happens because:

  • The inner click event on <i> fires, triggering a 1st update on nextTick (microtask)
  • The microtask is processed before the event bubbles to the outer div. During the update, a click listener is added to the outer div.
  • Because the DOM structure is the same, both the outer div and the inner element are reused.
  • The event finally reaches outer div, triggers the listener added by the 1st update, in turn triggering a 2nd update.

This is quite tricky in fix, and other libs that leverages microtask for update queueing also have this problem (e.g. Preact). React doesn't seem to have this problem because they use a synthetic event system (probably due to edge cases like this).

To work around it, you can simply give the two outer divs different keys to force them to be replaced during updates. This would prevent the bubbled event to be picked up:

<div class="header" v-if="expand" key="1"> // block 1
  <i @click="expand = false, countA++">Expand is True</i> // element 1
</div>
<div class="expand" v-if="!expand" @click="expand = true, countB++" key="2"> // block 2
  <i>Expand is False</i> // element 2
</div>
Was this page helpful?
0 / 5 - 0 ratings