Subscribe on changes!

v-for: inline native event listeners force re-render of each list component when one item in the list changes.

avatar
Dec 7th 2022

Vue version

3.2.45

Link to minimal reproduction

https://sfc.vuejs.org/#eNqVUbFOwzAQ/ZWTF1LRxlSIJUoqGFkQEmKqO6Sp07ptbOvsBFCUf+fstKULEmz33p3fvXvu2ZO1addKlrHcVaisByd9axdCq8Ya9NADyhoGqNE0cEOjN5fWs5fNiU95AEGJ2kJXRjsPJSIU4XmyFBqgL7cymw/Tn/ruCtxfg4cIVpMrra18t6SWKL2RnxMoFjSJmHblsZXLSK5SGoLbAuaD0Dkfz6FDCJA3eyy9JASQb1QXCyrjCd2sNlgIlqgpnOSVDu4Fg+wgv6ilQqncm0VZbgIOu4h7rI6qOhAR/Z3MEc/HTXxclfOLATZlY3qzprTp3hlN0fdhmERjwwmWQWQCR4kGLNjOe+syzl1dhZj3LjW45VSl2GqvGplK18zWaD6cRBIWLEhQEgOtPH/O/75Z0DW10vIVjXVJtHSOIHtpm7VEUqc/+lvWkO/mi76/SMAw5Jyo34IavgEsz+HY

Steps to reproduce

go to that link and click on the number There is only one object where the change occurs, but all components are re-rendered.

What is expected?

There is only one object where the change occurs, but all components are re-rendered.

What is actually happening?

There is only one object where the change occurs, but all components are re-rendered.

System Info

System:
    OS: Windows 10 10.0.19044
    CPU: (12) x64 AMD Ryzen 5 5600X 6-Core Processor
    Memory: 8.41 GB / 15.93 GB
  Binaries:
    Node: 16.17.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 8.19.1 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.19041.1266.0)
    Internet Explorer: 11.0.19041.1566
  npmPackages:
    vue: ^3.2.45 => 3.2.45

Any additional comments?

If you pass a non-age object directly as props, only the elements that change will be re-rendered. Why do all re-renders happen when age is passed as props instead of an object?

avatar
Dec 8th 2022

I don't quite understand your question, your description in expected is the same as the description in actually happening,What does all re-renders mean?

avatar
Dec 8th 2022

component re-renders

avatar
Dec 8th 2022

component re-renders

I get it 😂

avatar
Dec 8th 2022

The change is one object, but the actual re-rendering happens to all components

avatar
Dec 8th 2022

Cause

The click handler on the child gets a new inline function on every parent re-render, causing every child to update:

     const _item = (_openBlock(), _createBlock(Item, {
        key: i,
        isSpread: i.age,
        onClick: $event => (ageUp(index))             // <-- this causes the re-render 
      }, null, 8 /* PROPS */, ["isSpread", "onClick"]))
      _item.memo = _memo

Compiler seems to have problems creating cache entries for these handlers as they are unique to every child item - they use the index. Something for us to think about how to improve/fix.

Workaround/Improvement

Use v-memo to tell the for-loop which items need a re-render (Playground)

avatar
Dec 8th 2022

thanks for your detailed answer

avatar
Feb 24th 2023

Cause

The click handler on the child gets a new inline function on every parent re-render, causing every child to update:

     const _item = (_openBlock(), _createBlock(Item, {
        key: i,
        isSpread: i.age,
        onClick: $event => (ageUp(index))             // <-- this causes the re-render 
      }, null, 8 /* PROPS */, ["isSpread", "onClick"]))
      _item.memo = _memo

Compiler seems to have problems creating cache entries for these handlers as they are unique to every child item - they use the index. Something for us to think about how to improve/fix.

Workaround/Improvement

Use v-memo to tell the for-loop which items need a re-render (Playground)

I noticed that when the compiler compiles this code, it treats onClick as a dynamicProps. When its value is an arrow function, the component considers the previous props and next props to always be different, causing it to repeatedly re-render. In this case, I don't think that events which starts with on should be considered as dynamicProps to be checked for changes. Since it can be guaranteed that the value of @xxx is always a function, it seems that there are no scenarios where the event is updated.

avatar
Feb 24th 2023

Since it can be guaranteed that the value of @xxx is always a function, it seems that there are no scenarios where the event is updated.

It's not as easy as that, unfortunately.

It's always always a function, but it's always a new function, that could potentially do different things when being called (Vue can't be sure that the function's behavior is guaranteed to be stable across re-renders. Especially since that function could have been defined in a custom render function for all the renderer knows. But even from an SFC, things like this can happen:

<li v-for="(item, index) in items">
  <my-component :item="item" @click="() => doSomething(index)" />
</li>

The inline function above holds a reference to the index. if that index changes, only the new function, created during re-render, holds a reference to that new index. So we need to re-render the child so it can do potentially necessary updates with that new function.

there sure are ways to improve this, but simply ignoring these changes in the patch phase is not a valid solution.

Edit: It's worth clarifying that this does not affect component events it only affects listeners who are not declared via defineEmits/emits: and are subsequently added as event-listeners to the component's root element.

avatar
Feb 24th 2023

Indeed, it seems that I have oversimplified this problem. For now, v-memo seems to be the best way to solve this problem