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,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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${
|
||||
|
||||
Reference in New Issue
Block a user