Skip to content
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

feat(watcher): introduce 'final' flush mode #1599

Closed
wants to merge 1 commit into from
Closed

feat(watcher): introduce 'final' flush mode #1599

wants to merge 1 commit into from

Conversation

basvanmeurs
Copy link
Contributor

@basvanmeurs basvanmeurs commented Jul 16, 2020

In this PR the 'final' WatcherOptions flush mode is added.

'final' flush mode executes the watcher callbacks after all other effects (with multiple iterations).

Why is this necessary?

  it('multi-iteration reactivity: post', async () => {
    const count = ref(0)
    const count2 = ref(0)
    const count3 = ref(0)

    watchEffect(() => {
      count2.value = count.value * 2
    })

    watchEffect(() => {
      count3.value = count2.value * 2
    })

    const results: number[] = []
    const assertion = jest.fn(() => {
      const c1 = count.value
      const c2 = count2.value
      const c3 = count3.value
      results.push(c1 + 10 * c2 + 100 * c3)
    })

    watchEffect(
      () => {
        assertion()
      },
      {
        flush: 'post'
      }
    )

    expect(assertion).toHaveBeenCalledTimes(1)

    count.value++
    await nextTick()

    expect(results).toEqual([0, 21, 421])
    expect(assertion).toHaveBeenCalledTimes(3)
  })

assertion is executed double (3 times in total because of the initial call).

The reason is that value it watches may have inter-dependencies, triggering each other in multi-iteration flushPostQueue runs. In every iteration assertion may be added and invoked.

We have a real world use case. We have a component that creates some div elements with scrollbars. You can change the zoom value, which in turn changes the visibleRange using a watchEffects. Another part of the application watches zoom and visibleRangeX (and more) to finally draw stuff in a canvas. We noticed this draw function being called multiple times because of the issue described by @jods4. The draw function is very expensive so we don't want that to happen.

Although the recent commit 165068d (see #1595) mitigates the problem somewhat by checking if the effect was still on the queue (and it has not been run yet), it can still go wrong when watchers that have multi-level dependencies, or when the effect was already executed in the pending PostFlushCbs.

This PR addresses this problem by providing a way of postponing the effect until everything else has been processed. This allows you to make sure that an expensive watcher is run only once in multi-iteration flushes.

@yyx990803
Copy link
Member

First, I'd say this kind of chained watching is bad practice - you are relying on side effects to propagate the consistency of the state graph.

From your example, the only actual mutable state is zoom while visibleRange and all other state the depends on zoom are derived state, which by design should be modeled as computed properties:

const { ref, watchEffect, computed } = Vue

const count = ref(0)
const count2 = computed(() => count.value * 2)
const count3 = computed(() => count2.value * 2)

watchEffect(
  () => {
    const c1 = count.value
    const c2 = count2.value
    const c3 = count3.value
    // only runs once when count.value is mutated
  }
)

postFlushCb is for side effects and your problems roots in relying on side effects to stabilize the state graph - and when you do that, repeated invocation will sometimes be needed to reach stable state. Adding yet another flush queue doesn't fundamentally solve the problem. If you have multiple watchers using flush: 'final', you'd eventually run into repeated invocations in the final queue as well.

@yyx990803 yyx990803 closed this Jul 16, 2020
@basvanmeurs
Copy link
Contributor Author

Thanks for the suggestion, I didn't realise that they were handled differently than watchEffects in terms of scheduling.

I will try to convert my code and see if that works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants