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:
Evan You
2021-06-24 17:44:32 -04:00
parent 63a51ffcab
commit 87f69fd0bb
12 changed files with 221 additions and 208 deletions

View File

@@ -1,4 +1,3 @@
import { effect, stop } from '@vue/reactivity'
import {
queueJob,
nextTick,
@@ -576,20 +575,19 @@ describe('scheduler', () => {
// 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)
})
})

View File

@@ -1,12 +1,12 @@
import {
effect,
stop,
isRef,
Ref,
ComputedRef,
ReactiveEffect,
ReactiveEffectOptions,
isReactive,
ReactiveFlags
ReactiveFlags,
EffectScheduler
} from '@vue/reactivity'
import { SchedulerJob, queuePreFlushCb } from './scheduler'
import {
@@ -244,7 +244,7 @@ function doWatch(
let cleanup: () => void
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
cleanup = runner.options.onStop = () => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
@@ -268,12 +268,12 @@ function doWatch(
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 ||
@@ -300,7 +300,7 @@ function doWatch(
}
} else {
// watchEffect
runner()
effect.run()
}
}
@@ -308,7 +308,7 @@ function doWatch(
// 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') {
@@ -326,32 +326,35 @@ function doWatch(
}
}
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)
}
}
}

View File

@@ -1,7 +1,6 @@
import {
isReactive,
reactive,
stop,
track,
TrackOpTypes,
trigger,
@@ -575,7 +574,7 @@ function installCompatMount(
// stop effects
if (effects) {
for (let i = 0; i < effects.length; i++) {
stop(effects[i])
effects[i].stop()
}
}
// unmounted hook

View File

@@ -59,6 +59,7 @@ import { currentRenderingInstance } from './componentRenderContext'
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>
@@ -217,9 +218,14 @@ export interface ComponentInternalInstance {
*/
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.
* @internal
@@ -445,6 +451,7 @@ export function createComponentInstance(
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,

View File

@@ -48,15 +48,13 @@ import {
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'
@@ -286,23 +284,6 @@ export const enum MoveType {
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
@@ -378,7 +359,7 @@ export const setRef = (
// 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()
@@ -388,7 +369,7 @@ export const setRef = (
ref.value = value
}
if (value) {
;(doSet as SchedulerCb).id = -1
;(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense)
} else {
doSet()
@@ -1394,7 +1375,7 @@ function baseCreateRenderer(
// 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 {
@@ -1414,8 +1395,7 @@ function baseCreateRenderer(
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
@@ -1639,12 +1619,33 @@ function baseCreateRenderer(
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 = (
@@ -2284,7 +2285,7 @@ function baseCreateRenderer(
unregisterHMR(instance)
}
const { bum, effects, update, subTree, um } = instance
const { bum, effect, effects, update, subTree, um } = instance
// beforeUnmount hook
if (bum) {
@@ -2299,13 +2300,15 @@ function baseCreateRenderer(
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

View File

@@ -2,9 +2,26 @@ import { ErrorCodes, callWithErrorHandling } from './errorHandling'
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.
@@ -13,8 +30,7 @@ export interface SchedulerJob extends Function, Partial<ReactiveEffect> {
ownerInstance?: ComponentInternalInstance
}
export type SchedulerCb = Function & { id?: number }
export type SchedulerCbs = SchedulerCb | SchedulerCb[]
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
let isFlushing = false
let isFlushPending = false
@@ -22,12 +38,12 @@ 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()
@@ -36,7 +52,7 @@ let currentFlushPromise: Promise<void> | null = null
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,
@@ -105,9 +121,9 @@ export function invalidateJob(job: SchedulerJob) {
}
function queueCb(
cb: SchedulerCbs,
activeQueue: SchedulerCb[] | null,
pendingQueue: SchedulerCb[],
cb: SchedulerJobs,
activeQueue: SchedulerJob[] | null,
pendingQueue: SchedulerJob[],
index: number
) {
if (!isArray(cb)) {
@@ -129,11 +145,11 @@ function queueCb(
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)
}
@@ -205,8 +221,8 @@ export function flushPostFlushCbs(seen?: CountMap) {
}
}
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
@@ -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)) {
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${