如何使用 Vue Composition API / Vue 3 观察道具变化?

ux.*_*eer 38 javascript typescript vue.js vuejs3 vue-composition-api

虽然Vue Composition API RFC 参考站点有很多watch模块的高级使用场景,但没有关于如何观看组件 props 的示例?

Vue Composition API RFC 的主页Github 中的 vuejs/composition-api 中也没有提到它。

我创建了一个Codesandbox来详细说明这个问题。

<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>
Run Code Online (Sandbox Code Playgroud)
<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 its just a bare callback func to be run
    });

    return {};
  }
});
</script>
Run Code Online (Sandbox Code Playgroud)

编辑:虽然我的问题和代码示例最初是使用 JavaScript,但我实际上使用的是 TypeScript。托尼汤姆的第一个答案虽然有效,但会导致类型错误。Michal Levý的回答解决了这个问题。所以我在typescript事后标记了这个问题。

EDIT2:这是我针对这个自定义选择组件的反应布线的完善但准系统版本,在<b-form-select>from之上bootstrap-vue (否则是不可知的实现,但这个底层组件确实发出 @input 和 @change 事件,基于更改是通过编程还是以编程方式进行的通过用户交互)

<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://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>
Run Code Online (Sandbox Code Playgroud)

Mic*_*evý 43

如果你看一下这里watch输入很明显第一个参数watch可以是数组、函数或Ref<T>

props传递给setup函数的是反应性对象(可能由reactive()),它的属性是吸气剂。因此,watch在这种情况下,您所做的是将 getter 的值作为- string "initial"的第一个参数传递。因为 Vue 2 $watch API是在幕后使用的(并且Vue 3 中存在相同的功能),所以您实际上是在尝试在组件实例上查看名称为“initial”的不存在的属性。

您的回调只会被调用一次,再也不会被调用。它至少被调用一次的原因是因为新watchAPI 的行为与当前$watchimmediate选项类似(更新 03/03/2021 - 这后来被更改,并且在 Vue 3 的发布版本中,watch与在 Vue 2 中一样懒惰)

所以偶然地你做了托尼汤姆建议的同样的事情,但价值错误。在这两种情况下,如果您使用的是 TypeScript,则它不是有效代码

你可以这样做:

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

这里第一个函数由 Vue 立即执行以收集依赖项(以了解应该触发回调的内容),第二个函数是回调本身。

其他方法是使用 props 对象转换,toRefs因此它的属性将是类型Ref<T>,您可以将它们作为第一个参数传递watch


Sya*_*lai 16

我只是想在上面的答案中添加更多细节。正如 Michal 所提到的,props未来是一个对象,整体上是反应性的。但是, props 对象中的每个键本身都不是响应式的。

与值相比,我们需要调整对象中watch某个值的签名reactiveref

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

watch(() => props.selected, (selection, prevSelection) => { 
   /* ... */ 
})
Run Code Online (Sandbox Code Playgroud)
// directly watching a ref

const selected = ref(props.selected)

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

Run Code Online (Sandbox Code Playgroud)

即使不是问题中提到的情况,也只是提供更多信息:如果我们想查看多个属性,可以传递一个数组而不是单个引用

// Watching Multiple Sources

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

Run Code Online (Sandbox Code Playgroud)

  • 我在这里有一种“似曾相识”的感觉,因为我想我之前已经写过这个评论了**但是**第二个例子是**非常危险和错误的**,因为它创建了新的“选定”引用并**初始化**它具有“props.selected”的当前值。问题是从这一点开始,“selected”引用与“props.selected”值完全断开(如果 prop 值发生变化,ref 不会发生变化)。在大多数情况下这不是您想要的。 (9认同)

Rob*_*ert 13

这并没有解决如何“观察”属性的问题。但是,如果您想知道如何使用 Vue 的 Composition API 使 props 具有响应性,请继续阅读。在大多数情况下,您不必编写一堆代码来“观察”事物(除非您在更改后产生副作用)。

秘诀在于:组件props是响应式的。一旦你访问一个特定的道具,它就不是反应性的。这种划分或访问对象的一部分的过程称为“解构”。在新的 Composition API 中,您需要习惯于一直考虑这个问题——这是决定使用reactive()vs的关键部分ref()

所以我的建议(下面的代码)是,ref如果您想保留反应性,您可以获取所需的属性并将其设为a :

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
  }
}
Run Code Online (Sandbox Code Playgroud)

我当然希望 Vue 向导能够弄清楚如何使这更容易......但据我所知,这是我们必须使用 Composition API 编写的代码类型。

这是官方文档链接,他们直接警告您不要破坏道具。