computed,
reactive,
effect,
- stop,
ref,
WritableComputedRef,
isReadonly
expect(dummy).toBe(undefined)
value.foo = 1
expect(dummy).toBe(1)
- stop(cValue.effect)
+ cValue.effect.stop()
value.foo = 2
expect(dummy).toBe(1)
})
it('should expose value when stopped', () => {
const x = computed(() => 1)
- stop(x.effect)
+ x.effect.stop()
expect(x.value).toBe(1)
})
})
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', () => {
})
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
},
// should not run yet
expect(dummy).toBe(1)
// manually run
- runner()
+ run()
// should have run
expect(dummy).toBe(2)
})
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
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',
expect(dummy).toBeUndefined()
expect(onTrigger).toHaveBeenCalledTimes(2)
expect(events[1]).toEqual({
- effect: runner,
+ effect: runner.effect,
target: toRaw(obj),
type: TriggerOpTypes.DELETE,
key: 'foo',
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)', () => {
-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'
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
}
// 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
}
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
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
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()
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
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!
// 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()
}
}
}
resetTracking,
ITERATE_KEY,
ReactiveEffect,
+ ReactiveEffectRunner,
ReactiveEffectOptions,
+ EffectScheduler,
DebuggerEvent
} from './effect'
export { TrackOpTypes, TriggerOpTypes } from './operations'
-import { effect, stop } from '@vue/reactivity'
import {
queueJob,
nextTick,
// 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)
})
})
import {
- effect,
- stop,
isRef,
Ref,
ComputedRef,
+ ReactiveEffect,
ReactiveEffectOptions,
isReactive,
- ReactiveFlags
+ ReactiveFlags,
+ EffectScheduler
} from '@vue/reactivity'
import { SchedulerJob, queuePreFlushCb } from './scheduler'
import {
let cleanup: () => void
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
- cleanup = runner.options.onStop = () => {
+ cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
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 ||
}
} else {
// watchEffect
- runner()
+ effect.run()
}
}
// 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') {
}
}
- 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)
}
}
}
import {
isReactive,
reactive,
- stop,
track,
TrackOpTypes,
trigger,
// stop effects
if (effects) {
for (let i = 0; i < effects.length; i++) {
- stop(effects[i])
+ effects[i].stop()
}
}
// unmounted hook
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>
*/
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
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,
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'
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
// 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()
ref.value = value
}
if (value) {
- ;(doSet as SchedulerCb).id = -1
+ ;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense)
} else {
doSet()
// 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 {
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
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 = (
unregisterHMR(instance)
}
- const { bum, effects, update, subTree, um } = instance
+ const { bum, effect, effects, update, subTree, um } = instance
// beforeUnmount hook
if (bum) {
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
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.
ownerInstance?: ComponentInternalInstance
}
-export type SchedulerCb = Function & { id?: number }
-export type SchedulerCbs = SchedulerCb | SchedulerCb[]
+export type SchedulerJobs = SchedulerJob | SchedulerJob[]
let isFlushing = false
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()
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,
}
function queueCb(
- cb: SchedulerCbs,
- activeQueue: SchedulerCb[] | null,
- pendingQueue: SchedulerCb[],
+ cb: SchedulerJobs,
+ activeQueue: SchedulerJob[] | null,
+ pendingQueue: SchedulerJob[],
index: number
) {
if (!isArray(cb)) {
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)
}
}
}
-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
}
}
-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${