The watch inside the component cannot accurately capture every change of the v-model:props
Vue version
3.2.45
Link to minimal reproduction
https://stackblitz.com/edit/vitejs-vite-cr1nh2?file=src/App.vue
Steps to reproduce
Press f12 to open console printing, then click the trigger button for the page
What is expected?
Expect to print aaa and bbb
What is actually happening?
We actually printed two aaa's
System Info
Binaries:
Node: 18.13.0 - C:\Program Files\nodejs\node.EXE
npm: 8.19.3 - C:\Program Files\nodejs\npm.CMD
Browsers:
Edge: Spartan (44.22621.1194.0), Chromium (110.0.1587.41)
Internet Explorer: 11.0.22621.1
npmPackages:
vue: ^3.2.45 => 3.2.45
Any additional comments?
发生这个的原因是我在设计一个通用的输入组件,通常我们需要向外面暴露一个focus方法来让输入组件聚焦,可是这种方式在使用的时候不够简洁,于是我设计了一个基于状态变更的聚焦功能,我使用 v-model:focus api来暴露focus功能,用户只需要定义一个状态 state.focus,需要的时候给状态设置true即可,而且在业务中的具体场景即便一个页面可能有多个该组件,但是在同一时刻一个只会显示其中的一个,所以我希望使用一个状态控制所有的组件聚焦而不是定义多个,这有利于代码的简洁。
设计以上的组件之后发现大多数时候工作的很好,但是直到我将组件应用于 el-dialog 中,他失效了,出现了很奇怪的监听结果,这可能是一个极端的情况。
<template>
<div>Test组件</div>
</template>
<script lang="ts" setup>
import { watch, nextTick, } from 'vue';
const props = defineProps(['foo', 'debugName']);
const emit = defineEmits(['update:foo']);
watch(
() => props.foo,
async () => {
console.log(props.debugName);
await nextTick();
emit('update:foo', false); // 这个是为了让外面的某个状态始终保持 false
}
);
</script>
You can solve this problem with nextTick
method. I'm not sure if this is a bug or not.
The reason is that by the time the second watcher's getter is run, props.foo
has been reset to false
already by the first watcher's callback.
The getter function (() => props.foo
) does run for the second watcher, but the callback won't be run because the getter's return values hasn't changed - it was and is false
.
You can a) use watchEffect, b) use { flush: 'sync' }
c) use nextTick
as described previously or d) change your logic to not rely on this flip-flopping value.
I'm interested in hearing other members' opinions, but it doesn't seem to me like a bug, just a tricky edge case.
First, it is normal to print 'aaa' twice, because the foo property is changed to true
by the button, and changed to false
by the watch
callback.
And 'bbb' is not printed, which I guess is due to the slot's update mechanism.
You can use nextTick
as mentioned in the comments above, or you can set the flush option of watch
to post
. But the number of times the callback is executed will always be twice
<template> <div>Test组件</div> </template> <script lang="ts" setup> import { watch, nextTick, } from 'vue'; const props = defineProps(['foo', 'debugName']); const emit = defineEmits(['update:foo']); watch( () => props.foo, async () => { console.log(props.debugName); await nextTick(); emit('update:foo', false); // 这个是为了让外面的某个状态始终保持 false } ); </script>
You can solve this problem with
nextTick
method. I'm not sure if this is a bug or not.↳
I've tried this, but it doesn't always seem to work, and I see the recommendation below to add the flush attribute, which seems to be working as expected so far
The reason is that by the time the second watcher's getter is run,
props.foo
has been reset tofalse
already by the first watcher's callback.↳The getter function (
() => props.foo
) does run for the second watcher, but the callback won't be run because the getter's return values hasn't changed - it was and isfalse
.↳You can a) use watchEffect, b) use
{ flush: 'sync' }
c) usenextTick
as described previously or d) change your logic to not rely on this flip-flopping value.↳I'm interested in hearing other members' opinions, but it doesn't seem to me like a bug, just a tricky edge case.↳
Thank you for your analysis of this problem, which is very enlightening to me
First, it is normal to print 'aaa' twice, because the foo property is changed to
true
by the button, and changed tofalse
by thewatch
callback.↳And 'bbb' is not printed, which I guess is due to the slot's update mechanism.↳
You can use
nextTick
as mentioned in the comments above, or you can set the flush option ofwatch
topost
. But the number of times the callback is executed will always be twice↳
Thanks for your answer, this method is currently working as expected