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:
@@ -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