]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(reactivity): ensure computed always expose value
authorEvan You <yyx990803@gmail.com>
Fri, 28 May 2021 00:53:21 +0000 (20:53 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 28 May 2021 00:53:21 +0000 (20:53 -0400)
fix #3099

Also changes the original fix for #910 by moving the fix from
reactivity to the scheduler

packages/reactivity/__tests__/computed.spec.ts
packages/reactivity/__tests__/effect.spec.ts
packages/reactivity/src/effect.ts
packages/runtime-core/__tests__/scheduler.spec.ts
packages/runtime-core/src/scheduler.ts

index 9d60ad352c41d20b547fc9d3367596156e483ac9..0a9f9ecc8d1897d0d2cce417a37a7fe67bebd9e5 100644 (file)
@@ -193,4 +193,10 @@ describe('reactivity/computed', () => {
     expect(isReadonly(z)).toBe(false)
     expect(isReadonly(z.value.a)).toBe(false)
   })
+
+  it('should expose value when stopped', () => {
+    const x = computed(() => 1)
+    stop(x.effect)
+    expect(x.value).toBe(1)
+  })
 })
index 589c69195bf53e9d96f61be27d6e9dc75f956087..e4a8f338c3e4538cc3810f57d437aeab6e3879c4 100644 (file)
@@ -709,28 +709,6 @@ describe('reactivity/effect', () => {
     expect(dummy).toBe(3)
   })
 
-  it('stop with scheduler', () => {
-    let dummy
-    const obj = reactive({ prop: 1 })
-    const queue: (() => void)[] = []
-    const runner = effect(
-      () => {
-        dummy = obj.prop
-      },
-      {
-        scheduler: e => queue.push(e)
-      }
-    )
-    obj.prop = 2
-    expect(dummy).toBe(1)
-    expect(queue.length).toBe(1)
-    stop(runner)
-
-    // a scheduled effect should not execute anymore after stopped
-    queue.forEach(e => e())
-    expect(dummy).toBe(1)
-  })
-
   it('events: onStop', () => {
     const onStop = jest.fn()
     const runner = effect(() => {}, {
index 683f8fa9e3cfc23ea9deb65e43c4c2a85cddda0a..0e7adb6076c20235e1b4132d9f69441b2b91de16 100644 (file)
@@ -26,6 +26,21 @@ export interface ReactiveEffectOptions {
   onTrack?: (event: DebuggerEvent) => void
   onTrigger?: (event: DebuggerEvent) => void
   onStop?: () => void
+  /**
+   * Indicates whether the job is allowed to recursively trigger itself when
+   * managed by the scheduler.
+   *
+   * By default, a job cannot trigger itself because some built-in method calls,
+   * e.g. Array.prototype.push actually performs reads as well (#1740) which
+   * can lead to confusing infinite loops.
+   * The allowed cases are component update functions and watch callbacks.
+   * Component update functions may update child component props, which in turn
+   * trigger flush: "pre" watch callbacks that mutates state that the parent
+   * relies on (#1801). Watch callbacks doesn't track its dependencies so if it
+   * triggers itself again, it's likely intentional and it is the user's
+   * responsibility to perform recursive state mutation that eventually
+   * stabilizes (#1727).
+   */
   allowRecurse?: boolean
 }
 
@@ -84,7 +99,7 @@ function createReactiveEffect<T = any>(
 ): ReactiveEffect<T> {
   const effect = function reactiveEffect(): unknown {
     if (!effect.active) {
-      return options.scheduler ? undefined : fn()
+      return fn()
     }
     if (!effectStack.includes(effect)) {
       cleanup(effect)
index 953f928158db21a88270b948708679fa289e723a..5c68633441660f6908ecb6f2caa25b50ab088967 100644 (file)
@@ -1,3 +1,4 @@
+import { effect, stop } from '@vue/reactivity'
 import {
   queueJob,
   nextTick,
@@ -568,4 +569,27 @@ describe('scheduler', () => {
     await nextTick()
     expect(count).toBe(1)
   })
+
+  // #910
+  test('should not run stopped reactive effects', async () => {
+    const spy = jest.fn()
+
+    // simulate parent component that toggles child
+    const job1 = () => {
+      stop(job2)
+    }
+    job1.id = 0 // need the id to ensure job1 is sorted before job2
+
+    // simulate child that's triggered by the same reactive change that
+    // triggers its toggle
+    const job2 = effect(() => spy())
+    expect(spy).toHaveBeenCalledTimes(1)
+
+    queueJob(job1)
+    queueJob(job2)
+    await nextTick()
+
+    // should not be called again
+    expect(spy).toHaveBeenCalledTimes(1)
+  })
 })
index 5729afbbec43025ac72fcedef560f41c8c2acd17..adaf3c26e841aebf5ad3b3ae625efb7ab4a6899d 100644 (file)
@@ -3,27 +3,14 @@ import { isArray } from '@vue/shared'
 import { ComponentPublicInstance } from './componentPublicInstance'
 import { ComponentInternalInstance, getComponentName } from './component'
 import { warn } from './warning'
+import { ReactiveEffect } from '@vue/reactivity'
 
-export interface SchedulerJob {
-  (): void
+export interface SchedulerJob extends Function, Partial<ReactiveEffect> {
   /**
-   * unique job id, only present on raw effects, e.g. component render effect
+   * Attached by renderer.ts when setting up a component's render effect
+   * Used to obtain component information when reporting max recursive updates.
+   * dev only.
    */
-  id?: number
-  /**
-   * Indicates whether the job is allowed to recursively trigger itself.
-   * By default, a job cannot trigger itself because some built-in method calls,
-   * e.g. Array.prototype.push actually performs reads as well (#1740) which
-   * can lead to confusing infinite loops.
-   * The allowed cases are component update functions and watch callbacks.
-   * Component update functions may update child component props, which in turn
-   * trigger flush: "pre" watch callbacks that mutates state that the parent
-   * relies on (#1801). Watch callbacks doesn't track its dependencies so if it
-   * triggers itself again, it's likely intentional and it is the user's
-   * responsibility to perform recursive state mutation that eventually
-   * stabilizes (#1727).
-   */
-  allowRecurse?: boolean
   ownerInstance?: ComponentInternalInstance
 }
 
@@ -243,7 +230,7 @@ function flushJobs(seen?: CountMap) {
   try {
     for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
       const job = queue[flushIndex]
-      if (job) {
+      if (job && job.active !== false) {
         if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
           continue
         }