back to top

Vude.js: ciclo di vita di un componente

Nel corso della sua esistenza ciascun componente attraversa varie fasi e va incontro ad una serie di eventi predefiniti sotto il diretto controllo del framework. Fin dal momento in cui un componente viene creato e poi montato nel DOM, passando per le fasi di aggiornamento e terminando con l’istante in cui viene rimosso dal DOM e distrutto, Vue si occupa della gestione dell’intero ciclo di vita del componente stesso.

Cosa sono i lifecycle hooks di un componente

I Lifecycle Hooks sono dei metodi particolari messi a disposizione da Vue che possiamo opzionalmente implementare per ciascun componente. Tali metodi vengono opportunamente invocati da Vue e consentono quindi di eseguire determinate azioni nei momenti chiave del ciclo di vita.

Esistono 8 metodi principali che vengono eseguiti in corrispondenza di altrettanti eventi e che possiamo raggruppare in 4 differenti categorie a seconda della fase in cui vengono invocati.

I metodi da implementare sono dunque i seguenti:

  • beforeCreate() e created() vengono invocati da Vue durante la fase di creazione di un componente e possono essere utili per effettuare eventuali operazioni di inizializzazione prima che un componente venga aggiunto al DOM.
  • beforeMount() e mounted() vengono chiamati rispettivamente prima e dopo che Vue ha eseguito il rendering iniziale del componente.
  • i metodi beforeUpdate() e updated() sono invece chiamati durante il processo di aggiornamento del componente.
  • beforeDestroy() e destroyed() vengono infine invocati nella procedura di distruzione del componente

Attenzione: in Vue 3 i due lifecycle hooks beforeDestroy() e destroyed() sono stati rinominati rispettivamente beforeUnmount() e unmounted()!

Procediamo allora ad analizzare in dettaglio i diversi metodi sopra elencati.

beforeCreate()

Il primo metodo che viene invocato in modo sincrono da Vue all’inizio del ciclo di vita di un componente è beforeCreate(). Ad essere più precisi, beforeCreate() viene eseguito immediatamente dopo che un’istanza di tipo Vue è stata inizializzata (i componenti sono essi stessi istanze di Vue). In questo primo metodo non è ancora possibile interagire con nessun membro del componente in quanto la proprietà data (e l’intero sistema di reattività), le computed properties, i metodi e i watchers non sono ancora disponibili.

Per dimostrare quanto abbiamo appena affermato, realizziamo un semplice esempio in cui creiamo un nuovo componente nel file MyComponent.vue.

<template>
  <div>My component</div>  
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      name: 'Test'
    }
  },
  computed: {
    upperCaseName() {
      return this.name.toUpperCase();
    } 
  },
  beforeCreate() {
    console.log('data.name è pari a: ', this.name); // undefined
    console.log('computed property upperCaseName è pari a: ', this.upperCaseName); // undefined
    // Non invochiamo foo() perché è ancora 'undefined'
    console.log('il metodo foo: ', this.foo); // undefined
  },
  methods: {
    foo() {
      console.log('I am foo');
    }
  }
}
</script>

Possiamo poi verificare quando viene invocato il metodo beforeCreate() utilizzando la funzione di Instant Prototyping di Vue CLI. Nella cartella in cui è presente il file MyComponent.vue lanciamo il comando vue serve MyComponent.vue.

created()

Il secondo lifecycle hook ad essere invocato è created() che viene eseguito in modo sincrono dopo che un’istanza di tipo Vue è stata creata. A questo punto le proprietà dell’oggetto delle opzioni, usato per definire l’istanza, sono state tutte processate. È quindi possibile accedere alle proprietà dell’oggetto data, alle computed properties, ai metodi e così via. Non è però ancora iniziata la fase di montaggio nel DOM. Per questo motivo la proprietà $el dell’istanza è ancora undefined.

<template>
  <div>My component</div>  
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      name: 'Test'
    }
  },
  computed: {
    upperCaseName() {
      return this.name.toUpperCase();
    } 
  },
  created() {
    console.group('### created() hook ###');
    console.log('data.name è pari a: ', this.name); // Test
    console.log('computed property upperCaseName è pari a: ', this.upperCaseName); // TEST
    console.log('il metodo foo restituisce: ', this.foo()); // I am foo()
    console.groupEnd();
  },
  methods: {
    foo() {
      return 'I am foo()';
    }
  }
}
</script>
output del lifecycle hook created()

