如何使用 Vue Composition API / Vue 3 观察道具变化?
How to Watch Props Change with Vue Composition API / Vue 3?
Vue Composition API RFC Reference site虽然watch
模块有很多高级的使用场景,但是如何看组件道具?[=21=没有例子]
Vue Composition API RFC's main page or vuejs/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:这是我在 <b-form-select>
来自 [=17] 的 <b-form-select>
之上的这个自定义 select 组件的电抗接线的抛光但准系统版本=] (否则是不可知的实现,但这个底层组件确实会发出 @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>
按照以下方式更改您的观看方式。
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
});
如果你看一下 watch
输入 here 很明显 watch
的第一个参数可以是数组、函数或 Ref<T>
传递给 setup
函数的 props
是反应对象(可能由 Vue 3 中的 reactive()
), it's properties are getters. So what you doing is passing the value of the getter as the 1st argument of watch
- string "initial" in this case. Because Vue 2 $watch
API is used under the hood (and same function exists 创建),您实际上是在尝试观察不存在的 属性在您的组件实例上命名为“initial”。
您的回调只会被调用一次,不会再被调用。它至少被调用一次的原因是因为新 watch
API 的行为与当前 $watch
具有 immediate
选项(UPDATE 03/03/2021 - 后来更改了,在 Vue 3 的发行版中,watch
与 Vue 2 中的惰性方式相同)
所以你不小心做了 Tony Tom 建议的同样事情,但值错误。在这两种情况下,如果您使用的是 TypeScript
,它都不是有效代码
您可以这样做:
watch(() => props.selected, (first, second) => {
console.log(
"Watch props.selected function called with args:",
first,
second
);
});
这里第一个函数由 Vue 立即执行以收集依赖项(知道什么应该触发回调),第二个函数是回调本身。
其他方法是使用 toRefs
转换 props 对象,因此它的属性将是 Ref<T>
类型,您可以将它们作为 watch
[=28= 的第一个参数传递]
我只是想为上面的答案添加更多细节。正如 Michal 提到的,props
coming 是一个对象,并且作为一个整体是反应性的。但是,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, ...]) => {
/* ... */
})
这没有解决如何 "watch" 属性的问题。但是,如果您想知道如何使用 Vue 的 Composition API 使道具响应,请继续阅读。在大多数情况下,您不必为 "watch" 事物编写一堆代码(除非您在更改后产生副作用)。
秘诀在于:组件 props
是反应性的。一旦你访问了一个特定的道具,它就不是反应性的。这种划分或访问对象的一部分的过程称为"destructuring"。在新的 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 向导能够弄清楚如何使这更容易......但据我所知,这是我们必须使用组合编写的代码类型 API。
这是一个 link to the official documentation,他们在其中直接警告您不要破坏 props。
None 上面的选项对我有用,但我认为我找到了一种似乎非常有效的简单方法,可以在组合中保持 vue2 编码风格 api
只需为道具创建一个 ref
别名,例如:
myPropAlias = ref(props.myProp)
然后你用别名做所有事情
对我来说很有魅力而且极简
Vue Composition API RFC Reference site虽然watch
模块有很多高级的使用场景,但是如何看组件道具?[=21=没有例子]
Vue Composition API RFC's main page or vuejs/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:这是我在 <b-form-select>
来自 [=17] 的 <b-form-select>
之上的这个自定义 select 组件的电抗接线的抛光但准系统版本=] (否则是不可知的实现,但这个底层组件确实会发出 @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>
按照以下方式更改您的观看方式。
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
});
如果你看一下 watch
输入 here 很明显 watch
的第一个参数可以是数组、函数或 Ref<T>
setup
函数的 props
是反应对象(可能由 Vue 3 中的 reactive()
), it's properties are getters. So what you doing is passing the value of the getter as the 1st argument of watch
- string "initial" in this case. Because Vue 2 $watch
API is used under the hood (and same function exists 创建),您实际上是在尝试观察不存在的 属性在您的组件实例上命名为“initial”。
您的回调只会被调用一次,不会再被调用。它至少被调用一次的原因是因为新 watch
API 的行为与当前 $watch
具有 immediate
选项(UPDATE 03/03/2021 - 后来更改了,在 Vue 3 的发行版中,watch
与 Vue 2 中的惰性方式相同)
所以你不小心做了 Tony Tom 建议的同样事情,但值错误。在这两种情况下,如果您使用的是 TypeScript
,它都不是有效代码您可以这样做:
watch(() => props.selected, (first, second) => {
console.log(
"Watch props.selected function called with args:",
first,
second
);
});
这里第一个函数由 Vue 立即执行以收集依赖项(知道什么应该触发回调),第二个函数是回调本身。
其他方法是使用 toRefs
转换 props 对象,因此它的属性将是 Ref<T>
类型,您可以将它们作为 watch
[=28= 的第一个参数传递]
我只是想为上面的答案添加更多细节。正如 Michal 提到的,props
coming 是一个对象,并且作为一个整体是反应性的。但是,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, ...]) => {
/* ... */
})
这没有解决如何 "watch" 属性的问题。但是,如果您想知道如何使用 Vue 的 Composition API 使道具响应,请继续阅读。在大多数情况下,您不必为 "watch" 事物编写一堆代码(除非您在更改后产生副作用)。
秘诀在于:组件 props
是反应性的。一旦你访问了一个特定的道具,它就不是反应性的。这种划分或访问对象的一部分的过程称为"destructuring"。在新的 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 向导能够弄清楚如何使这更容易......但据我所知,这是我们必须使用组合编写的代码类型 API。
这是一个 link to the official documentation,他们在其中直接警告您不要破坏 props。
None 上面的选项对我有用,但我认为我找到了一种似乎非常有效的简单方法,可以在组合中保持 vue2 编码风格 api
只需为道具创建一个 ref
别名,例如:
myPropAlias = ref(props.myProp)
然后你用别名做所有事情
对我来说很有魅力而且极简