Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Jun 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/reactivity/__tests__/computed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
computed,
reactive,
effect,
stop,
ref,
WritableComputedRef,
isReadonly
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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)
})
})
23 changes: 12 additions & 11 deletions packages/reactivity/__tests__/effect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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
},
Expand All @@ -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)
})
Expand All @@ -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
Expand All @@ -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',
Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/reactivity/__tests__/readonly.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Expand Down
18 changes: 7 additions & 11 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -35,27 +35,23 @@ 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
}

get value() {
// 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
}

Expand Down
185 changes: 87 additions & 98 deletions packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -62,78 +29,100 @@ 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
}

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()
const n = effectStack.length
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
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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()
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/reactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export {
resetTracking,
ITERATE_KEY,
ReactiveEffect,
ReactiveEffectRunner,
ReactiveEffectOptions,
EffectScheduler,
DebuggerEvent
} from './effect'
export { TrackOpTypes, TriggerOpTypes } from './operations'
Loading