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:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
// can be attached after creation
|
||||
onStop?: () => void
|
||||
// dev only
|
||||
onTrack?: (event: DebuggerEvent) => void
|
||||
// dev only
|
||||
onTrigger?: (event: DebuggerEvent) => void
|
||||
|
||||
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
|
||||
}
|
||||
constructor(
|
||||
public fn: () => T,
|
||||
public scheduler: EffectScheduler | null = null,
|
||||
// allow recursive self-invocation
|
||||
public allowRecurse = false
|
||||
) {}
|
||||
|
||||
export function stop(effect: ReactiveEffect) {
|
||||
if (effect.active) {
|
||||
cleanup(effect)
|
||||
if (effect.options.onStop) {
|
||||
effect.options.onStop()
|
||||
run() {
|
||||
if (!this.active) {
|
||||
return this.fn()
|
||||
}
|
||||
effect.active = false
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
const { deps } = this
|
||||
if (deps.length) {
|
||||
for (let i = 0; i < deps.length; i++) {
|
||||
deps[i].delete(this)
|
||||
}
|
||||
deps.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.active) {
|
||||
this.cleanup()
|
||||
if (this.onStop) {
|
||||
this.onStop()
|
||||
}
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup(effect: ReactiveEffect) {
|
||||
const { deps } = effect
|
||||
if (deps.length) {
|
||||
for (let i = 0; i < deps.length; i++) {
|
||||
deps[i].delete(effect)
|
||||
}
|
||||
deps.length = 0
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,9 @@ export {
|
||||
resetTracking,
|
||||
ITERATE_KEY,
|
||||
ReactiveEffect,
|
||||
ReactiveEffectRunner,
|
||||
ReactiveEffectOptions,
|
||||
EffectScheduler,
|
||||
DebuggerEvent
|
||||
} from './effect'
|
||||
export { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||
|
||||
Reference in New Issue
Block a user