【问题标题】:How to Watch Props Change with Vue Composition API / Vue 3?如何使用 Vue Composition API / Vue 3 观察道具变化?
【发布时间】:2020-03-26 07:04:39
【问题描述】:

虽然Vue Composition API RFC Reference site 有很多使用watch 模块的高级使用场景,但没有关于如何观看组件道具的示例?

Vue Composition API RFC's main pagevuejs/composition-api in Github 均未提及。

我创建了一个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>
<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>

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

EDIT2:这是我在 bootstrap-vue&lt;b-form-select&gt; 之上的这个自定义选择组件的反应布线的抛光但准系统版本(否则不可知论实现,但这个底层组件确实会发出 @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>

【问题讨论】:

  • 为什么你不能在你带入setup函数的props上使用watch?首先将它们变成`Refs,基本上制作一个响应式副本,它应该会在后续更改时触发。
  • 这不是正确的问题。我们不应该在 Vue 中观看道具!你不能像我们在 Vue 2 中那样解构 props 的事实似乎是倒退了一大步。请参阅“toRefs”(以及将来的“toRef”),了解如何避免这种为了设置另一个值而查看道具的反模式。

标签: javascript typescript vue.js vuejs3 vue-composition-api


【解决方案1】:

如下更改您的观看方法。

 watch("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
    });

【讨论】:

  • 不错的收获!然而,一个额外的错误,因为我正在使用 TypeScript - 关于这个的想法?我在 setup 函数上的 props 参数确实有一个自定义接口类型:'"selected"' 类型的参数不可分配给 'WatcherSource[]' 类型的参数。
  • 这没有意义。监视源不能是字符串。
  • @m4heshd 在 Vue 2 中,watch 参数可以是一个字符串(至少在 Options API 中)——在这种情况下,观察者位于与 Vue 实例(组件)同名的属性上字符串通过。所有的困惑都来自这样一个事实,即第一个版本的组合 API 是作为插件构建在 Vue 2 选项 API 之上的
  • @MichalLevý 我确实知道。但问题不在于 Vue 3 吗?
  • @m4heshd 是的,现在是这样,但是当我(和托尼)最初回答时,它是关于 Vue2 + 组合 API 插件(只需查看编辑历史)。您仍然可以在我的答案中看到它的残余...我猜需要更新它
【解决方案2】:

如果你看一下watch 输入here,很明显watch 的第一个参数可以是数组、函数或Ref&lt;T&gt;

props 传递给setup 函数是反应对象(可能由reactive() 制作),它的属性是getter。所以你所做的是将getter的值作为watch的第一个参数传递——在这种情况下是字符串“initial”。因为 Vue 2 $watch API 在后台使用(与 Vue 3 中的相同函数 exists),您实际上是在尝试在组件实例上查看名称为“initial”的不存在的属性。

您的回调只会被调用一次,并且不会再被调用。它至少被调用一次的原因是因为新的 watch API 的行为类似于当前的 $watchimmediate 选项(UPDATE 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
      );
    });

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

另一种方法是使用 toRefs 转换 props 对象,因此它的属性将是 Ref&lt;T&gt; 类型,您可以将它们作为 watch 的第一个参数传递

【讨论】:

  • 我已将第一个参数重命名为“newValue”。使用 'any' 键入有效,但使用特定类型时会出现类型错误:Argument of type '() => string |号码 |布尔值 | undefined' 不可分配给“WatcherSource[]”类型的参数。输入 '() => 字符串 |号码 |布尔值 | undefined' 缺少类型 'WatcherSource[]' 的以下属性:pop、push、concat、join 等 25 个。
  • 但是,使用索引访问类型我的道具类型接口没有给出错误:道具['selected']
  • 抱歉,我还不是 TypeScript 专家。您想要的是使用我的答案(function watch&lt;T&gt;)中引用的类型中的第二个定义。也许与 TS 解决方案共享沙箱?顺便说一句,你真的需要回调中的旧值吗?
  • 实际上第二个参数是未定义的,所以我不确定如何访问旧值......但在这种情况下不需要。我为我的问题添加了一个编辑,并为所有这些反应性接线提供了一个更精美的示例。
【解决方案3】:

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

ref 值相比,我们需要调整reactive 对象中的值的watch 签名

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

watch(() => props.selected, (selection, prevSelection) => { 
   /* ... */ 
})
// directly watching a ref

const selected = ref(props.selected)

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

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

// Watching Multiple Sources

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

【讨论】:

  • 再举一个例子:一起看 ref 和 reactive(包括 props):watch([ref, () =&gt; reactiveObj.property, () =&gt; prop.val], ([refNew, reactiveNew, propValNew], [refOld, reactiveOld, propValOld]) =&gt; { /* Code here */ });
【解决方案4】:

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

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

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

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

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

这是link to the official documentation,他们直接警告您不要破坏道具。

【讨论】:

  • 这是重新渲染模板所需要的吗? watch 的用法对我来说似乎没有意义,因为我没有任何副作用要做。
  • 太棒了。我疯了,我错过了文档'toRefs'......所以, const data = ref(props.data) 不是反应性的!
  • 最适合我的解决方案
【解决方案5】:

以上选项都不适合我,但我想我找到了一种简单的方法,似乎可以很好地在组合 api 中保持 vue2 编码风格

只需为 prop 创建一个 ref 别名,例如:

myPropAlias = ref(props.myProp)

你可以用别名做所有事情

对我来说就像一个魅力和最小的工作

【讨论】:

  • 要创建别名,您需要使用toRefstoRef。您创建的不是别名。它是新的ref,用 prop 的 当前值 初始化。当父母将道具更新为新值时,您的“别名”不会改变。在大多数情况下,这不是您想要的
猜你喜欢
  • 1970-01-01
  • 2021-09-26
  • 2021-08-30
  • 2021-08-15
  • 2018-01-15
  • 2020-06-16
  • 2019-09-11
  • 1970-01-01
  • 2021-10-10
相关资源
最近更新 更多