Dato che abbiamo accesso ai metodi e all’oggetto data, il lifecycle hook created viene spesso utilizzato per effettuare richeste HTTP asincrone al fine di prelevare da un server remoto dei dati con i quali inizializzare un componente.

Nell’esempio sottostante vogliamo creare una semplice applicazione che utilizza la REST API di QuoteGarden per prelevare delle citazioni di personaggi illustri.

Nel file App.vue importiamo il componente Quote a cui passiamo due props opzionali colorSchema e loadingSpinnerColor. In particolare colorSchema è un oggetto in cui le chiavi delle proprietà sono espresse secondo la sintassi delle Custom Properties CSS.

// App.vue
<template>
  <div class="container">
    <Quote 
      :colorSchema="colorSchema"
      :loadingSpinnerColor="loadingSpinnerColor"
    />
  </div>
</template>

<script>
import Quote from './Quote';

export default {
  name: 'App',
  data() {
    return {
      colorSchema: {
        '--primary-color-100': 'hsl(205, 82%, 98%)',
        '--primary-color-200': 'hsl(205, 96%, 84%)',
        '--primary-color-300': 'hsl(205, 82%, 78%)',
        '--primary-color-400': 'hsl(205, 76%, 65%)',
        '--primary-color-500': 'hsl(205, 64%, 60%)',
        '--primary-color-600': 'hsl(205, 64%, 44%)',
        '--primary-color-700': 'hsl(205, 76%, 40%)',
        '--primary-color-800': 'hsl(205, 80%, 30%)',
        '--primary-color-900': 'hsl(205, 98%, 22%)',
        '--error-color-500': 'hsl(360, 65%, 40%)'
      },
      loadingSpinnerColor: 'hsl(205, 82%, 78%)'
    }
  },
  components: {
    Quote
  }
}
</script>

<style>
  html, body {
    height: 100%;
  }

  .container {
    margin-top: 2.5rem;
    display: flex;
    justify-content: center;
  }
</style>

Nel componente Quote importiamo LoadingSpinner che verrà mostrato sullo schermo mentre il componente cerca di recuperare le informazioni dal server remoto. Quote richiede i nuovi dati dal server avviando una nuova richiesta nel lifecycle hook created in cui abbiamo utilizzato async/await per la gestione delle promise.

Se riceviamo correttamente i dati, settiamo la proprietà data.quote opportunamente e nascondiamo il componente LoadingSpinner.

Per rendere il componente personalizzabile, abbiamo usato delle Custom Properties CSS associando la prop colorSchema all’attributo style applicato all’elemento <div class"quote-container">. Le stesse Custom Properties sono state quindi impiegate nella definizione delle diverse regole CSS relative agli elementi presenti nel componente.

// Quote.vue
<template>
  <div class="quote-container" :style="colorSchema">
    <LoadingSpinner v-if="loading" :color="loadingSpinnerColor" />
    <blockquote v-if="!loading && !error">
      <p>{{ this.quote.text }}</p>
      <footer>{{ this.quote.author }}</footer>
    </blockquote>
    <p class="error" v-if="error">
      {{ error }}
    </p>
  </div>
</template>

<script>
import LoadingSpinner from './LoadingSpinner';

export default {
  name: 'Quote',
  components: {
    LoadingSpinner
  },
  props: {
    colorSchema: {
      type: Object,
      default() {
        return {
          '--primary-color-100': 'hsl(210, 38%, 97%)',
          '--primary-color-200': 'hsl(212, 35%, 99%)',
          '--primary-color-300': 'hsl(210, 32%, 78%)',
          '--primary-color-400': 'hsl(210, 28%, 72%)',
          '--primary-color-500': 'hsl(209, 22%, 60%)',
          '--primary-color-600': 'hsl(210, 22%, 44%)',
          '--primary-color-700': 'hsl(209, 29%, 36%)',
          '--primary-color-800': 'hsl(209, 34%, 30%)',
          '--primary-color-900': 'hsl(208, 60%, 14%)',
          '--error-color-500': 'hsl(360, 65%, 40%)'
        }
      }
    },
    loadingSpinnerColor: {
      type: String,
      default: 'hsl(210, 32%, 78%)'
    }
  },
  data() {
    return {
      loading: false,
      quote: {
        text: '',
        author: ''
      },
      error: '',
    }
  },
  async created() {
    try {
      this.loading = true;

      const response = 
        await fetch('https://quote-garden.herokuapp.com/quotes/random');
      const data = await response.json();

      this.quote.text = data.quoteText;
      this.quote.author = data.quoteAuthor || 'Anonymous';

      this.loading = false;
    } catch(err) {
      this.error = err.message;
      this.loading = false
    }
  }
}
</script>

