4 votes

Pourquoi Vue3 redonne-t-il inutilement les nœuds dans V-for ?

Voici un petit test que j'ai fait pour étudier le re-rendu inutile des nœuds pour les listes dans vue3 (vue2 a le même comportement) : https://kasheftin.github.io/vue3-rerender/ . C'est le code source : https://github.com/Kasheftin/vue3-rerender/tree/master .

J'essaie de comprendre pourquoi Vue rend à nouveau les nœuds déjà rendus dans V-for dans certains cas. Je connais (et fournirai ci-dessous) quelques techniques pour éviter le re-rendu, mais pour moi il est crucial de comprendre la théorie.

Pour les tests, j'ai ajouté une directive v-test factice qui ne fait qu'enregistrer le déclenchement des hooks mounted/beforeUnmount.

Test 1

<div v-for="i in n" :key="i">
  <div>{{ i }}</div>
  <div v-test="log2">{{ log(i) }}</div>
</div>

Résultat : tous les noeuds sont re-rendus lorsque n augmente. Pourquoi ? Comment éviter cela ?

Test 2

Test2.vue:
<RerenderNumber v-for="i in n" :key="i" :i="i" />

RerenderNumber.vue:
<template>
  <div v-test="log2">{{ log() }}</div>
</template>

Résultat : Il fonctionne correctement. Le déplacement du contenu interne de test1 vers un composant distinct résout le problème. Pourquoi ?

Test 3

<RerenderObject v-for="i in n" :key="i" :test="{ i: { i: { i } } }" />

Résultat : un nouveau rendu inutile. Il semble qu'il ne soit pas permis de construire des objets à la volée dans le cycle avant de les envoyer à un composant enfant, probablement pour les raisons suivantes {} != {} en JavaScript.

Test 4

<template>
  <RerenderNumberStore v-for="item in items" :key="item.id" :item="item" />
</template>

<script>
export default {
  computed: {
    items () {
      return this.$store.state.items
    }
  },
  methods: {
    addItem () {
      this.$store.commit('addItem', { id: this.items.length, name: `Item ${this.items.length}` })
    }
  }
}
</script>

Ici, le magasin Vuex le plus simple est utilisé. Il fonctionne correctement - pas de re-rendu inutile même si l'élément prop est un objet.

Test 5

<RerenderNumberStore v-for="item in items" :key="item.id" :item="{ id: item.id, name: item.name }" />

Identique au test 4, mais l'élément prop a été restructuré - et nous obtenons un re-rendu inutile.

Test 6

Test6.vue:
<RerenderNumberStoreById v-for="item in items" :key="item.id" :item-id="item.id" />

RerenderNumberStoreById.vue:
<template>
  <div v-test="log">{{ item.name }}</div>
</template>

<script>
export default {
  props: ['itemId'],
  computed: {
    item () { return this.$store.state.items.find(item => item.id === this.itemId) }
  }
}
</script>

Résultat : un nouveau rendu inutile. Pourquoi ? Je ne peux pas trouver de raison pour laquelle le comportement diffère du test 4. Celui-ci est moins clair pour moi - l'élément calculé n'est pas modifié de quelque façon que ce soit lorsque le nouvel élément est ajouté au tableau des éléments. Il renvoie le MÊME objet. Il doit être mis en cache, correspondre à la valeur précédente et ne déclenche aucune mise à jour dans le DOM.

5voto

Alex Pakka Points 2682

Vue est un système réactif, donc, pour répondre à cette question, il faut comprendre comment les observables cachables fonctionnent et quelle est leur granularité. Donc, s'il vous plaît, soyez indulgent avec moi.

Imaginez que vous avez une fonction coûteuse, par exemple

getCurrentTotal() { return state.x + state.y; }

et il n'a pas d'effets secondaires, c'est-à-dire pour le même x y y le résultat est exactement le même, et nous n'aurons jamais besoin de l'appeler à nouveau, à moins que l'une des deux valeurs ne change.

Pour permettre l'observation, il faut utiliser un wrapper comme suit

const state = reactive({x:1,y:2,z:3})

Ce wrapper va créer une carte des observateurs :

--- initial state ---
x -> []
y -> []
z -> []

(peu importe où cette carte "vit" ou sous quelle forme, il existe de nombreuses stratégies)

Il créera également un cache des résultats.

Lorsque votre fonction est appelée pour la première fois (alias "dry run"), chaque accès au réactif state est mémorisé, et la carte des observateurs est mise à jour en :

--- after first run of getCurrentTotal() ---
x -> [getCurrentTotal]
y -> [getCurrentTotal]
z -> []

et le cache des résultats sera getCurrentTotal,{x:1, y:2} -> 3 (simplifié).

Maintenant, si vous faites quelque chose comme

state.x++

le passeur pour state.x trouvera qu'il doit exécuter getCurrentTotal() encore une fois, parce que {x:2, y:2} n'est pas dans le cache, et voilà, vous avez une mise à jour.

Maintenant, TLDR :

Dans votre premier exemple Test1, une fonction observable est l'ensemble de la boucle for :

observedRenderer1() {
   for i in n: 
     add or modify (if :key exists) a div and inside put all the stuff
} 

Remarque : elle sera appelée lors de tout changement dans le fichier n et va parcourir toute la boucle. Pas de raccourcis ici.

Dans votre deuxième exemple Test2,

observedRenderer2() {
   for i in n: 
      callSomeOtherRenderer(i)
} 

Aha ! La boucle est toujours là. Mais maintenant notre unité de travail est plus granulaire. Le système réactif vérifie son cache et n'appelle pas les moteurs de rendu pour les éléments suivants RerenderNumber(1) ou RenderNumber(2) s'il a déjà ces résultats.

La réalité est un peu plus complexe, Vue conserve une copie de tous les résultats dans le DOM virtuel (à ne pas confondre avec le Shadow DOM !) où il conserve suffisamment d'informations pour savoir shouldComponentUpdate ou pas. Oui, il serait possible de créer un VNode dans l'arbre virtuel pour chaque div dans l'itération de la boucle. Mais alors, pour un tableau dense de 100x100 cellules, vous auriez 10k objets dans votre arbre et, en tant qu'utilisateur de Vue, vous ne serez jamais en mesure de l'optimiser.

Si votre question ressemble à la découverte d'un bogue, il s'agit en fait d'un mécanisme puissant qui vous permet de contrôler exactement la granularité de vos mises à jour. C'est une sorte de compromis entre la mémoire et la vitesse.

Le test 3 (ou le test 5) échoue pour une raison plus profonde, mais dans le même ordre d'idées : vous créez de nouveaux objets à chaque itération et l'appel d'équations profondes sur ces objets pendant le rendu est trop coûteux dans la vie réelle. Passez-les en tant que props séparés comme dans Test4 et tout ira bien.

Le test 6 est facile à expliquer si l'on pense que, lors de l'essai à blanc, chaque élément devait être exécuté sur l'ensemble de la collection d'éléments, de sorte que la carte des dépendances de chaque élément rendu. RerenderNumberStoreById est constitué de tous les éléments de la liste.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X