Mutation inside onBeforeUpdate causes sideEffect delayed for 1 tick, how can I watch for el change?
Version
3.0.11
Reproduction link
https://github.com/mannok/vue-rel-el-test.git
Steps to reproduce
- go to the repository
- clone the project
- npm run dev
- open the project home page and open console tab
- click add one 5a. watch console
- click add one 6a. watch console
What is expected?
what you can see in console: 5a. before update there are 5 div 6a. before update there are 6 div
What is actually happening?
what you can see in console: 5a. before update 6a. there are 5 div before update
seems that the side effects caused in onBeforeUpdate
have been marked. However, these side effects have not been flushed after the update cycle. How can I watch for el
change? Thanks!
watch() and watchEffect() effects are run before the DOM is mounted or updated so the template ref hasn't been updated when the watcher runs the effect.
see https://v3.vuejs.org/guide/composition-api-template-refs.html#watching-template-refs
watch(
() => vm.divs,
(to) => {
console.log(`there are ${to.length} div`);
},{
flush:'post'
}
);
Thanks @edison1105, but I think it is not only related to template ref. The case is that "mutations' sideEffect inside onBeforeUpdate
delayed for 1 tick". No matter the case is ref
or not, mutations in onBeforeUpdate
delayed. Is this by design?
Please take the following code to test.
<template>
<label>{{ counter }}</label>
<button @click="(evt) => counter++">add one</button>
</template>
<script lang="ts">
import { ref, computed, onBeforeMount, onMounted, reactive, onBeforeUpdate, watch } from 'vue';
export default {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setup(): any {
const vm = reactive({
counter: 0,
updatedTime: undefined as number | undefined
});
onBeforeMount(() => {
watch(
() => vm.updatedTime,
(to) => {
console.log(`last update time: ${vm.updatedTime}`);
}
);
});
onMounted(() => {});
onBeforeUpdate(() => {
console.log('before update');
vm.updatedTime = Date.now();
});
return vm;
}
};
</script>
<style scoped lang="scss"></style>
I want to say that it has something to do with the execution timing of the watch
- flush: 'pre': this is the default, in the scheduler and the effect will be called synchronously before the component mount.
- flush: 'sync': out of the scheduler and the effect will be called synchronously
- flush: 'post': in the scheduler and the effect will be called asynchronously after the component update
so the watch callback did not trigger the first time because vm.updatedTime
not updated yet.
Thanks @edison1105 , OK I get this now. However, as the documents mentioned that
watch() and watchEffect() effects are run before the DOM is mounted or updated
using the following codes:
<template>
<ul>
<li :ref="(el) => (divs[i] = el)" v-for="(item, i) in lists" :key="item">
{{ item }}
</li>
</ul>
<button @click="(evt) => lists.push(lists.length)">add one</button>
</template>
<script lang="ts">
import { ref, computed, onBeforeMount, onMounted, reactive, onBeforeUpdate, watch } from 'vue';
export default {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setup(): any {
const vm = (window.vm = reactive({
lists: [0, 1, 2, 3],
divs: []
}));
onBeforeMount(() => {
watch(
() => vm.divs.length,
(to) => {
console.log(`there are ${to} div`);
}
);
});
onMounted(() => {});
onBeforeUpdate(() => {
console.log('before update');
vm.divs = [];
});
return vm;
}
};
</script>
<style scoped lang="scss"></style>
Supposed that the watcher's side effect would only be fired once before mount
, but the console shows me 4 lines of console log at the beginning of the page which were executed from the same watcher, before that, I didn't make any mutation.
i.e.
there are 1 div
there are 2 div
there are 3 div
there are 4 div
this is expected.
because :ref="(el) => (divs[i] = el)"
will be called 4 times in setRef
during patch
.
In other words, the watch callback will be called synchronously 4 times.
and if change to this:
watch(
() => vm.divs.length,
(to) => {
console.log(`there are ${to} div`);
},
{flush:'post'}
);
I believe it will only output there are 4 div
.
@edison1105 How come the watch callback will be called synchronously? I have tried to expose the vm and executed the following in console:
vm.divs.push('a');
vm.divs.push('b');
vm.divs.push('c');
vm.divs.push('d');
It only output there are 8 div
once. How come the mutation during patch
is synchronize and for other case, it is async?
they are different.
executing vm.divs.push('a');
in the console will trigger the update logic of the component. before start update the component, the flushPreFlushCbs
will be executed, and the jobs in the queue will be deduplicated. So only there are 8 div
is output.
If you are interested, you can add a breakpoint in the watch callback for debugging, and you will clearly see the entire process.
Re:
Supposed that the watcher's side effect would only be fired once before mount, but the console shows me 4 lines of console log at the beginning of the page which were executed from the same watcher, before that, I didn't make any mutation.
there are 1 div
there are 2 div
there are 3 div
there are 4 div
First of all, you should always use flush: 'post'
to detect template ref changes.
- Answer why you console output four times:
When doing the initial rendering, there is no so-called "flush queue". So a watch with pre
option must be called synchronously, and this behavior is equivalent to having the sync
option, this means it will not be deduplicated, so the console output four times. This only happens when you change state in the template, e.g. :ref="(el) => (divs[i] = el)"
.
- Answer why the console only outputs once for subsequent state changes:
Because there is a flush queue at this time, and we can do de-duplication.
Again, you should always use flush: 'post'
to detect template ref changes.