<style scoped>
  blockquote {
    max-width: 640px;
    font-family: 'Lora', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
    padding: 2rem;
    background-color: var(--primary-color-100);
    border: 1px solid var(--primary-color-300);
    border-radius: 16px;
  }

  blockquote p {
    margin-top: 0;
    font-style: italic;
    font-size: 1rem;
    line-height: 1.5;
    color: var(--primary-color-900);
  }

  blockquote::before {
    /* Virgolette */
    content: '201c';
    font-size: 7rem;
    line-height: .8;
    color: var(--primary-color-400);
    display: block;
    margin-bottom: -2.5rem;
  }

  blockquote footer {
    color: var(--primary-color-600);
    font-weight: 700;
    font-size: 0.875rem;
    text-align: right;
    margin-top: 1.5rem;
  }

  blockquote footer::before {
    /* trattino lungo seguito da spazio */
    content: '2014020'
  }

  .error {
    text-align: center;
    font-size: 1rem;
    color: var(--error-color-500);
  }
</style>

Ed infine riportiamo il componente LoadingSpinner.

// LoadingSpinner.vue
<template>
  <div class="loading-spinner-container">
    <div class="circles">
      <div 
        v-for="i in 4" 
        :key="i" 
        :style="style">
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'LoadingSpinner',
  props: {
    color: {
      type: String,
      default: 'black'
    }
  },
  computed: {
    style() {
      return `background-color: ${this.color}`
    }
  }
}
</script>

<style scoped>
.loading-spinner-container {
  text-align: center;
}

/* Codice originale: https://loading.io/css/ */
.circles {
  display: inline-block;
  position: relative;
  width: 80px;
  height: 16px;
}
.circles div {
  position: absolute;
  top: 16px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.circles div:nth-child(1) {
  left: 8px;
  animation: circles1 0.6s infinite;
}
.circles div:nth-child(2) {
  left: 8px;
  animation: circles2 0.6s infinite;
}
.circles div:nth-child(3) {
  left: 32px;
  animation: circles2 0.6s infinite;
}
.circles div:nth-child(4) {
  left: 56px;
  animation: circles3 0.6s infinite;
}
@keyframes circles1 {
  0% {
    transform: scale(0);
  }
  100% {
    transform: scale(1);
  }
}
@keyframes circles3 {
  0% {
    transform: scale(1);
  }
  100% {
    transform: scale(0);
  }
}
@keyframes circles2 {
  0% {
    transform: translate(0, 0);
  }
  100% {
    transform: translate(24px, 0);
  }
}
</style>

Supponendo di aver creato i tre file all’interno di una certa cartella, possiamo visualizzare un’anteprima della nostra semplice applicazione con il comando vue serve.

https://vimeo.com/462643608

beforeMount()

Nel momento in cui viene invocato il lifecycle hook beforeMount() il template del componente è stato già compilato ma non è stata ancora effettuato il primo rendering. A questo punto la proprietà $el non è ancora definita

Questo metodo non viene invocato in caso di rendering lato server.

mounted()

Questo metodo viene invocato dopo beforeMount(). A questo punto un componente è stato montato nel DOM. La proprietà $el viene opportunamente settata.

<template>
  <div>My component</div>  
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      name: 'Test'
    }
  },
  computed: {
    upperCaseName() {
      return this.name.toUpperCase();
    } 
  },
  mounted() {
    console.group('### mounted() hook ###');
    console.log('this.$el: ', this.$el); // <div>My component</div> 
    console.log('data.name è pari a: ', this.name); // Test
    console.log('computed property upperCaseName è pari a: ', this.upperCaseName); // TEST
    console.log('il metodo foo restituisce: ', this.foo()); // I am foo()
    console.groupEnd();
  },
  methods: {
    foo() {
      return 'I am foo()';
    }
  }
}
</script>
output del lifecycle hook mounted()

