Website-Suche

javascript - Wie man Änderungen von Props mit der Vue Composition API/Vue 3 überwacht?


Während die Vue Composition API RFC-Referenzseite viele fortgeschrittene Anwendungsfälle mit dem watch-Modul bietet, gibt es keine Beispiele dafür, wie man Komponenten-Props überwacht.

Es wird weder auf der Hauptseite des Vue Composition API RFC noch auf vuejs/composition-api auf Github erwähnt.

Ich habe einen Codesandbox erstellt, um dieses Problem näher zu erläutern.

<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>

Obwohl meine Frage und das Codebeispiel ursprünglich mit JavaScript erstellt wurden, verwende ich TypeScript. Die erste Antwort von Tony Tom funktionierte zwar, führte jedoch zu einem Tippfehler. Dies wurde durch die Antwort von Michal Levý gelöst. Daher habe ich diese Frage anschließend mit typescript markiert.

Hier ist meine ausgefeilte, aber dennoch grundlegende Version der reaktiven Verdrahtungen für diese benutzerdefinierte Select-Komponente, basierend auf <b-form-select> von bootstrap-vue (ansonsten agnostische Implementierung, aber diese zugrunde liegende Komponente sendet sowohl @input- als auch @change-Ereignisse, abhängig davon, ob die Änderung programmatisch oder durch Benutzerinteraktion vorgenommen wurde).

<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>

Antwort #1

Wenn Sie sich die watch-Typen hier ansehen, wird deutlich, dass das erste Argument von watch ein array, eine function oder ein Ref<T> sein könnte.

props, die an die setup-Funktion übergeben werden, sind ein reaktives Objekt (wahrscheinlich durch readonly(reactive())), seine Eigenschaften sind Getter. Was Sie also tun, ist, den Wert des Getters als erstes Argument von watch zu übergeben, in diesem Fall den String „initial“. Da unter der Haube die Vue 2 $watch-API verwendet wird (und dieselbe Funktion in Vue 3 existiert), versuchen Sie effektiv, eine nicht existierende Eigenschaft mit dem Namen „initial“ in Ihrer Komponenteninstanz zu beobachten.

Ihr Callback wurde nur einmal aufgerufen. Der Grund, warum es mindestens einmal aufgerufen wurde, ist, dass die neue watch-API sich wie das aktuelle $watch mit der immediate-Option verhält (UPDATE 03/03/2021 - dies wurde später geändert, und in der Release-Version von Vue 3 ist watch genauso faul wie in Vue 2)

Also machst du aus Versehen dasselbe, was Tony Tom vorgeschlagen hat, aber mit dem falschen Wert. In beiden Fällen ist der Code nicht gültig, wenn du TypeScript verwendest.

Du kannst stattdessen Folgendes tun:

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

Hier wird die erste Funktion sofort von Vue ausgeführt, um Abhängigkeiten zu sammeln (um zu wissen, was den Rückruf auslösen sollte), und die zweite Funktion ist der Rückruf selbst.

Eine andere Möglichkeit wäre, das Props-Objekt mit toRefs zu konvertieren, sodass seine Eigenschaften vom Typ Ref<T> sind und Sie sie als erstes Argument von watch übergeben können.

Die meiste Zeit ist es jedoch nicht nötig, Requisiten zu beobachten. Verwenden Sie einfach props.xxx direkt in Ihrer Vorlage (oder setup) und lassen Sie Vue den Rest erledigen.

Antwort #2

Ich wollte nur einige weitere Details zur obigen Antwort hinzufügen. Wie Michal erwähnt hat, ist das props, das kommt, ein Objekt und als Ganzes reaktiv. Aber jeder Schlüssel im Props-Objekt ist für sich genommen nicht reaktiv.

Wir müssen die watch-Signatur für einen Wert im reactive-Objekt im Vergleich zu einem ref-Wert anpassen

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

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

Hinweis: Sehen Sie sich den Kommentar von Michal Levý unten an, bevor Sie möglicherweise diesen fehlerhaften Code verwenden:

// directly watching a value

const selected = ref(someValue)

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

Nur noch ein paar weitere Informationen, auch wenn es nicht der im Fragebeispiel erwähnte Fall ist: Wenn wir mehrere Eigenschaften überwachen wollen, kann man statt einer einzelnen Referenz ein Array übergeben.

// Watching Multiple Sources

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

Antwort #3

Dies geht nicht auf die Frage ein, wie man Eigenschaften „beobachtet“. Aber wenn Sie wissen möchten, wie man Props mit der Composition API von Vue reaktiv macht, dann lesen Sie weiter. In den meisten Fällen müssen Sie keinen Haufen Code schreiben, um Dinge „zu beobachten“ (es sei denn, Sie erzeugen nach Änderungen Nebeneffekte).

Das Geheimnis ist folgendes: Die Komponenteneigenschaften props SIND reaktiv. Sobald Sie auf eine bestimmte Eigenschaft zugreifen, ist sie NICHT reaktiv. Dieser Prozess, einen Teil eines Objekts herauszugreifen oder darauf zuzugreifen, wird als „Destructuring“ bezeichnet. In der neuen Composition API müssen Sie sich daran gewöhnen, ständig darüber nachzudenken - es ist ein wesentlicher Teil der Entscheidung, reactive() gegenüber ref() zu verwenden.

Also schlage ich vor (Code unten), dass Sie die Eigenschaft, die Sie benötigen, nehmen und sie zu einem ref machen, wenn Sie die Reaktivität bewahren möchten:

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
  }
}

Ich hoffe wirklich, dass die Vue-Zauberer herausfinden können, wie man das einfacher machen kann... aber soweit ich weiß, ist dies die Art von Code, die wir mit der Composition API schreiben müssen.

Hier ist ein Link zur offiziellen Dokumentation, in der sie Sie direkt davor warnen, Props zu destrukturieren.

Verwandte Artikel