javascript - Hvordan overvåke props-endringer med Vue Composition API/Vue 3?


Mens Vue Composition API RFC-referansesiden har mange avanserte bruksscenarier med watch-modulen, finnes det ingen eksempler på hvordan man overvåker komponentprops.

Det er heller ikke nevnt på hovedsiden til Vue Composition API RFC eller vuejs/composition-api på Github.

Jeg har laget en Codesandbox for å utdype dette problemet.

<template>
  <div id="app">
    <img width="25%" src="./assets/logo.png">
    <br>
    <p>Prop watch demo with select input using v-model:</p>
    <PropWatchDemo :selected="testValue"/>
  </div>
</template>

<script>
import { createComponent, onMounted, ref } from "@vue/composition-api";
import PropWatchDemo from "./components/PropWatchDemo.vue";

export default createComponent({
  name: "App",
  components: {
    PropWatchDemo
  },
  setup: (props, context) => {
    const testValue = ref("initial");

    onMounted(() => {
      setTimeout(() => {
        console.log("Changing input prop value after 3s delay");
        testValue.value = "changed";
        // This value change does not trigger watchers?
      }, 3000);
    });

    return {
      testValue
    };
  }
});
</script>
<template>
  <select v-model="selected">
    <option value="null">null value</option>
    <option value>Empty value</option>
  </select>
</template>

<script>
import { createComponent, watch } from "@vue/composition-api";

export default createComponent({
  name: "MyInput",
  props: {
    selected: {
      type: [String, Number],
      required: true
    }
  },
  setup(props) {
    console.log("Setup props:", props);

    watch((first, second) => {
      console.log("Watch function called with args:", first, second);
      // First arg function registerCleanup, second is undefined
    });

    // watch(props, (first, second) => {
    //   console.log("Watch props function called with args:", first, second);
    //   // Logs error:
    //   // Failed watching path: "[object Object]" Watcher only accepts simple
    //   // dot-delimited paths. For full control, use a function instead.
    // })

    watch(props.selected, (first, second) => {
      console.log(
        "Watch props.selected function called with args:",
        first,
        second
      );
      // Both props are undefined so it's just a bare callback func to be run
    });

    return {};
  }
});
</script>

Selv om spørsmålet og kodeeksemplet mitt opprinnelig var i JavaScript, bruker jeg TypeScript. Tony Toms første svar, selv om det fungerte, førte til en typefeil. Dette ble løst av Michal Levýs svar. Så jeg har tagget dette spørsmålet med typescript etterpå.

Her er min polerte, men enkle versjon av de reaktive koblingene for denne tilpassede select-komponenten, basert på <b-form-select> fra bootstrap-vue (ellers agnostisk implementering, men denne underliggende komponenten sender både @input og @change hendelser, avhengig av om endringen ble gjort programmessig eller av brukerinteraksjon).

<template>
  <b-form-select
    v-model="selected"
    :options="{}"
    @input="handleSelection('input', $event)"
    @change="handleSelection('change', $event)"
  />
</template>

<script lang="ts">
import {
  createComponent, SetupContext, Ref, ref, watch, computed,
} from '@vue/composition-api';

interface Props {
  value?: string | number | boolean;
}

export default createComponent({
  name: 'CustomSelect',
  props: {
    value: {
      type: [String, Number, Boolean],
      required: false, // Accepts null and undefined as well
    },
  },
  setup(props: Props, context: SetupContext) {
    // Create a Ref from prop, as two-way binding is allowed only with sync -modifier,
    // with passing prop in parent and explicitly emitting update event on child:
    // Ref: https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
    // Ref: https://medium.com/@jithilmt/vue-js-2-two-way-data-binding-in-parent-and-child-components-1cd271c501ba
    const selected: Ref<Props['value']> = ref(props.value);

    const handleSelection = function emitUpdate(type: 'input' | 'change', value: Props['value']) {
      // For sync -modifier where 'value' is the prop name
      context.emit('update:value', value);
      // For @input and/or @change event propagation
      // @input emitted by the select component when value changed <programmatically>
      // @change AND @input both emitted on <user interaction>
      context.emit(type, value);
    };

    // Watch prop value change and assign to value 'selected' Ref
    watch(() => props.value, (newValue: Props['value']) => {
      selected.value = newValue;
    });

    return {
      selected,
      handleSelection,
    };
  },
});
</script>

Svar #1

Hvis du tar en titt på watch-typene her, blir det klart at det første argumentet til watch kan være en array, function eller Ref<T>