Se un componente ha dei figli, non è detto che questi sono anche stati completamente inseriti nel DOM. Per questo motivo, per eseguire delle operazioni sul DOM ed assicurarsi che l’intero albero di elementi sia pronto, è possibile eseguire delle operazioni all’interno di una funzione callback che passiamo al metodo this.$nextTick(callback). Quest’ultimo posticipa l’esecuzione della funzione callback che sarà invocata quando è stato completato il processo di rendering dell’intera view.

Anche questo metodo non viene invocato in caso di rendering lato server.

beforeUpdate()

Viene invocato quando vengono modificati dei dati che richiedono l’aggiornamento del DOM. In questo metodo è possibile accedere al DOM nello stato precedente alle modifiche in quanto il DOM non è stato ancora aggiornato.

Questa funzione non viene invocata in caso di rendering lato server.

updated()

Il lifecycle hook update() viene invocato dopo beforeUpdate(). A questo punto il DOM è stato aggiornato ed è possibile accedere alle nuove informazioni ed eventualemente eseguire delle operazioni sul DOM che si trova in un nuovo stato.

È opportuno specificare che non conviene modificare lo stato di un componente in questo metodo ed è consiglitato utilizzare invece le computed properties o i watchers per intervenire in risposta a degli aggiornamenti.

Come per il metodo mounted(), non è garantito che eventuali componenti figli sono stati completamente aggiornati nel DOM. Per lo stesso motivo specificato sopra è consigliato utilizzare il metodo this.$nextTick().

updated() {
  this.$nextTick(function () {
    // Qui l'intera view è stata aggiornata
  })
}

beforeDestroy()

Questo metodo è eseguito immediatamente prima che un’istanza di tipo Vue venga distrutta. L’istanza è ancora funzionante ed è possibile eseguire operazioni di pulizia prima della distruzione definitiva dell’istanza.

destroyed()

Quando viene invocato il metodo destroyed(), l’istanza è stata ormai rimossa dal DOM insieme ad eventuali istanze discendenti. Tutte le direttive applicate sono state a questo punto dissociate dall’istanza. Abbiamo però ancora accesso alle proprietà dell’oggetto data così come ai metodi e alle computed properties. Si tratta dell’ultimo metodo del ciclo di vita di un’istanza.

È opportuno notare che, come nel caso di beforeDestroy(), anche questo metodo non viene invocato durante la procedura di rendering lato server, tecnica che non illustreremo in questa guida introduttiva, ma di cui abbiamo brevemente elencato i vantaggi nella prima lezione.

Utilizzando la direttiva v-if applicata al componente MyComponent possiamo allora vedere come vengono invocati i metodi beforeDestroy() e destroyed() quando un componente viene rimosso dal DOM.

Definiamo dunque il solito componente MyComponent.

<template>
  <div>My component</div>  
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      name: 'Test'
    }
  },
  computed: {
    upperCaseName() {
      return this.name.toUpperCase();
    } 
  },
  beforeDestroy() {
    console.log('## beforeDestroy ##');
    console.log(this.name); // Test
  },
  destroyed() {
    console.log('## destroyed ##');
    console.log(this.upperCaseName); // TEST
  },
  methods: {
    foo() {
      return 'I am foo()';
    }
  }
}
</script>

Ed importiamolo poi all’interno di un componente App in cui usiamo la direttiva v-if per rimuovere MyComponent dal DOM.

<template>
  <div>
    <MyComponent v-if="show" />
    <button @click="show = !show">Destroy Component</button>
  </div>
</template>

<script>
import MyComponent from './MyComponent';

export default {
  name: 'App',
  components: {
    MyComponent
  },
  data() {
    return {
      show: true
    }
  }
}
</script>

Per verificare il funzionamento del nostro esempio possiamo eseguire il comando vue serve all’interno della cartella che contiene i due componenti.

Riepilogo

In questa lezione abbiamo parlato del ciclo di vita di un componente ed abbiamo presentato quelli che vengono definiti Lifecycle hooks. Si tratta di particolari metodi che possiamo implementare in ciascun componente per il quale intendiamo eseguire determinate operazioni al verificarsi di uno degli eventi chiave del ciclo di vita del componente.

Dopo aver illustrato le diverse fasi in cui va incontro un componente, nella prossima lezione vedremo come creare dei componenti dinamici che consentono di sostituire un componente con altri componenti a run-time senza utilizzare Vue Router.

Pubblicitร