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:
parent
63a51ffcab
commit
87f69fd0bb
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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',
|
||||||
|
@ -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)', () => {
|
||||||
|
@ -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,
|
|
||||||
scheduler: () => {
|
|
||||||
if (!this._dirty) {
|
if (!this._dirty) {
|
||||||
this._dirty = true
|
this._dirty = true
|
||||||
triggerRefValue(this)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
effect._isEffect = true
|
|
||||||
effect.active = true
|
|
||||||
effect.raw = fn
|
|
||||||
effect.deps = []
|
|
||||||
effect.options = options
|
|
||||||
return effect
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanup(effect: ReactiveEffect) {
|
cleanup() {
|
||||||
const { deps } = effect
|
const { deps } = this
|
||||||
if (deps.length) {
|
if (deps.length) {
|
||||||
for (let i = 0; i < deps.length; i++) {
|
for (let i = 0; i < deps.length; i++) {
|
||||||
deps[i].delete(effect)
|
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
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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${
|
||||||
|
Loading…
x
Reference in New Issue
Block a user