props som sendes til setup-funksjonen er et reaktivt objekt (sannsynligvis via readonly(reactive())), dets egenskaper er getters. Så det du gjør er å sende verdien av getteren som det første argumentet til watch, strengen "initial" i dette tilfellet. Fordi Vue 2 $watch-API-en brukes under panseret (og samme funksjon finnes i Vue 3), prøver du effektivt å overvåke en ikke-eksisterende egenskap med navnet "initial" på komponentinstansen din.

Tilbakesporingen din ble bare kalt én gang. Grunnen til at den ble kalt minst én gang, er fordi den nye watch-API-en oppfører seg som den nåværende $watch med immediate-alternativet (OPPDATERING 03/03/2021 - dette ble senere endret, og i utgivelsesversjonen av Vue 3 er watch like lat som den var i Vue 2)

Så ved et uhell gjør du det samme som Tony Tom foreslo, men med feil verdi. I begge tilfeller er det ikke gyldig kode når du bruker TypeScript.

Du kan gjøre dette i stedet:

watch(() => props.selected, (first, second) => {
      console.log(
        "Watch props.selected function called with args:",
        first,
        second
      );
    });

Her blir den første funksjonen utført umiddelbart av Vue for å samle avhengigheter (for å vite hva som skal utløse tilbakeringingen), og den andre funksjonen er selve tilbakeringingen.

En annen måte ville være å konvertere props-objektet ved å bruke toRefs, slik at egenskapene blir av typen Ref<T>, og du kan sende dem som det første argumentet til watch.

Men mesteparten av tiden er det ikke nødvendig å overvåke props. Bruk rett og slett props.xxx direkte i malen din (eller setup) og la Vue gjøre resten.

Svar #2

Jeg ville bare legge til noen flere detaljer til svaret ovenfor. Som Michal nevnte, er props som kommer et objekt og er reaktivt som helhet. Men hver nøkkel i props-objektet er ikke reaktiv på egenhånd.

Vi må justere watch-signaturen for en verdi i reactive-objektet sammenlignet med en ref-verdi

// watching value of a reactive object (watching a getter)

watch(() => props.selected, (selection, prevSelection) => { 
   /* ... */ 
})

Merk: Se kommentaren fra Michal Levý nedenfor før du bruker denne potensielt feilaktige koden:

// directly watching a value

const selected = ref(someValue)

watch(selected, (selection, prevSelection) => { 
   /* ... */ 
})

Bare litt mer informasjon selv om det ikke er det nevnte tilfellet i spørsmålet: Hvis vi vil overvåke flere egenskaper, kan man sende inn et array i stedet for en enkelt referanse

// Watching Multiple Sources

watch([ref1, ref2, ...], ([refVal1, refVal2, ...],[prevRef1, prevRef2, ...]) => { 
   /* ... */ 
})

Svar #3

Dette tar ikke for seg spørsmålet om hvordan man kan "observere" properties. Men hvis du vil vite hvordan du gjør props responsive med Vues Composition API, så les videre. I de fleste tilfeller bør du ikke måtte skrive en masse kode for å "observere" ting (med mindre du oppretter sideeffekter etter endringer).

Hemmeligheten er denne: Komponent props ER reaktiv. Så snart du får tilgang til en bestemt prop, er den IKKE reaktiv. Denne prosessen med å dele ut eller få tilgang til en del av et objekt kalles «destructuring». I den nye Composition API må du bli vant til å tenke på dette hele tiden - det er en nøkkeldel av avgjørelsen om å bruke reactive() vs ref().

Så det jeg foreslår (kode nedenfor) er at du tar egenskapen du trenger og gjør den til en ref hvis du vil bevare reaktivitet:

export default defineComponent({
  name: 'MyAwesomestComponent',
  props: {
    title: {
      type: String,
      required: true,
    },
    todos: {
      type: Array as PropType<Todo[]>,
      default: () => [],
    },
    ...
  },
  setup(props){ // this is important--pass the root props object in!!!
    ...
    // Now I need a reactive reference to my "todos" array...
    var todoRef = toRefs(props).todos
    ...
    // I can pass todoRef anywhere, with reactivity intact--changes from parents will flow automatically.
    // To access the "raw" value again:
    todoRef.value
    // Soon we'll have "unref" or "toRaw" or some official way to unwrap a ref object
    // But for now you can just access the magical ".value" attribute
  }
}

Jeg håper virkelig at Vue-ekspertene kan finne ut hvordan man gjør dette enklere... men så vidt jeg vet er dette typen kode vi må skrive med Composition API.

Her er en lenke til den offisielle dokumentasjonen, hvor de advarer deg direkte mot å dekonstruere props.

Relaterte artikler