Subscribe on changes!

The watch inside the component cannot accurately capture every change of the v-model:props

avatar
Feb 13th 2023

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 中,他失效了,出现了很奇怪的监听结果,这可能是一个极端的情况。

avatar
Feb 13th 2023
<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.

avatar
Feb 13th 2023

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.

avatar
Feb 13th 2023

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

avatar
Feb 15th 2023
<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

avatar
Feb 15th 2023

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

Thank you for your analysis of this problem, which is very enlightening to me

avatar
Feb 15th 2023

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↳

Thanks for your answer, this method is currently working as expected