perf(reactivity): improve reactive effect memory usage (#4001)

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.
This commit is contained in:
Evan You 2021-06-24 17:44:32 -04:00
parent 63a51ffcab
commit 87f69fd0bb
12 changed files with 221 additions and 208 deletions

View File

@ -2,7 +2,6 @@ import {
computed, computed,
reactive, reactive,
effect, effect,
stop,
ref, ref,
WritableComputedRef, WritableComputedRef,
isReadonly isReadonly
@ -125,7 +124,7 @@ describe('reactivity/computed', () => {
expect(dummy).toBe(undefined) expect(dummy).toBe(undefined)
value.foo = 1 value.foo = 1
expect(dummy).toBe(1) expect(dummy).toBe(1)
stop(cValue.effect) cValue.effect.stop()
value.foo = 2 value.foo = 2
expect(dummy).toBe(1) expect(dummy).toBe(1)
}) })
@ -196,7 +195,7 @@ describe('reactivity/computed', () => {
it('should expose value when stopped', () => { it('should expose value when stopped', () => {
const x = computed(() => 1) const x = computed(() => 1)
stop(x.effect) x.effect.stop()
expect(x.value).toBe(1) expect(x.value).toBe(1)
}) })
}) })

View File

@ -494,7 +494,7 @@ describe('reactivity/effect', () => {
const runner = effect(() => {}) const runner = effect(() => {})
const otherRunner = effect(runner) const otherRunner = effect(runner)
expect(runner).not.toBe(otherRunner) 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', () => { it('should not run multiple times for a single mutation', () => {
@ -590,12 +590,13 @@ describe('reactivity/effect', () => {
}) })
it('scheduler', () => { it('scheduler', () => {
let runner: any, dummy let dummy
const scheduler = jest.fn(_runner => { let run: any
runner = _runner const scheduler = jest.fn(() => {
run = runner
}) })
const obj = reactive({ foo: 1 }) const obj = reactive({ foo: 1 })
effect( const runner = effect(
() => { () => {
dummy = obj.foo dummy = obj.foo
}, },
@ -609,7 +610,7 @@ describe('reactivity/effect', () => {
// should not run yet // should not run yet
expect(dummy).toBe(1) expect(dummy).toBe(1)
// manually run // manually run
runner() run()
// should have run // should have run
expect(dummy).toBe(2) expect(dummy).toBe(2)
}) })
@ -633,19 +634,19 @@ describe('reactivity/effect', () => {
expect(onTrack).toHaveBeenCalledTimes(3) expect(onTrack).toHaveBeenCalledTimes(3)
expect(events).toEqual([ expect(events).toEqual([
{ {
effect: runner, effect: runner.effect,
target: toRaw(obj), target: toRaw(obj),
type: TrackOpTypes.GET, type: TrackOpTypes.GET,
key: 'foo' key: 'foo'
}, },
{ {
effect: runner, effect: runner.effect,
target: toRaw(obj), target: toRaw(obj),
type: TrackOpTypes.HAS, type: TrackOpTypes.HAS,
key: 'bar' key: 'bar'
}, },
{ {
effect: runner, effect: runner.effect,
target: toRaw(obj), target: toRaw(obj),
type: TrackOpTypes.ITERATE, type: TrackOpTypes.ITERATE,
key: ITERATE_KEY key: ITERATE_KEY
@ -671,7 +672,7 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(2) expect(dummy).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1) expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toEqual({ expect(events[0]).toEqual({
effect: runner, effect: runner.effect,
target: toRaw(obj), target: toRaw(obj),
type: TriggerOpTypes.SET, type: TriggerOpTypes.SET,
key: 'foo', key: 'foo',
@ -684,7 +685,7 @@ describe('reactivity/effect', () => {
expect(dummy).toBeUndefined() expect(dummy).toBeUndefined()
expect(onTrigger).toHaveBeenCalledTimes(2) expect(onTrigger).toHaveBeenCalledTimes(2)
expect(events[1]).toEqual({ expect(events[1]).toEqual({
effect: runner, effect: runner.effect,
target: toRaw(obj), target: toRaw(obj),
type: TriggerOpTypes.DELETE, type: TriggerOpTypes.DELETE,
key: 'foo', key: 'foo',

View File

@ -382,7 +382,7 @@ describe('reactivity/readonly', () => {
const eff = effect(() => { const eff = effect(() => {
roArr.includes(2) 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)', () => { test('readonly should track and trigger if wrapping reactive original (collection)', () => {

View File

@ -1,4 +1,4 @@
import { effect, ReactiveEffect } from './effect' import { ReactiveEffect } from './effect'
import { Ref, trackRefValue, triggerRefValue } from './ref' import { Ref, trackRefValue, triggerRefValue } from './ref'
import { isFunction, NOOP } from '@vue/shared' import { isFunction, NOOP } from '@vue/shared'
import { ReactiveFlags, toRaw } from './reactive' import { ReactiveFlags, toRaw } from './reactive'
@ -35,16 +35,12 @@ class ComputedRefImpl<T> {
private readonly _setter: ComputedSetter<T>, private readonly _setter: ComputedSetter<T>,
isReadonly: boolean isReadonly: boolean
) { ) {
this.effect = effect(getter, { this.effect = new ReactiveEffect(getter, () => {
lazy: true, if (!this._dirty) {
scheduler: () => { this._dirty = true
if (!this._dirty) { triggerRefValue(this)
this._dirty = true
triggerRefValue(this)
}
} }
}) })
this[ReactiveFlags.IS_READONLY] = isReadonly 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 // the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this) const self = toRaw(this)
if (self._dirty) { if (self._dirty) {
self._value = this.effect() self._value = self.effect.run()!
self._dirty = false self._dirty = false
} }
trackRefValue(this) trackRefValue(self)
return self._value return self._value
} }

View File

@ -1,5 +1,5 @@
import { TrackOpTypes, TriggerOpTypes } from './operations' 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. // The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class // 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> type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>() const targetMap = new WeakMap<any, KeyToDepMap>()
export interface ReactiveEffect<T = any> { export type EffectScheduler = () => void
(): 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 DebuggerEvent = { export type DebuggerEvent = {
effect: ReactiveEffect effect: ReactiveEffect
@ -62,52 +29,34 @@ let activeEffect: ReactiveEffect | undefined
export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key 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 { // can be attached after creation
return fn && fn._isEffect === true onStop?: () => void
} // dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
export function effect<T = any>( constructor(
fn: () => T, public fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ public scheduler: EffectScheduler | null = null,
): ReactiveEffect<T> { // allow recursive self-invocation
if (isEffect(fn)) { public allowRecurse = false
fn = fn.raw ) {}
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
export function stop(effect: ReactiveEffect) { run() {
if (effect.active) { if (!this.active) {
cleanup(effect) return this.fn()
if (effect.options.onStop) {
effect.options.onStop()
} }
effect.active = false if (!effectStack.includes(this)) {
} this.cleanup()
}
let uid = 0
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try { try {
enableTracking() enableTracking()
effectStack.push(effect) effectStack.push((activeEffect = this))
activeEffect = effect return this.fn()
return fn()
} finally { } finally {
effectStack.pop() effectStack.pop()
resetTracking() resetTracking()
@ -115,25 +64,65 @@ function createReactiveEffect<T = any>(
activeEffect = n > 0 ? effectStack[n - 1] : undefined activeEffect = n > 0 ? effectStack[n - 1] : undefined
} }
} }
} as ReactiveEffect }
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse cleanup() {
effect._isEffect = true const { deps } = this
effect.active = true if (deps.length) {
effect.raw = fn for (let i = 0; i < deps.length; i++) {
effect.deps = [] deps[i].delete(this)
effect.options = options }
return effect deps.length = 0
}
}
stop() {
if (this.active) {
this.cleanup()
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
} }
function cleanup(effect: ReactiveEffect) { export interface ReactiveEffectOptions {
const { deps } = effect lazy?: boolean
if (deps.length) { scheduler?: EffectScheduler
for (let i = 0; i < deps.length; i++) { allowRecurse?: boolean
deps[i].delete(effect) onStop?: () => void
} onTrack?: (event: DebuggerEvent) => void
deps.length = 0 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 let shouldTrack = true
@ -185,8 +174,8 @@ export function trackEffects(
if (!dep.has(activeEffect!)) { if (!dep.has(activeEffect!)) {
dep.add(activeEffect!) dep.add(activeEffect!)
activeEffect!.deps.push(dep) activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.options.onTrack) { if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.options.onTrack( activeEffect!.onTrack(
Object.assign( Object.assign(
{ {
effect: activeEffect! effect: activeEffect!
@ -284,13 +273,13 @@ export function triggerEffects(
// spread into array for stabilization // spread into array for stabilization
for (const effect of [...dep]) { for (const effect of [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) { if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.options.onTrigger) { if (__DEV__ && effect.onTrigger) {
effect.options.onTrigger(extend({ effect }, debuggerEventExtraInfo)) effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
} }
if (effect.options.scheduler) { if (effect.scheduler) {
effect.options.scheduler(effect) effect.scheduler()
} else { } else {
effect() effect.run()
} }
} }
} }

View File

@ -46,7 +46,9 @@ export {
resetTracking, resetTracking,
ITERATE_KEY, ITERATE_KEY,
ReactiveEffect, ReactiveEffect,
ReactiveEffectRunner,
ReactiveEffectOptions, ReactiveEffectOptions,
EffectScheduler,
DebuggerEvent DebuggerEvent
} from './effect' } from './effect'
export { TrackOpTypes, TriggerOpTypes } from './operations' export { TrackOpTypes, TriggerOpTypes } from './operations'

View File

@ -1,4 +1,3 @@
import { effect, stop } from '@vue/reactivity'
import { import {
queueJob, queueJob,
nextTick, nextTick,
@ -576,20 +575,19 @@ describe('scheduler', () => {
// simulate parent component that toggles child // simulate parent component that toggles child
const job1 = () => { 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 // simulate child that's triggered by the same reactive change that
// triggers its toggle // triggers its toggle
const job2 = effect(() => spy()) const job2 = () => spy()
expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledTimes(0)
queueJob(job1) queueJob(job1)
queueJob(job2) queueJob(job2)
await nextTick() await nextTick()
// should not be called again // should not be called
expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledTimes(0)
}) })
}) })

View File

@ -1,12 +1,12 @@
import { import {
effect,
stop,
isRef, isRef,
Ref, Ref,
ComputedRef, ComputedRef,
ReactiveEffect,
ReactiveEffectOptions, ReactiveEffectOptions,
isReactive, isReactive,
ReactiveFlags ReactiveFlags,
EffectScheduler
} from '@vue/reactivity' } from '@vue/reactivity'
import { SchedulerJob, queuePreFlushCb } from './scheduler' import { SchedulerJob, queuePreFlushCb } from './scheduler'
import { import {
@ -244,7 +244,7 @@ function doWatch(
let cleanup: () => void let cleanup: () => void
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => { let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
cleanup = runner.options.onStop = () => { cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
} }
} }
@ -268,12 +268,12 @@ function doWatch(
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => { const job: SchedulerJob = () => {
if (!runner.active) { if (!effect.active) {
return return
} }
if (cb) { if (cb) {
// watch(source, cb) // watch(source, cb)
const newValue = runner() const newValue = effect.run()
if ( if (
deep || deep ||
forceTrigger || forceTrigger ||
@ -300,7 +300,7 @@ function doWatch(
} }
} else { } else {
// watchEffect // watchEffect
runner() effect.run()
} }
} }
@ -308,7 +308,7 @@ function doWatch(
// it is allowed to self-trigger (#1727) // it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb job.allowRecurse = !!cb
let scheduler: ReactiveEffectOptions['scheduler'] let scheduler: EffectScheduler
if (flush === 'sync') { if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') { } else if (flush === 'post') {
@ -326,32 +326,35 @@ function doWatch(
} }
} }
const runner = effect(getter, { const effect = new ReactiveEffect(getter, scheduler)
lazy: true,
onTrack,
onTrigger,
scheduler
})
recordInstanceBoundEffect(runner, instance) if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
recordInstanceBoundEffect(effect, instance)
// initial run // initial run
if (cb) { if (cb) {
if (immediate) { if (immediate) {
job() job()
} else { } else {
oldValue = runner() oldValue = effect.run()
} }
} else if (flush === 'post') { } else if (flush === 'post') {
queuePostRenderEffect(runner, instance && instance.suspense) queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else { } else {
runner() effect.run()
} }
return () => { return () => {
stop(runner) effect.stop()
if (instance) { if (instance) {
remove(instance.effects!, runner) remove(instance.effects!, effect)
} }
} }
} }

View File

@ -1,7 +1,6 @@
import { import {
isReactive, isReactive,
reactive, reactive,
stop,
track, track,
TrackOpTypes, TrackOpTypes,
trigger, trigger,
@ -575,7 +574,7 @@ function installCompatMount(
// stop effects // stop effects
if (effects) { if (effects) {
for (let i = 0; i < effects.length; i++) { for (let i = 0; i < effects.length; i++) {
stop(effects[i]) effects[i].stop()
} }
} }
// unmounted hook // unmounted hook

View File

@ -59,6 +59,7 @@ import { currentRenderingInstance } from './componentRenderContext'
import { startMeasure, endMeasure } from './profiling' import { startMeasure, endMeasure } from './profiling'
import { convertLegacyRenderFn } from './compat/renderFn' import { convertLegacyRenderFn } from './compat/renderFn'
import { globalCompatConfig, validateCompatConfig } from './compat/compatConfig' import { globalCompatConfig, validateCompatConfig } from './compat/compatConfig'
import { SchedulerJob } from './scheduler'
export type Data = Record<string, unknown> export type Data = Record<string, unknown>
@ -217,9 +218,14 @@ export interface ComponentInternalInstance {
*/ */
subTree: VNode subTree: VNode
/** /**
* The reactive effect for rendering and patching the component. Callable. * Main update effect
* @internal
*/ */
update: ReactiveEffect effect: ReactiveEffect
/**
* Bound effect runner to be passed to schedulers
*/
update: SchedulerJob
/** /**
* The render function that returns vdom tree. * The render function that returns vdom tree.
* @internal * @internal
@ -445,6 +451,7 @@ export function createComponentInstance(
root: null!, // to be immediately set root: null!, // to be immediately set
next: null, next: null,
subTree: null!, // will be set synchronously right after creation 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 update: null!, // will be set synchronously right after creation
render: null, render: null,
proxy: null, proxy: null,

View File

@ -48,15 +48,13 @@ import {
flushPostFlushCbs, flushPostFlushCbs,
invalidateJob, invalidateJob,
flushPreFlushCbs, flushPreFlushCbs,
SchedulerCb SchedulerJob
} from './scheduler' } from './scheduler'
import { import {
effect,
stop,
ReactiveEffectOptions,
isRef, isRef,
pauseTracking, pauseTracking,
resetTracking resetTracking,
ReactiveEffect
} from '@vue/reactivity' } from '@vue/reactivity'
import { updateProps } from './componentProps' import { updateProps } from './componentProps'
import { updateSlots } from './componentSlots' import { updateSlots } from './componentSlots'
@ -286,23 +284,6 @@ export const enum MoveType {
REORDER 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__ export const queuePostRenderEffect = __FEATURE_SUSPENSE__
? queueEffectWithSuspense ? queueEffectWithSuspense
: queuePostFlushCb : queuePostFlushCb
@ -378,7 +359,7 @@ export const setRef = (
// null values means this is unmount and it should not overwrite another // null values means this is unmount and it should not overwrite another
// ref with the same key // ref with the same key
if (value) { if (value) {
;(doSet as SchedulerCb).id = -1 ;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense) queuePostRenderEffect(doSet, parentSuspense)
} else { } else {
doSet() doSet()
@ -388,7 +369,7 @@ export const setRef = (
ref.value = value ref.value = value
} }
if (value) { if (value) {
;(doSet as SchedulerCb).id = -1 ;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense) queuePostRenderEffect(doSet, parentSuspense)
} else { } else {
doSet() doSet()
@ -1394,7 +1375,7 @@ function baseCreateRenderer(
// in case the child component is also queued, remove it to avoid // in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush. // double updating the same child component in the same flush.
invalidateJob(instance.update) invalidateJob(instance.update)
// instance.update is the reactive effect runner. // instance.update is the reactive effect.
instance.update() instance.update()
} }
} else { } else {
@ -1414,8 +1395,7 @@ function baseCreateRenderer(
isSVG, isSVG,
optimized optimized
) => { ) => {
// create reactive effect for rendering const componentUpdateFn = () => {
instance.update = effect(function componentEffect() {
if (!instance.isMounted) { if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode const { el, props } = initialVNode
@ -1639,12 +1619,33 @@ function baseCreateRenderer(
popWarningContext() 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__) { if (__DEV__) {
// @ts-ignore effect.onTrack = instance.rtc
instance.update.ownerInstance = instance ? 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 = ( const updateComponentPreRender = (
@ -2284,7 +2285,7 @@ function baseCreateRenderer(
unregisterHMR(instance) unregisterHMR(instance)
} }
const { bum, effects, update, subTree, um } = instance const { bum, effect, effects, update, subTree, um } = instance
// beforeUnmount hook // beforeUnmount hook
if (bum) { if (bum) {
@ -2299,13 +2300,15 @@ function baseCreateRenderer(
if (effects) { if (effects) {
for (let i = 0; i < effects.length; i++) { 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 // update may be null if a component is unmounted before its async
// setup has resolved. // setup has resolved.
if (update) { if (effect) {
stop(update) effect.stop()
// so that scheduler will no longer invoke it
update.active = false
unmount(subTree, instance, parentSuspense, doRemove) unmount(subTree, instance, parentSuspense, doRemove)
} }
// unmounted hook // unmounted hook

View File

@ -2,9 +2,26 @@ import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { ComponentInternalInstance, getComponentName } from './component' import { ComponentInternalInstance, getComponentName } from './component'
import { warn } from './warning' 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 * Attached by renderer.ts when setting up a component's render effect
* Used to obtain component information when reporting max recursive updates. * Used to obtain component information when reporting max recursive updates.
@ -13,8 +30,7 @@ export interface SchedulerJob extends Function, Partial<ReactiveEffect> {
ownerInstance?: ComponentInternalInstance ownerInstance?: ComponentInternalInstance
} }
export type SchedulerCb = Function & { id?: number } export type SchedulerJobs = SchedulerJob | SchedulerJob[]
export type SchedulerCbs = SchedulerCb | SchedulerCb[]
let isFlushing = false let isFlushing = false
let isFlushPending = false let isFlushPending = false
@ -22,12 +38,12 @@ let isFlushPending = false
const queue: SchedulerJob[] = [] const queue: SchedulerJob[] = []
let flushIndex = 0 let flushIndex = 0
const pendingPreFlushCbs: SchedulerCb[] = [] const pendingPreFlushCbs: SchedulerJob[] = []
let activePreFlushCbs: SchedulerCb[] | null = null let activePreFlushCbs: SchedulerJob[] | null = null
let preFlushIndex = 0 let preFlushIndex = 0
const pendingPostFlushCbs: SchedulerCb[] = [] const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerCb[] | null = null let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0 let postFlushIndex = 0
const resolvedPromise: Promise<any> = Promise.resolve() const resolvedPromise: Promise<any> = Promise.resolve()
@ -36,7 +52,7 @@ let currentFlushPromise: Promise<void> | null = null
let currentPreFlushParentJob: SchedulerJob | null = null let currentPreFlushParentJob: SchedulerJob | null = null
const RECURSION_LIMIT = 100 const RECURSION_LIMIT = 100
type CountMap = Map<SchedulerJob | SchedulerCb, number> type CountMap = Map<SchedulerJob, number>
export function nextTick<T = void>( export function nextTick<T = void>(
this: T, this: T,
@ -105,9 +121,9 @@ export function invalidateJob(job: SchedulerJob) {
} }
function queueCb( function queueCb(
cb: SchedulerCbs, cb: SchedulerJobs,
activeQueue: SchedulerCb[] | null, activeQueue: SchedulerJob[] | null,
pendingQueue: SchedulerCb[], pendingQueue: SchedulerJob[],
index: number index: number
) { ) {
if (!isArray(cb)) { if (!isArray(cb)) {
@ -129,11 +145,11 @@ function queueCb(
queueFlush() queueFlush()
} }
export function queuePreFlushCb(cb: SchedulerCb) { export function queuePreFlushCb(cb: SchedulerJob) {
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex) queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
} }
export function queuePostFlushCb(cb: SchedulerCbs) { export function queuePostFlushCb(cb: SchedulerJobs) {
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex) queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
} }
@ -205,8 +221,8 @@ export function flushPostFlushCbs(seen?: CountMap) {
} }
} }
const getId = (job: SchedulerJob | SchedulerCb) => const getId = (job: SchedulerJob): number =>
job.id == null ? Infinity : job.id job.id == null ? Infinity : job.id!
function flushJobs(seen?: CountMap) { function flushJobs(seen?: CountMap) {
isFlushPending = false 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)) { if (!seen.has(fn)) {
seen.set(fn, 1) seen.set(fn, 1)
} else { } else {
const count = seen.get(fn)! const count = seen.get(fn)!
if (count > RECURSION_LIMIT) { if (count > RECURSION_LIMIT) {
const instance = (fn as SchedulerJob).ownerInstance const instance = fn.ownerInstance
const componentName = instance && getComponentName(instance.type) const componentName = instance && getComponentName(instance.type)
warn( warn(
`Maximum recursive updates exceeded${ `Maximum recursive updates exceeded${