-
-
Notifications
You must be signed in to change notification settings - Fork 8.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rendering updates occur before watch handlers #1706
Comments
This is intended and expected behavior.
Your understanding is exactly the opposite of the intended design: watchers are intended for performing non-state side effects like operating on the DOM, so it defaults to post flush so that if a state change triggered both a render update and a watcher, the watcher would be accessing the already updated DOM by default. Mutating state in a watcher is an anti-pattern and you should be using computed properties to build up your reactive state graph instead of relying on watchers to "chain" state changes together. I don't understand your data loading example.
Again the opposite: the expected state used for rendering should be the exact after direct mutation and before any watcher is run. If you mutate state inside a watcher callback then it will trigger another update. It's never inconsistent. All of this boils down to your pattern of relying on watchers watching some state to mutate other state - which you should refactor into computed properties or explicitly use pre-flush watchers. |
@yyx990803 My apologies. In my attempt to be concise I failed to explain myself clearly. My test case was only designed to illustrate the problem, it certainly wasn't a compelling example of correct I will try to be clearer this time.
I'll attempt to illustrate this using the Hacker News Clone Example. Specifically, this file: The relevant parts for my explanation are: export default {
data: () => ({
loading: true
}),
computed: {
item () {
return this.$store.state.items[this.$route.params.id]
}
},
beforeMount () {
this.fetchComments()
},
watch: {
item: 'fetchComments'
},
methods: {
fetchComments () {
if (!this.item || !this.item.kids) {
return
}
this.loading = true
fetchComments(this.$store, this.item).then(() => {
this.loading = false
})
}
}
} In Vue 2 the following steps would occur when
In Vue 3 things would be a little different:
In this simple application the extra rendering run doesn't cause any serious problems. However...
The example of using Vue 2: https://jsfiddle.net/skirtle/3txfq02z/6/ When a single character is typed in the box the Vue 2 example performs a single update. The Vue 3 example performs two updates. The key lines from the code are: watch: {
// whenever question changes, this function will run
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
}, The first thing it does is assign a new value to There are dozens of examples in the Vuetify source. A quick search for Sure, there are also cases where Vuetify uses The timing change for There are several examples of That file also features some entertaining uses of To the best of my knowledge the design decisions that led to these timing changes are not documented anywhere. If they are I would love to see that. Currently the only justification I have for the new If the final decision is that the trade-offs are worth it then I feel it needs to be properly documented so that everyone can appreciate what has changed and why. |
BREAKING CHANGE: watch APIs now default to use `flush: 'pre'` instead of `flush: 'post'`. - This change affects `watch`, `watchEffect`, the `watch` component option, and `this.$watch`. - As pointed out by @skirtles-code in [this comment](#1706 (comment)), Vue 2's watch behavior is pre-flush, and the ecosystem has many uses of watch that assumes the pre-flush behavior. Defaulting to post-flush can result in unnecessary re-renders without the users being aware of it. - With this change, watchers need to specify `{ flush: 'post' }` via options to trigger callback after Vue render updates. Note that specifying `{ flush: 'post' }` will also defer `watchEffect`'s initial run to wait for the component's initial render.
@skirtles-code great breakdown! |
Version
3.0.0-rc.4
Reproduction link
https://jsfiddle.net/skirtle/rd4y1xj3/9/
Steps to reproduce
debugger
statement.watch
.What is expected?
In Vue 2 the
watch
would run prior to rendering updates. See:https://jsfiddle.net/skirtle/je5qdrLp/4/
What is actually happening?
The
watch
runs after rendering, triggering a second phase of rendering.This appears to be because the default value for
flush
is'post'
. However, that seems like a strange default as awatch
(when used via the options API) will generally make some kind of data change that will require further rendering updates.In particular, consider the case where
watch
is used to load data asynchronously. Typically that will involve setting some sort ofisLoading
flag to show a load mask while the data is loading. With Vue 2 that would have triggered a single rendering update but with Vue 3 RC4 it will go through two lots of rendering.Worse, as the
watch
hasn't run yet the template could be trying to render inconsistent data.With Vue 3 the
updated
hook will also run twice. However, thewatch
handler is called between the DOM update and the call to theupdated
hook. This leads to a potentially confusing scenario where the DOM doesn't reflect what is in the data whenupdated
is called. It seems counter-intuitive for awatch
to run between a DOM update and the correspondingupdated
hook.The text was updated successfully, but these errors were encountered: