]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf(reactivity): improve reactive effect memory usage (#4001)
authorEvan You <yyx990803@gmail.com>
Thu, 24 Jun 2021 21:44:32 +0000 (17:44 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 16 Jul 2021 18:30:49 +0000 (14:30 -0400)
Based on #2345 , but with smaller API change

- Use class implementation for `ReactiveEffect`
- Switch internal creation of effects to use the class constructor
- Avoid options object allocation
- Avoid creating bound effect runner function (used in schedulers) when not necessary.
- Consumes ~17% less memory compared to last commit
- Introduces a very minor breaking change: the `scheduler` option passed to `effect` no longer receives the runner function.

12 files changed:
packages/reactivity/__tests__/computed.spec.ts
packages/reactivity/__tests__/effect.spec.ts
packages/reactivity/__tests__/readonly.spec.ts
packages/reactivity/src/computed.ts
packages/reactivity/src/effect.ts
packages/reactivity/src/index.ts
packages/runtime-core/__tests__/scheduler.spec.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/compat/global.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/scheduler.ts

index 0a9f9ecc8d1897d0d2cce417a37a7fe67bebd9e5..6811f1029e531398bab699b0afa60163a16cd6f1 100644 (file)
@@ -2,7 +2,6 @@ import {
   computed,
   reactive,
   effect,
-  stop,
   ref,
   WritableComputedRef,
   isReadonly
@@ -125,7 +124,7 @@ describe('reactivity/computed', () => {
     expect(dummy).toBe(undefined)
     value.foo = 1
     expect(dummy).toBe(1)
-    stop(cValue.effect)
+    cValue.effect.stop()
     value.foo = 2
     expect(dummy).toBe(1)
   })
@@ -196,7 +195,7 @@ describe('reactivity/computed', () => {
 
   it('should expose value when stopped', () => {
     const x = computed(() => 1)
-    stop(x.effect)
+    x.effect.stop()
     expect(x.value).toBe(1)
   })
 })
index e4a8f338c3e4538cc3810f57d437aeab6e3879c4..d458a17d8409a0945334c697c44b1c80a5e25612 100644 (file)
@@ -494,7 +494,7 @@ describe('reactivity/effect', () => {
     const runner = effect(() => {})
     const otherRunner = effect(runner)
     expect(runner).not.toBe(otherRunner)
-    expect(runner.raw).toBe(otherRunner.raw)
+    expect(runner.effect.fn).toBe(otherRunner.effect.fn)
   })
 
   it('should not run multiple times for a single mutation', () => {
@@ -590,12 +590,13 @@ describe('reactivity/effect', () => {
   })
 
   it('scheduler', () => {
-    let runner: any, dummy
-    const scheduler = jest.fn(_runner => {
-      runner = _runner
+    let dummy
+    let run: any
+    const scheduler = jest.fn(() => {
+      run = runner
     })
     const obj = reactive({ foo: 1 })
-    effect(
+    const runner = effect(
       () => {
         dummy = obj.foo
       },
@@ -609,7 +610,7 @@ describe('reactivity/effect', () => {
     // should not run yet
     expect(dummy).toBe(1)
     // manually run
-    runner()
+    run()
     // should have run
     expect(dummy).toBe(2)
   })
@@ -633,19 +634,19 @@ describe('reactivity/effect', () => {
     expect(onTrack).toHaveBeenCalledTimes(3)
     expect(events).toEqual([
       {
-        effect: runner,
+        effect: runner.effect,
         target: toRaw(obj),
         type: TrackOpTypes.GET,
         key: 'foo'
       },
       {
-        effect: runner,
+        effect: runner.effect,
         target: toRaw(obj),
         type: TrackOpTypes.HAS,
         key: 'bar'
       },
       {
-        effect: runner,
+        effect: runner.effect,
         target: toRaw(obj),
         type: TrackOpTypes.ITERATE,
         key: ITERATE_KEY
@@ -671,7 +672,7 @@ describe('reactivity/effect', () => {
     expect(dummy).toBe(2)
     expect(onTrigger).toHaveBeenCalledTimes(1)
     expect(events[0]).toEqual({
-      effect: runner,
+      effect: runner.effect,
       target: toRaw(obj),
       type: TriggerOpTypes.SET,
       key: 'foo',
@@ -684,7 +685,7 @@ describe('reactivity/effect', () => {
     expect(dummy).toBeUndefined()
     expect(onTrigger).toHaveBeenCalledTimes(2)
     expect(events[1]).toEqual({
-      effect: runner,
+      effect: runner.effect,
       target: toRaw(obj),
       type: TriggerOpTypes.DELETE,
       key: 'foo',
index 80115b2645d120fefd7add7ff200408c2506d8ba..c8bf65b3876c191ed06febb9b944579891158019 100644 (file)
@@ -382,7 +382,7 @@ describe('reactivity/readonly', () => {
     const eff = effect(() => {
       roArr.includes(2)
     })
-    expect(eff.deps.length).toBe(0)
+    expect(eff.effect.deps.length).toBe(0)
   })
 
   test('readonly should track and trigger if wrapping reactive original (collection)', () => {
index 965ff3eec6bb1928eae057224af9b667d988bfa3..396e50ecdb068d52b3f28d096e4d484fa92ed6e7 100644 (file)
@@ -1,4 +1,4 @@
-import { effect, ReactiveEffect } from './effect'
+import { ReactiveEffect } from './effect'
 import { Ref, trackRefValue, triggerRefValue } from './ref'
 import { isFunction, NOOP } from '@vue/shared'
 import { ReactiveFlags, toRaw } from './reactive'
@@ -35,16 +35,12 @@ class ComputedRefImpl<T> {
     private readonly _setter: ComputedSetter<T>,
     isReadonly: boolean
   ) {
-    this.effect = effect(getter, {
-      lazy: true,
-      scheduler: () => {
-        if (!this._dirty) {
-          this._dirty = true
-          triggerRefValue(this)
-        }
+    this.effect = new ReactiveEffect(getter, () => {
+      if (!this._dirty) {
+        this._dirty = true
+        triggerRefValue(this)
       }
     })
-
     this[ReactiveFlags.IS_READONLY] = isReadonly
   }
 
@@ -52,10 +48,10 @@ class ComputedRefImpl<T> {
     // the computed ref may get wrapped by other proxies e.g. readonly() #3376
     const self = toRaw(this)
     if (self._dirty) {
-      self._value = this.effect()
+      self._value = self.effect.run()!
       self._dirty = false
     }
-    trackRefValue(this)
+    trackRefValue(self)
     return self._value
   }
 
index df74769e14b4e1b85fd401336811c16a4f315cbb..d2733e93cd6ddabe5d09e84f88090d7243998d68 100644 (file)
@@ -1,5 +1,5 @@
 import { TrackOpTypes, TriggerOpTypes } from './operations'
-import { EMPTY_OBJ, extend, isArray, isIntegerKey, isMap } from '@vue/shared'
+import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
 
 // The main WeakMap that stores {target -> key -> dep} connections.
 // Conceptually, it's easier to think of a dependency as a Dep class
@@ -9,40 +9,7 @@ type Dep = Set<ReactiveEffect>
 type KeyToDepMap = Map<any, Dep>
 const targetMap = new WeakMap<any, KeyToDepMap>()
 
-export interface ReactiveEffect<T = any> {
-  (): T
-  _isEffect: true
-  id: number
-  active: boolean
-  raw: () => T
-  deps: Array<Dep>
-  options: ReactiveEffectOptions
-  allowRecurse: boolean
-}
-
-export interface ReactiveEffectOptions {
-  lazy?: boolean
-  scheduler?: (job: ReactiveEffect) => void
-  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
-}
+export type EffectScheduler = () => void
 
 export type DebuggerEvent = {
   effect: ReactiveEffect
@@ -62,52 +29,34 @@ let activeEffect: ReactiveEffect | undefined
 
 export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
 export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
+export class ReactiveEffect<T = any> {
+  active = true
+  deps: Dep[] = []
 
-export function isEffect(fn: any): fn is ReactiveEffect {
-  return fn && fn._isEffect === true
-}
-
-export function effect<T = any>(
-  fn: () => T,
-  options: ReactiveEffectOptions = EMPTY_OBJ
-): ReactiveEffect<T> {
-  if (isEffect(fn)) {
-    fn = fn.raw
-  }
-  const effect = createReactiveEffect(fn, options)
-  if (!options.lazy) {
-    effect()
-  }
-  return effect
-}
-
-export function stop(effect: ReactiveEffect) {
-  if (effect.active) {
-    cleanup(effect)
-    if (effect.options.onStop) {
-      effect.options.onStop()
-    }
-    effect.active = false
-  }
-}
+  // can be attached after creation
+  onStop?: () => void
+  // dev only
+  onTrack?: (event: DebuggerEvent) => void
+  // dev only
+  onTrigger?: (event: DebuggerEvent) => void
 
-let uid = 0
+  constructor(
+    public fn: () => T,
+    public scheduler: EffectScheduler | null = null,
+    // allow recursive self-invocation
+    public allowRecurse = false
+  ) {}
 
-function createReactiveEffect<T = any>(
-  fn: () => T,
-  options: ReactiveEffectOptions
-): ReactiveEffect<T> {
-  const effect = function reactiveEffect(): unknown {
-    if (!effect.active) {
-      return fn()
+  run() {
+    if (!this.active) {
+      return this.fn()
     }
-    if (!effectStack.includes(effect)) {
-      cleanup(effect)
+    if (!effectStack.includes(this)) {
+      this.cleanup()
       try {
         enableTracking()
-        effectStack.push(effect)
-        activeEffect = effect
-        return fn()
+        effectStack.push((activeEffect = this))
+        return this.fn()
       } finally {
         effectStack.pop()
         resetTracking()
@@ -115,25 +64,65 @@ function createReactiveEffect<T = any>(
         activeEffect = n > 0 ? effectStack[n - 1] : undefined
       }
     }
-  } as ReactiveEffect
-  effect.id = uid++
-  effect.allowRecurse = !!options.allowRecurse
-  effect._isEffect = true
-  effect.active = true
-  effect.raw = fn
-  effect.deps = []
-  effect.options = options
-  return effect
-}
+  }
 
-function cleanup(effect: ReactiveEffect) {
-  const { deps } = effect
-  if (deps.length) {
-    for (let i = 0; i < deps.length; i++) {
-      deps[i].delete(effect)
+  cleanup() {
+    const { deps } = this
+    if (deps.length) {
+      for (let i = 0; i < deps.length; i++) {
+        deps[i].delete(this)
+      }
+      deps.length = 0
     }
-    deps.length = 0
   }
+
+  stop() {
+    if (this.active) {
+      this.cleanup()
+      if (this.onStop) {
+        this.onStop()
+      }
+      this.active = false
+    }
+  }
+}
+
+export interface ReactiveEffectOptions {
+  lazy?: boolean
+  scheduler?: EffectScheduler
+  allowRecurse?: boolean
+  onStop?: () => void
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
+}
+
+export interface ReactiveEffectRunner<T = any> {
+  (): T
+  effect: ReactiveEffect
+}
+
+export function effect<T = any>(
+  fn: () => T,
+  options?: ReactiveEffectOptions
+): ReactiveEffectRunner {
+  if ((fn as ReactiveEffectRunner).effect) {
+    fn = (fn as ReactiveEffectRunner).effect.fn
+  }
+
+  const _effect = new ReactiveEffect(fn)
+  if (options) {
+    extend(_effect, options)
+  }
+  if (!options || !options.lazy) {
+    _effect.run()
+  }
+  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
+  runner.effect = _effect
+  return runner
+}
+
+export function stop(runner: ReactiveEffectRunner) {
+  runner.effect.stop()
 }
 
 let shouldTrack = true
@@ -185,8 +174,8 @@ export function trackEffects(
   if (!dep.has(activeEffect!)) {
     dep.add(activeEffect!)
     activeEffect!.deps.push(dep)
-    if (__DEV__ && activeEffect!.options.onTrack) {
-      activeEffect!.options.onTrack(
+    if (__DEV__ && activeEffect!.onTrack) {
+      activeEffect!.onTrack(
         Object.assign(
           {
             effect: activeEffect!
@@ -284,13 +273,13 @@ export function triggerEffects(
   // spread into array for stabilization
   for (const effect of [...dep]) {
     if (effect !== activeEffect || effect.allowRecurse) {
-      if (__DEV__ && effect.options.onTrigger) {
-        effect.options.onTrigger(extend({ effect }, debuggerEventExtraInfo))
+      if (__DEV__ && effect.onTrigger) {
+        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
       }
-      if (effect.options.scheduler) {
-        effect.options.scheduler(effect)
+      if (effect.scheduler) {
+        effect.scheduler()
       } else {
-        effect()
+        effect.run()
       }
     }
   }
index 240410141e457dc6edd0a224e236bc782efebe27..e392f182439d08b4d639c538c7907e5cdd28ebcb 100644 (file)
@@ -46,7 +46,9 @@ export {
   resetTracking,
   ITERATE_KEY,
   ReactiveEffect,
+  ReactiveEffectRunner,
   ReactiveEffectOptions,
+  EffectScheduler,
   DebuggerEvent
 } from './effect'
 export { TrackOpTypes, TriggerOpTypes } from './operations'
index 5c68633441660f6908ecb6f2caa25b50ab088967..92727e9953782fecb3ac632a714acff22200c4da 100644 (file)
@@ -1,4 +1,3 @@
-import { effect, stop } from '@vue/reactivity'
 import {
   queueJob,
   nextTick,
@@ -576,20 +575,19 @@ describe('scheduler', () => {
 
     // simulate parent component that toggles child
     const job1 = () => {
-      stop(job2)
+      // @ts-ignore
+      job2.active = false
     }
-    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)
+    const job2 = () => spy()
+    expect(spy).toHaveBeenCalledTimes(0)
 
     queueJob(job1)
     queueJob(job2)
     await nextTick()
 
-    // should not be called again
-    expect(spy).toHaveBeenCalledTimes(1)
+    // should not be called
+    expect(spy).toHaveBeenCalledTimes(0)
   })
 })
index 2abc4e1447ff585982986b7189eda537d9f17a7e..f589ab62951e8aacff6f9c0821bce5717a72975f 100644 (file)
@@ -1,12 +1,12 @@
 import {
-  effect,
-  stop,
   isRef,
   Ref,
   ComputedRef,
+  ReactiveEffect,
   ReactiveEffectOptions,
   isReactive,
-  ReactiveFlags
+  ReactiveFlags,
+  EffectScheduler
 } from '@vue/reactivity'
 import { SchedulerJob, queuePreFlushCb } from './scheduler'
 import {
@@ -244,7 +244,7 @@ function doWatch(
 
   let cleanup: () => void
   let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
-    cleanup = runner.options.onStop = () => {
+    cleanup = effect.onStop = () => {
       callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
     }
   }
@@ -268,12 +268,12 @@ function doWatch(
 
   let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
   const job: SchedulerJob = () => {
-    if (!runner.active) {
+    if (!effect.active) {
       return
     }
     if (cb) {
       // watch(source, cb)
-      const newValue = runner()
+      const newValue = effect.run()
       if (
         deep ||
         forceTrigger ||
@@ -300,7 +300,7 @@ function doWatch(
       }
     } else {
       // watchEffect
-      runner()
+      effect.run()
     }
   }
 
@@ -308,7 +308,7 @@ function doWatch(
   // it is allowed to self-trigger (#1727)
   job.allowRecurse = !!cb
 
-  let scheduler: ReactiveEffectOptions['scheduler']
+  let scheduler: EffectScheduler
   if (flush === 'sync') {
     scheduler = job as any // the scheduler function gets called directly
   } else if (flush === 'post') {
@@ -326,32 +326,35 @@ function doWatch(
     }
   }
 
-  const runner = effect(getter, {
-    lazy: true,
-    onTrack,
-    onTrigger,
-    scheduler
-  })
+  const effect = new ReactiveEffect(getter, scheduler)
 
-  recordInstanceBoundEffect(runner, instance)
+  if (__DEV__) {
+    effect.onTrack = onTrack
+    effect.onTrigger = onTrigger
+  }
+
+  recordInstanceBoundEffect(effect, instance)
 
   // initial run
   if (cb) {
     if (immediate) {
       job()
     } else {
-      oldValue = runner()
+      oldValue = effect.run()
     }
   } else if (flush === 'post') {
-    queuePostRenderEffect(runner, instance && instance.suspense)
+    queuePostRenderEffect(
+      effect.run.bind(effect),
+      instance && instance.suspense
+    )
   } else {
-    runner()
+    effect.run()
   }
 
   return () => {
-    stop(runner)
+    effect.stop()
     if (instance) {
-      remove(instance.effects!, runner)
+      remove(instance.effects!, effect)
     }
   }
 }
index 7823cc6a7e13641b94a29f720c642b83f98efb32..f843b5ead140a1252f24a7037cf0f249df015399 100644 (file)
@@ -1,7 +1,6 @@
 import {
   isReactive,
   reactive,
-  stop,
   track,
   TrackOpTypes,
   trigger,
@@ -575,7 +574,7 @@ function installCompatMount(
         // stop effects
         if (effects) {
           for (let i = 0; i < effects.length; i++) {
-            stop(effects[i])
+            effects[i].stop()
           }
         }
         // unmounted hook
index 172e24332eae32663fe68739993c59a73f6e1226..4da57d4436842127278c08d96fa2d2f3fa96fce8 100644 (file)
@@ -59,6 +59,7 @@ import { currentRenderingInstance } from './componentRenderContext'
 import { startMeasure, endMeasure } from './profiling'
 import { convertLegacyRenderFn } from './compat/renderFn'
 import { globalCompatConfig, validateCompatConfig } from './compat/compatConfig'
+import { SchedulerJob } from './scheduler'
 
 export type Data = Record<string, unknown>
 
@@ -217,9 +218,14 @@ export interface ComponentInternalInstance {
    */
   subTree: VNode
   /**
-   * The reactive effect for rendering and patching the component. Callable.
+   * Main update effect
+   * @internal
+   */
+  effect: ReactiveEffect
+  /**
+   * Bound effect runner to be passed to schedulers
    */
-  update: ReactiveEffect
+  update: SchedulerJob
   /**
    * The render function that returns vdom tree.
    * @internal
@@ -445,6 +451,7 @@ export function createComponentInstance(
     root: null!, // to be immediately set
     next: null,
     subTree: null!, // will be set synchronously right after creation
+    effect: null!, // will be set synchronously right after creation
     update: null!, // will be set synchronously right after creation
     render: null,
     proxy: null,
index 545b5561c7cf456c0e6b943a23d2b74b199baa28..5ecee3edd4c07f448a8c3fa34cb86fd2a77f7fa0 100644 (file)
@@ -48,15 +48,13 @@ import {
   flushPostFlushCbs,
   invalidateJob,
   flushPreFlushCbs,
-  SchedulerCb
+  SchedulerJob
 } from './scheduler'
 import {
-  effect,
-  stop,
-  ReactiveEffectOptions,
   isRef,
   pauseTracking,
-  resetTracking
+  resetTracking,
+  ReactiveEffect
 } from '@vue/reactivity'
 import { updateProps } from './componentProps'
 import { updateSlots } from './componentSlots'
@@ -286,23 +284,6 @@ export const enum MoveType {
   REORDER
 }
 
-const prodEffectOptions = {
-  scheduler: queueJob,
-  // #1801, #2043 component render effects should allow recursive updates
-  allowRecurse: true
-}
-
-function createDevEffectOptions(
-  instance: ComponentInternalInstance
-): ReactiveEffectOptions {
-  return {
-    scheduler: queueJob,
-    allowRecurse: true,
-    onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0,
-    onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0
-  }
-}
-
 export const queuePostRenderEffect = __FEATURE_SUSPENSE__
   ? queueEffectWithSuspense
   : queuePostFlushCb
@@ -378,7 +359,7 @@ export const setRef = (
     // null values means this is unmount and it should not overwrite another
     // ref with the same key
     if (value) {
-      ;(doSet as SchedulerCb).id = -1
+      ;(doSet as SchedulerJob).id = -1
       queuePostRenderEffect(doSet, parentSuspense)
     } else {
       doSet()
@@ -388,7 +369,7 @@ export const setRef = (
       ref.value = value
     }
     if (value) {
-      ;(doSet as SchedulerCb).id = -1
+      ;(doSet as SchedulerJob).id = -1
       queuePostRenderEffect(doSet, parentSuspense)
     } else {
       doSet()
@@ -1394,7 +1375,7 @@ function baseCreateRenderer(
         // in case the child component is also queued, remove it to avoid
         // double updating the same child component in the same flush.
         invalidateJob(instance.update)
-        // instance.update is the reactive effect runner.
+        // instance.update is the reactive effect.
         instance.update()
       }
     } else {
@@ -1414,8 +1395,7 @@ function baseCreateRenderer(
     isSVG,
     optimized
   ) => {
-    // create reactive effect for rendering
-    instance.update = effect(function componentEffect() {
+    const componentUpdateFn = () => {
       if (!instance.isMounted) {
         let vnodeHook: VNodeHook | null | undefined
         const { el, props } = initialVNode
@@ -1639,12 +1619,33 @@ function baseCreateRenderer(
           popWarningContext()
         }
       }
-    }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
+    }
+
+    // create reactive effect for rendering
+    const effect = (instance.effect = new ReactiveEffect(
+      componentUpdateFn,
+      () => queueJob(instance.update),
+      true /* allowRecurse */
+    ))
+
+    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
+    update.id = instance.uid
+    // allowRecurse
+    // #1801, #2043 component render effects should allow recursive updates
+    update.allowRecurse = true
 
     if (__DEV__) {
-      // @ts-ignore
-      instance.update.ownerInstance = instance
+      effect.onTrack = instance.rtc
+        ? e => invokeArrayFns(instance.rtc!, e)
+        : void 0
+      effect.onTrigger = instance.rtg
+        ? e => invokeArrayFns(instance.rtg!, e)
+        : void 0
+      // @ts-ignore (for scheduler)
+      update.ownerInstance = instance
     }
+
+    update()
   }
 
   const updateComponentPreRender = (
@@ -2284,7 +2285,7 @@ function baseCreateRenderer(
       unregisterHMR(instance)
     }
 
-    const { bum, effects, update, subTree, um } = instance
+    const { bum, effect, effects, update, subTree, um } = instance
 
     // beforeUnmount hook
     if (bum) {
@@ -2299,13 +2300,15 @@ function baseCreateRenderer(
 
     if (effects) {
       for (let i = 0; i < effects.length; i++) {
-        stop(effects[i])
+        effects[i].stop()
       }
     }
     // update may be null if a component is unmounted before its async
     // setup has resolved.
-    if (update) {
-      stop(update)
+    if (effect) {
+      effect.stop()
+      // so that scheduler will no longer invoke it
+      update.active = false
       unmount(subTree, instance, parentSuspense, doRemove)
     }
     // unmounted hook
index 2dcb84973e808df597c306105e39c144b0a2da7e..3c79a3be210ca0c823fbbe963a0544086aad2f6c 100644 (file)
@@ -2,9 +2,26 @@ import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import { isArray } from '@vue/shared'
 import { ComponentInternalInstance, getComponentName } from './component'
 import { warn } from './warning'
-import { ReactiveEffect } from '@vue/reactivity'
 
-export interface SchedulerJob extends Function, Partial<ReactiveEffect> {
+export interface SchedulerJob extends Function {
+  id?: number
+  active?: boolean
+  /**
+   * Indicates whether the effect 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
   /**
    * Attached by renderer.ts when setting up a component's render effect
    * Used to obtain component information when reporting max recursive updates.
@@ -13,8 +30,7 @@ export interface SchedulerJob extends Function, Partial<ReactiveEffect> {
   ownerInstance?: ComponentInternalInstance
 }
 
-export type SchedulerCb = Function & { id?: number }
-export type SchedulerCbs = SchedulerCb | SchedulerCb[]
+export type SchedulerJobs = SchedulerJob | SchedulerJob[]
 
 let isFlushing = false
 let isFlushPending = false
@@ -22,12 +38,12 @@ let isFlushPending = false
 const queue: SchedulerJob[] = []
 let flushIndex = 0
 
-const pendingPreFlushCbs: SchedulerCb[] = []
-let activePreFlushCbs: SchedulerCb[] | null = null
+const pendingPreFlushCbs: SchedulerJob[] = []
+let activePreFlushCbs: SchedulerJob[] | null = null
 let preFlushIndex = 0
 
-const pendingPostFlushCbs: SchedulerCb[] = []
-let activePostFlushCbs: SchedulerCb[] | null = null
+const pendingPostFlushCbs: SchedulerJob[] = []
+let activePostFlushCbs: SchedulerJob[] | null = null
 let postFlushIndex = 0
 
 const resolvedPromise: Promise<any> = Promise.resolve()
@@ -36,7 +52,7 @@ let currentFlushPromise: Promise<void> | null = null
 let currentPreFlushParentJob: SchedulerJob | null = null
 
 const RECURSION_LIMIT = 100
-type CountMap = Map<SchedulerJob | SchedulerCb, number>
+type CountMap = Map<SchedulerJob, number>
 
 export function nextTick<T = void>(
   this: T,
@@ -105,9 +121,9 @@ export function invalidateJob(job: SchedulerJob) {
 }
 
 function queueCb(
-  cb: SchedulerCbs,
-  activeQueue: SchedulerCb[] | null,
-  pendingQueue: SchedulerCb[],
+  cb: SchedulerJobs,
+  activeQueue: SchedulerJob[] | null,
+  pendingQueue: SchedulerJob[],
   index: number
 ) {
   if (!isArray(cb)) {
@@ -129,11 +145,11 @@ function queueCb(
   queueFlush()
 }
 
-export function queuePreFlushCb(cb: SchedulerCb) {
+export function queuePreFlushCb(cb: SchedulerJob) {
   queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
 }
 
-export function queuePostFlushCb(cb: SchedulerCbs) {
+export function queuePostFlushCb(cb: SchedulerJobs) {
   queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
 }
 
@@ -205,8 +221,8 @@ export function flushPostFlushCbs(seen?: CountMap) {
   }
 }
 
-const getId = (job: SchedulerJob | SchedulerCb) =>
-  job.id == null ? Infinity : job.id
+const getId = (job: SchedulerJob): number =>
+  job.id == null ? Infinity : job.id!
 
 function flushJobs(seen?: CountMap) {
   isFlushPending = false
@@ -256,13 +272,13 @@ function flushJobs(seen?: CountMap) {
   }
 }
 
-function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob | SchedulerCb) {
+function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
   if (!seen.has(fn)) {
     seen.set(fn, 1)
   } else {
     const count = seen.get(fn)!
     if (count > RECURSION_LIMIT) {
-      const instance = (fn as SchedulerJob).ownerInstance
+      const instance = fn.ownerInstance
       const componentName = instance && getComponentName(instance.type)
       warn(
         `Maximum recursive updates exceeded${