From 44dedfb9c72dd9045fac2eb697ed91d4a08feb56 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Fri, 9 Oct 2020 16:02:41 +0200 Subject: [PATCH 01/11] feat(effect): use Class for effect Reduces memory usage for (computed) refs by 25% and improves creation performance --- packages/reactivity/__tests__/effect.spec.ts | 18 +-- packages/reactivity/src/computed.ts | 2 +- packages/reactivity/src/effect.ts | 106 ++++++++++-------- packages/runtime-core/src/apiWatch.ts | 10 +- .../src/componentPublicInstance.ts | 2 +- .../src/components/BaseTransition.ts | 2 +- packages/runtime-core/src/hmr.ts | 4 +- packages/runtime-core/src/renderer.ts | 9 +- .../__tests__/directives/vOn.spec.ts | 14 +-- packages/shared/__tests__/looseEqual.spec.ts | 14 +-- 10 files changed, 98 insertions(+), 83 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 6be4d8e25dd..4155d93553c 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -423,8 +423,8 @@ describe('reactivity/effect', () => { } const effect1 = effect(greet) const effect2 = effect(greet) - expect(typeof effect1).toBe('function') - expect(typeof effect2).toBe('function') + expect(typeof effect1).toBe('object') + expect(typeof effect2).toBe('object') expect(effect1).not.toBe(greet) expect(effect1).not.toBe(effect2) }) @@ -460,10 +460,10 @@ describe('reactivity/effect', () => { }) expect(dummy).toBe('other') - runner() + runner.run() expect(dummy).toBe('other') run = true - runner() + runner.run() expect(dummy).toBe('value') obj.prop = 'World' expect(dummy).toBe('World') @@ -490,7 +490,7 @@ describe('reactivity/effect', () => { it('should not double wrap if the passed function is a effect', () => { const runner = effect(() => {}) - const otherRunner = effect(runner) + const otherRunner = effect(runner.executor) expect(runner).not.toBe(otherRunner) expect(runner.raw).toBe(otherRunner.raw) }) @@ -520,7 +520,7 @@ describe('reactivity/effect', () => { const childeffect = effect(childSpy) const parentSpy = jest.fn(() => { dummy.num2 = nums.num2 - childeffect() + childeffect.run() dummy.num3 = nums.num3 }) effect(parentSpy) @@ -581,7 +581,7 @@ describe('reactivity/effect', () => { const runner = effect(() => (dummy = obj.foo), { lazy: true }) expect(dummy).toBe(undefined) - expect(runner()).toBe(1) + expect(runner.run()).toBe(1) expect(dummy).toBe(1) obj.foo = 2 expect(dummy).toBe(2) @@ -703,7 +703,7 @@ describe('reactivity/effect', () => { expect(dummy).toBe(2) // stopped effect should still be manually callable - runner() + runner.run() expect(dummy).toBe(3) }) @@ -752,7 +752,7 @@ describe('reactivity/effect', () => { // observed value in inner stopped effect // will track outer effect as an dependency effect(() => { - runner() + runner.run() }) expect(dummy).toBe(2) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 7f2c5b50d80..6823abebeb4 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -49,7 +49,7 @@ class ComputedRefImpl { get value() { if (this._dirty) { - this._value = this.effect() + this._value = this.effect.run() as T this._dirty = false } track(toRaw(this), TrackOpTypes.GET, 'value') diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 683f8fa9e3c..b0a403de7e9 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -9,20 +9,63 @@ type Dep = Set type KeyToDepMap = Map const targetMap = new WeakMap() -export interface ReactiveEffect { - (): T - _isEffect: true - id: number - active: boolean - raw: () => T - deps: Array - options: ReactiveEffectOptions +export class ReactiveEffect { + public allowRecurse = !!this.options.allowRecurse + public id = uid++ + public active = true + public deps: Dep[] = [] + private runner?: ReactiveEffectFunction + + constructor(public raw: () => T, public options: ReactiveEffectOptions) {} + + public run() { + if (!this.active) { + return this.options.scheduler ? undefined : this.raw() + } + if (!effectStack.includes(this)) { + cleanup(this) + try { + enableTracking() + effectStack.push(this) + activeEffect = this + return this.raw() + } finally { + effectStack.pop() + resetTracking() + activeEffect = effectStack[effectStack.length - 1] + } + } + } + + public get executor(): ReactiveEffectFunction { + if (!this.runner) { + const runner = () => { + return this.run() + } + runner._effect = this + runner.allowRecurse = this.allowRecurse + this.runner = runner + } + return this.runner + } +} + +export interface ReactiveEffectFunction { + (): T | undefined + _effect: ReactiveEffect allowRecurse: boolean } +function createReactiveEffect( + fn: () => T, + options: ReactiveEffectOptions +): ReactiveEffect { + return new ReactiveEffect(fn, options) +} + export interface ReactiveEffectOptions { lazy?: boolean - scheduler?: (job: ReactiveEffect) => void + scheduler?: (job: () => void) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void @@ -48,8 +91,8 @@ let activeEffect: ReactiveEffect | undefined export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') -export function isEffect(fn: any): fn is ReactiveEffect { - return fn && fn._isEffect === true +export function isEffect(fn: any): fn is ReactiveEffectFunction { + return fn && !!fn._effect } export function effect( @@ -57,11 +100,12 @@ export function effect( options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { if (isEffect(fn)) { - fn = fn.raw + fn = fn._effect.raw } const effect = createReactiveEffect(fn, options) + if (!options.lazy) { - effect() + effect.run() } return effect } @@ -78,38 +122,6 @@ export function stop(effect: ReactiveEffect) { let uid = 0 -function createReactiveEffect( - fn: () => T, - options: ReactiveEffectOptions -): ReactiveEffect { - const effect = function reactiveEffect(): unknown { - if (!effect.active) { - return options.scheduler ? undefined : fn() - } - if (!effectStack.includes(effect)) { - cleanup(effect) - try { - enableTracking() - effectStack.push(effect) - activeEffect = effect - return fn() - } finally { - effectStack.pop() - resetTracking() - activeEffect = effectStack[effectStack.length - 1] - } - } - } 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) { @@ -247,9 +259,9 @@ export function trigger( }) } if (effect.options.scheduler) { - effect.options.scheduler(effect) + effect.options.scheduler(effect.executor) } else { - effect() + effect.run() } } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 62592d3938c..fe807461b4c 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -236,7 +236,7 @@ function doWatch( } if (cb) { // watch(source, cb) - const newValue = runner() + const newValue = runner.run() if (deep || forceTrigger || hasChanged(newValue, oldValue)) { // cleanup before running cb again if (cleanup) { @@ -252,7 +252,7 @@ function doWatch( } } else { // watchEffect - runner() + runner.run() } } @@ -292,12 +292,12 @@ function doWatch( if (immediate) { job() } else { - oldValue = runner() + oldValue = runner.run() } } else if (flush === 'post') { - queuePostRenderEffect(runner, instance && instance.suspense) + queuePostRenderEffect(runner.executor, instance && instance.suspense) } else { - runner() + runner.run() } return () => { diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 2ac9b8a47a1..2a04b212e3d 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -212,7 +212,7 @@ const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), { $root: i => i.root && i.root.proxy, $emit: i => i.emit, $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type), - $forceUpdate: i => () => queueJob(i.update), + $forceUpdate: i => () => queueJob(i.update.executor), $nextTick: i => nextTick.bind(i.proxy!), $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) } as PublicPropertiesMap) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index fc0ebb9e42c..dcbcbd01a1a 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -220,7 +220,7 @@ const BaseTransitionImpl = { // return placeholder node and queue update when leave finishes leavingHooks.afterLeave = () => { state.isLeaving = false - instance.update() + instance.update.run() } return emptyPlaceholder(child) } else if (mode === 'in-out') { diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 9a2d20def51..2d216cf20ab 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -90,7 +90,7 @@ function rerender(id: string, newRender?: Function) { instance.renderCache = [] // this flag forces child components with slot content to update isHmrUpdating = true - instance.update() + instance.update.run() isHmrUpdating = false }) } @@ -125,7 +125,7 @@ function reload(id: string, newComp: ComponentOptions | ClassComponent) { // 4. Force the parent instance to re-render. This will cause all updated // components to be unmounted and re-mounted. Queue the update so that we // don't end up forcing the same parent to re-render multiple times. - queueJob(instance.parent.update) + queueJob(instance.parent.update.executor) } else if (instance.appContext.reload) { // root instance mounted via createApp() has a reload method instance.appContext.reload() diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index f0182c16f64..7e92e0dde74 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1324,9 +1324,9 @@ function baseCreateRenderer( instance.next = n2 // 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) + invalidateJob(instance.update.executor) // instance.update is the reactive effect runner. - instance.update() + instance.update.run() } } else { // no update needed. just copy over properties @@ -1521,7 +1521,10 @@ function baseCreateRenderer( // props update may have triggered pre-flush watchers. // flush them before the render update. - flushPreFlushCbs(undefined, instance.update) + flushPreFlushCbs( + undefined, + instance.update ? instance.update.executor : null + ) } const patchChildren: PatchChildrenFn = ( diff --git a/packages/runtime-dom/__tests__/directives/vOn.spec.ts b/packages/runtime-dom/__tests__/directives/vOn.spec.ts index e2417d95677..e0b0da879a2 100644 --- a/packages/runtime-dom/__tests__/directives/vOn.spec.ts +++ b/packages/runtime-dom/__tests__/directives/vOn.spec.ts @@ -41,9 +41,9 @@ describe('runtime-dom: v-on directive', () => { }) test('it should support key modifiers and system modifiers', () => { - const keyNames = ["ctrl","shift","meta","alt"] + const keyNames = ['ctrl', 'shift', 'meta', 'alt'] - keyNames.forEach(keyName=>{ + keyNames.forEach(keyName => { const el = document.createElement('div') const fn = jest.fn() //
@@ -52,28 +52,28 @@ describe('runtime-dom: v-on directive', () => { 'arrow-left' ]) patchEvent(el, 'onKeyup', null, nextValue, null) - + triggerEvent(el, 'keyup', e => (e.key = 'a')) expect(fn).not.toBeCalled() - + triggerEvent(el, 'keyup', e => { e[`${keyName}Key`] = false e.key = 'esc' }) expect(fn).not.toBeCalled() - + triggerEvent(el, 'keyup', e => { e[`${keyName}Key`] = true e.key = 'Escape' }) expect(fn).toBeCalledTimes(1) - + triggerEvent(el, 'keyup', e => { e[`${keyName}Key`] = true e.key = 'ArrowLeft' }) expect(fn).toBeCalledTimes(2) - }); + }) }) test('it should support "exact" modifier', () => { diff --git a/packages/shared/__tests__/looseEqual.spec.ts b/packages/shared/__tests__/looseEqual.spec.ts index fe321cd1539..75bb25058b7 100644 --- a/packages/shared/__tests__/looseEqual.spec.ts +++ b/packages/shared/__tests__/looseEqual.spec.ts @@ -54,27 +54,27 @@ describe('utils/looseEqual', () => { const date2 = new Date(2019, 1, 2, 3, 4, 5, 7) const file1 = new File([''], 'filename.txt', { type: 'text/plain', - lastModified: date1.getTime(), + lastModified: date1.getTime() }) const file2 = new File([''], 'filename.txt', { type: 'text/plain', - lastModified: date1.getTime(), + lastModified: date1.getTime() }) const file3 = new File([''], 'filename.txt', { type: 'text/plain', - lastModified: date2.getTime(), + lastModified: date2.getTime() }) const file4 = new File([''], 'filename.csv', { type: 'text/csv', - lastModified: date1.getTime(), + lastModified: date1.getTime() }) const file5 = new File(['abcdef'], 'filename.txt', { type: 'text/plain', - lastModified: date1.getTime(), + lastModified: date1.getTime() }) const file6 = new File(['12345'], 'filename.txt', { type: 'text/plain', - lastModified: date1.getTime(), + lastModified: date1.getTime() }) // Identical file object references @@ -163,7 +163,7 @@ describe('utils/looseEqual', () => { const date1 = new Date(2019, 1, 2, 3, 4, 5, 6) const file1 = new File([''], 'filename.txt', { type: 'text/plain', - lastModified: date1.getTime(), + lastModified: date1.getTime() }) expect(looseEqual(123, '123')).toBe(true) From f9ed4c36cbe3bf9dadeecf55509a286eb3355e93 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Sat, 10 Oct 2020 15:40:42 +0200 Subject: [PATCH 02/11] feat(effect): do not store 'lazy' flag unnecessarily --- packages/reactivity/__tests__/effect.spec.ts | 4 ++-- packages/reactivity/src/computed.ts | 19 +++++++++++-------- packages/reactivity/src/effect.ts | 10 +++++----- packages/runtime-core/src/apiWatch.ts | 17 ++++++++++------- .../src/componentPublicInstance.ts | 2 +- packages/runtime-core/src/hmr.ts | 2 +- packages/runtime-core/src/renderer.ts | 7 ++----- 7 files changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 4155d93553c..e65377050f0 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -490,7 +490,7 @@ describe('reactivity/effect', () => { it('should not double wrap if the passed function is a effect', () => { const runner = effect(() => {}) - const otherRunner = effect(runner.executor) + const otherRunner = effect(runner.func) expect(runner).not.toBe(otherRunner) expect(runner.raw).toBe(otherRunner.raw) }) @@ -578,7 +578,7 @@ describe('reactivity/effect', () => { it('lazy', () => { const obj = reactive({ foo: 1 }) let dummy - const runner = effect(() => (dummy = obj.foo), { lazy: true }) + const runner = effect(() => (dummy = obj.foo), {}, true) expect(dummy).toBe(undefined) expect(runner.run()).toBe(1) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 6823abebeb4..0502aa4e668 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -34,15 +34,18 @@ class ComputedRefImpl { private readonly _setter: ComputedSetter, isReadonly: boolean ) { - this.effect = effect(getter, { - lazy: true, - scheduler: () => { - if (!this._dirty) { - this._dirty = true - trigger(toRaw(this), TriggerOpTypes.SET, 'value') + this.effect = effect( + getter, + { + scheduler: () => { + if (!this._dirty) { + this._dirty = true + trigger(toRaw(this), TriggerOpTypes.SET, 'value') + } } - } - }) + }, + true + ) this[ReactiveFlags.IS_READONLY] = isReadonly } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index b0a403de7e9..fe22368f1f2 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -37,7 +37,7 @@ export class ReactiveEffect { } } - public get executor(): ReactiveEffectFunction { + public get func(): ReactiveEffectFunction { if (!this.runner) { const runner = () => { return this.run() @@ -64,7 +64,6 @@ function createReactiveEffect( } export interface ReactiveEffectOptions { - lazy?: boolean scheduler?: (job: () => void) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void @@ -97,14 +96,15 @@ export function isEffect(fn: any): fn is ReactiveEffectFunction { export function effect( fn: () => T, - options: ReactiveEffectOptions = EMPTY_OBJ + options: ReactiveEffectOptions = EMPTY_OBJ, + lazy: boolean = false ): ReactiveEffect { if (isEffect(fn)) { fn = fn._effect.raw } const effect = createReactiveEffect(fn, options) - if (!options.lazy) { + if (!lazy) { effect.run() } return effect @@ -259,7 +259,7 @@ export function trigger( }) } if (effect.options.scheduler) { - effect.options.scheduler(effect.executor) + effect.options.scheduler(effect.func) } else { effect.run() } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index fe807461b4c..4e1fdd9b908 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -278,12 +278,15 @@ function doWatch( } } - const runner = effect(getter, { - lazy: true, - onTrack, - onTrigger, - scheduler - }) + const runner = effect( + getter, + { + onTrack, + onTrigger, + scheduler + }, + true + ) recordInstanceBoundEffect(runner) @@ -295,7 +298,7 @@ function doWatch( oldValue = runner.run() } } else if (flush === 'post') { - queuePostRenderEffect(runner.executor, instance && instance.suspense) + queuePostRenderEffect(runner.func, instance && instance.suspense) } else { runner.run() } diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 2a04b212e3d..742f08631cf 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -212,7 +212,7 @@ const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), { $root: i => i.root && i.root.proxy, $emit: i => i.emit, $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type), - $forceUpdate: i => () => queueJob(i.update.executor), + $forceUpdate: i => () => queueJob(i.update.func), $nextTick: i => nextTick.bind(i.proxy!), $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) } as PublicPropertiesMap) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 2d216cf20ab..0603bbaff1c 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -125,7 +125,7 @@ function reload(id: string, newComp: ComponentOptions | ClassComponent) { // 4. Force the parent instance to re-render. This will cause all updated // components to be unmounted and re-mounted. Queue the update so that we // don't end up forcing the same parent to re-render multiple times. - queueJob(instance.parent.update.executor) + queueJob(instance.parent.update.func) } else if (instance.appContext.reload) { // root instance mounted via createApp() has a reload method instance.appContext.reload() diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 7e92e0dde74..30ac73c792e 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1324,7 +1324,7 @@ function baseCreateRenderer( instance.next = n2 // 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.executor) + invalidateJob(instance.update.func) // instance.update is the reactive effect runner. instance.update.run() } @@ -1521,10 +1521,7 @@ function baseCreateRenderer( // props update may have triggered pre-flush watchers. // flush them before the render update. - flushPreFlushCbs( - undefined, - instance.update ? instance.update.executor : null - ) + flushPreFlushCbs(undefined, instance.update ? instance.update.func : null) } const patchChildren: PatchChildrenFn = ( From 134e33c3f79c4a2a72d9dc6ae1c1d09c36d5efa7 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Mon, 12 Oct 2020 09:04:35 +0200 Subject: [PATCH 03/11] feat(effect): set 'allowRecurse' as param Move out of the ReactiveEffectOptions in an effort to remove the necessity of creating this object for most use cases, saving memory and performance. --- packages/reactivity/__tests__/effect.spec.ts | 2 +- packages/reactivity/src/computed.ts | 1 + packages/reactivity/src/effect.ts | 14 +- packages/runtime-core/src/apiWatch.ts | 1 + packages/runtime-core/src/renderer.ts | 287 ++++++++++--------- 5 files changed, 156 insertions(+), 149 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index e65377050f0..fb756b81518 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -578,7 +578,7 @@ describe('reactivity/effect', () => { it('lazy', () => { const obj = reactive({ foo: 1 }) let dummy - const runner = effect(() => (dummy = obj.foo), {}, true) + const runner = effect(() => (dummy = obj.foo), {}, false, true) expect(dummy).toBe(undefined) expect(runner.run()).toBe(1) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 0502aa4e668..344569779dd 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -44,6 +44,7 @@ class ComputedRefImpl { } } }, + false, true ) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index fe22368f1f2..58379dd6379 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -10,13 +10,16 @@ type KeyToDepMap = Map const targetMap = new WeakMap() export class ReactiveEffect { - public allowRecurse = !!this.options.allowRecurse public id = uid++ public active = true public deps: Dep[] = [] private runner?: ReactiveEffectFunction - constructor(public raw: () => T, public options: ReactiveEffectOptions) {} + constructor( + public raw: () => T, + public allowRecurse: boolean, + public options: ReactiveEffectOptions + ) {} public run() { if (!this.active) { @@ -58,9 +61,10 @@ export interface ReactiveEffectFunction { function createReactiveEffect( fn: () => T, + allowRecurse: boolean, options: ReactiveEffectOptions ): ReactiveEffect { - return new ReactiveEffect(fn, options) + return new ReactiveEffect(fn, allowRecurse, options) } export interface ReactiveEffectOptions { @@ -68,7 +72,6 @@ export interface ReactiveEffectOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void - allowRecurse?: boolean } export type DebuggerEvent = { @@ -97,12 +100,13 @@ export function isEffect(fn: any): fn is ReactiveEffectFunction { export function effect( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ, + allowRecurse: boolean = false, lazy: boolean = false ): ReactiveEffect { if (isEffect(fn)) { fn = fn._effect.raw } - const effect = createReactiveEffect(fn, options) + const effect = createReactiveEffect(fn, allowRecurse, options) if (!lazy) { effect.run() diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 4e1fdd9b908..2b0d364c6bb 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -285,6 +285,7 @@ function doWatch( onTrigger, scheduler }, + false, true ) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 30ac73c792e..d8707578bf9 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -266,9 +266,7 @@ export const enum MoveType { } const prodEffectOptions = { - scheduler: queueJob, - // #1801, #2043 component render effects should allow recursive updates - allowRecurse: true + scheduler: queueJob } function createDevEffectOptions( @@ -276,7 +274,6 @@ function createDevEffectOptions( ): 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 } @@ -1346,53 +1343,132 @@ function baseCreateRenderer( optimized ) => { // create reactive effect for rendering - instance.update = effect(function componentEffect() { - if (!instance.isMounted) { - let vnodeHook: VNodeHook | null | undefined - const { el, props } = initialVNode - const { bm, m, parent } = instance - - // beforeMount hook - if (bm) { - invokeArrayFns(bm) - } - // onVnodeBeforeMount - if ((vnodeHook = props && props.onVnodeBeforeMount)) { - invokeVNodeHook(vnodeHook, parent, initialVNode) - } - - // render - if (__DEV__) { - startMeasure(instance, `render`) - } - const subTree = (instance.subTree = renderComponentRoot(instance)) - if (__DEV__) { - endMeasure(instance, `render`) - } + instance.update = effect( + function componentEffect() { + if (!instance.isMounted) { + let vnodeHook: VNodeHook | null | undefined + const { el, props } = initialVNode + const { bm, m, parent } = instance + + // beforeMount hook + if (bm) { + invokeArrayFns(bm) + } + // onVnodeBeforeMount + if ((vnodeHook = props && props.onVnodeBeforeMount)) { + invokeVNodeHook(vnodeHook, parent, initialVNode) + } - if (el && hydrateNode) { + // render if (__DEV__) { - startMeasure(instance, `hydrate`) + startMeasure(instance, `render`) } - // vnode has adopted host node - perform hydration instead of mount. - hydrateNode( - initialVNode.el as Node, - subTree, - instance, - parentSuspense - ) + const subTree = (instance.subTree = renderComponentRoot(instance)) if (__DEV__) { - endMeasure(instance, `hydrate`) + endMeasure(instance, `render`) + } + + if (el && hydrateNode) { + if (__DEV__) { + startMeasure(instance, `hydrate`) + } + // vnode has adopted host node - perform hydration instead of mount. + hydrateNode( + initialVNode.el as Node, + subTree, + instance, + parentSuspense + ) + if (__DEV__) { + endMeasure(instance, `hydrate`) + } + } else { + if (__DEV__) { + startMeasure(instance, `patch`) + } + patch( + null, + subTree, + container, + anchor, + instance, + parentSuspense, + isSVG + ) + if (__DEV__) { + endMeasure(instance, `patch`) + } + initialVNode.el = subTree.el } + // mounted hook + if (m) { + queuePostRenderEffect(m, parentSuspense) + } + // onVnodeMounted + if ((vnodeHook = props && props.onVnodeMounted)) { + queuePostRenderEffect(() => { + invokeVNodeHook(vnodeHook!, parent, initialVNode) + }, parentSuspense) + } + // activated hook for keep-alive roots. + // #1742 activated hook must be accessed after first render + // since the hook may be injected by a child keep-alive + const { a } = instance + if ( + a && + initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + ) { + queuePostRenderEffect(a, parentSuspense) + } + instance.isMounted = true } else { + // updateComponent + // This is triggered by mutation of component's own state (next: null) + // OR parent calling processComponent (next: VNode) + let { next, bu, u, parent, vnode } = instance + let originNext = next + let vnodeHook: VNodeHook | null | undefined + if (__DEV__) { + pushWarningContext(next || instance.vnode) + } + + if (next) { + next.el = vnode.el + updateComponentPreRender(instance, next, optimized) + } else { + next = vnode + } + + // beforeUpdate hook + if (bu) { + invokeArrayFns(bu) + } + // onVnodeBeforeUpdate + if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) { + invokeVNodeHook(vnodeHook, parent, next, vnode) + } + + // render + if (__DEV__) { + startMeasure(instance, `render`) + } + const nextTree = renderComponentRoot(instance) + if (__DEV__) { + endMeasure(instance, `render`) + } + const prevTree = instance.subTree + instance.subTree = nextTree + if (__DEV__) { startMeasure(instance, `patch`) } patch( - null, - subTree, - container, - anchor, + prevTree, + nextTree, + // parent may have changed if it's in a teleport + hostParentNode(prevTree.el!)!, + // anchor may have changed if it's in a fragment + getNextHostNode(prevTree), instance, parentSuspense, isSVG @@ -1400,111 +1476,36 @@ function baseCreateRenderer( if (__DEV__) { endMeasure(instance, `patch`) } - initialVNode.el = subTree.el - } - // mounted hook - if (m) { - queuePostRenderEffect(m, parentSuspense) - } - // onVnodeMounted - if ((vnodeHook = props && props.onVnodeMounted)) { - queuePostRenderEffect(() => { - invokeVNodeHook(vnodeHook!, parent, initialVNode) - }, parentSuspense) - } - // activated hook for keep-alive roots. - // #1742 activated hook must be accessed after first render - // since the hook may be injected by a child keep-alive - const { a } = instance - if ( - a && - initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - ) { - queuePostRenderEffect(a, parentSuspense) - } - instance.isMounted = true - } else { - // updateComponent - // This is triggered by mutation of component's own state (next: null) - // OR parent calling processComponent (next: VNode) - let { next, bu, u, parent, vnode } = instance - let originNext = next - let vnodeHook: VNodeHook | null | undefined - if (__DEV__) { - pushWarningContext(next || instance.vnode) - } - - if (next) { - next.el = vnode.el - updateComponentPreRender(instance, next, optimized) - } else { - next = vnode - } - - // beforeUpdate hook - if (bu) { - invokeArrayFns(bu) - } - // onVnodeBeforeUpdate - if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) { - invokeVNodeHook(vnodeHook, parent, next, vnode) - } - - // render - if (__DEV__) { - startMeasure(instance, `render`) - } - const nextTree = renderComponentRoot(instance) - if (__DEV__) { - endMeasure(instance, `render`) - } - const prevTree = instance.subTree - instance.subTree = nextTree - - if (__DEV__) { - startMeasure(instance, `patch`) - } - patch( - prevTree, - nextTree, - // parent may have changed if it's in a teleport - hostParentNode(prevTree.el!)!, - // anchor may have changed if it's in a fragment - getNextHostNode(prevTree), - instance, - parentSuspense, - isSVG - ) - if (__DEV__) { - endMeasure(instance, `patch`) - } - next.el = nextTree.el - if (originNext === null) { - // self-triggered update. In case of HOC, update parent component - // vnode el. HOC is indicated by parent instance's subTree pointing - // to child component's vnode - updateHOCHostEl(instance, nextTree.el) - } - // updated hook - if (u) { - queuePostRenderEffect(u, parentSuspense) - } - // onVnodeUpdated - if ((vnodeHook = next.props && next.props.onVnodeUpdated)) { - queuePostRenderEffect(() => { - invokeVNodeHook(vnodeHook!, parent, next!, vnode) - }, parentSuspense) - } + next.el = nextTree.el + if (originNext === null) { + // self-triggered update. In case of HOC, update parent component + // vnode el. HOC is indicated by parent instance's subTree pointing + // to child component's vnode + updateHOCHostEl(instance, nextTree.el) + } + // updated hook + if (u) { + queuePostRenderEffect(u, parentSuspense) + } + // onVnodeUpdated + if ((vnodeHook = next.props && next.props.onVnodeUpdated)) { + queuePostRenderEffect(() => { + invokeVNodeHook(vnodeHook!, parent, next!, vnode) + }, parentSuspense) + } - if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { - devtoolsComponentUpdated(instance) - } + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsComponentUpdated(instance) + } - if (__DEV__) { - popWarningContext() + if (__DEV__) { + popWarningContext() + } } - } - }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) + }, + __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions, + true // #1801, #2043 component render effects should allow recursive updates + ) } const updateComponentPreRender = ( From 6b26c38316fda49cee134b18d0d24fd2c6599f87 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Mon, 12 Oct 2020 11:12:10 +0200 Subject: [PATCH 04/11] feat(effect): reduce memory consumption Move several options to params, so that an object is not required for watchers or computeds. This reduces memory. On top of that, place some optional params on the prototype unless non-default. --- packages/reactivity/__tests__/effect.spec.ts | 23 ++++---- .../__tests__/shallowReactive.spec.ts | 6 +++ packages/reactivity/src/computed.ts | 10 ++-- packages/reactivity/src/effect.ts | 54 ++++++++++++++----- packages/runtime-core/src/apiWatch.ts | 18 +++---- packages/runtime-core/src/renderer.ts | 11 ++-- 6 files changed, 74 insertions(+), 48 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index fb756b81518..2a56aab8778 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -578,7 +578,7 @@ describe('reactivity/effect', () => { it('lazy', () => { const obj = reactive({ foo: 1 }) let dummy - const runner = effect(() => (dummy = obj.foo), {}, false, true) + const runner = effect(() => (dummy = obj.foo), undefined, false, true) expect(dummy).toBe(undefined) expect(runner.run()).toBe(1) @@ -593,12 +593,9 @@ describe('reactivity/effect', () => { runner = _runner }) const obj = reactive({ foo: 1 }) - effect( - () => { - dummy = obj.foo - }, - { scheduler } - ) + effect(() => { + dummy = obj.foo + }, scheduler) expect(scheduler).not.toHaveBeenCalled() expect(dummy).toBe(1) // should be called on first trigger @@ -625,6 +622,9 @@ describe('reactivity/effect', () => { dummy = 'bar' in obj dummy = Object.keys(obj) }, + undefined, + false, + false, { onTrack } ) expect(dummy).toEqual(['foo', 'bar']) @@ -662,6 +662,9 @@ describe('reactivity/effect', () => { () => { dummy = obj.foo }, + undefined, + false, + false, { onTrigger } ) @@ -715,9 +718,7 @@ describe('reactivity/effect', () => { () => { dummy = obj.prop }, - { - scheduler: e => queue.push(e) - } + e => queue.push(e) ) obj.prop = 2 expect(dummy).toBe(1) @@ -731,7 +732,7 @@ describe('reactivity/effect', () => { it('events: onStop', () => { const onStop = jest.fn() - const runner = effect(() => {}, { + const runner = effect(() => {}, undefined, false, false, { onStop }) diff --git a/packages/reactivity/__tests__/shallowReactive.spec.ts b/packages/reactivity/__tests__/shallowReactive.spec.ts index 5997d045b5a..e1187798f2b 100644 --- a/packages/reactivity/__tests__/shallowReactive.spec.ts +++ b/packages/reactivity/__tests__/shallowReactive.spec.ts @@ -68,6 +68,9 @@ describe('shallowReactive', () => { () => { a = Array.from(shallowSet) }, + undefined, + false, + false, { onTrack: onTrackFn } @@ -113,6 +116,9 @@ describe('shallowReactive', () => { () => { a = Array.from(shallowArray) }, + undefined, + false, + false, { onTrack: onTrackFn } diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 344569779dd..0430e5f8dd4 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -36,12 +36,10 @@ class ComputedRefImpl { ) { this.effect = effect( getter, - { - scheduler: () => { - if (!this._dirty) { - this._dirty = true - trigger(toRaw(this), TriggerOpTypes.SET, 'value') - } + () => { + if (!this._dirty) { + this._dirty = true + trigger(toRaw(this), TriggerOpTypes.SET, 'value') } }, false, diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 58379dd6379..1f7ee4f8105 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -9,21 +9,37 @@ type Dep = Set type KeyToDepMap = Map const targetMap = new WeakMap() +type EffectScheduler = (job: () => void) => void + export class ReactiveEffect { public id = uid++ - public active = true public deps: Dep[] = [] private runner?: ReactiveEffectFunction constructor( public raw: () => T, - public allowRecurse: boolean, - public options: ReactiveEffectOptions - ) {} + allowRecurse: boolean, + public scheduler: EffectScheduler | undefined, + options: ReactiveEffectOptions | undefined + ) { + if (allowRecurse) { + this.allowRecurse = true + } + if (options) { + this.options = options + } + } + + public setOnStop(func: () => void) { + if (this.options === EMPTY_OBJ) { + this.options = {} + } + this.options.onStop = func + } public run() { if (!this.active) { - return this.options.scheduler ? undefined : this.raw() + return this.scheduler ? undefined : this.raw() } if (!effectStack.includes(this)) { cleanup(this) @@ -53,6 +69,17 @@ export class ReactiveEffect { } } +// Use prototype for optional properties to minimize memory usage. +export interface ReactiveEffect { + active: boolean + allowRecurse: boolean + options: ReactiveEffectOptions +} + +ReactiveEffect.prototype.active = true +ReactiveEffect.prototype.allowRecurse = false +ReactiveEffect.prototype.options = EMPTY_OBJ + export interface ReactiveEffectFunction { (): T | undefined _effect: ReactiveEffect @@ -62,13 +89,13 @@ export interface ReactiveEffectFunction { function createReactiveEffect( fn: () => T, allowRecurse: boolean, - options: ReactiveEffectOptions + scheduler: EffectScheduler | undefined, + options: ReactiveEffectOptions | undefined ): ReactiveEffect { - return new ReactiveEffect(fn, allowRecurse, options) + return new ReactiveEffect(fn, allowRecurse, scheduler, options) } export interface ReactiveEffectOptions { - scheduler?: (job: () => void) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void @@ -99,14 +126,15 @@ export function isEffect(fn: any): fn is ReactiveEffectFunction { export function effect( fn: () => T, - options: ReactiveEffectOptions = EMPTY_OBJ, + scheduler: EffectScheduler | undefined = undefined, allowRecurse: boolean = false, - lazy: boolean = false + lazy: boolean = false, + options: ReactiveEffectOptions | undefined = undefined ): ReactiveEffect { if (isEffect(fn)) { fn = fn._effect.raw } - const effect = createReactiveEffect(fn, allowRecurse, options) + const effect = createReactiveEffect(fn, allowRecurse, scheduler, options) if (!lazy) { effect.run() @@ -262,8 +290,8 @@ export function trigger( oldTarget }) } - if (effect.options.scheduler) { - effect.options.scheduler(effect.func) + if (effect.scheduler) { + effect.scheduler(effect.func) } else { effect.run() } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 2b0d364c6bb..9224fca040e 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -209,9 +209,10 @@ function doWatch( let cleanup: () => void const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => { - cleanup = runner.options.onStop = () => { + cleanup = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } + runner.setOnStop(cleanup) } // in SSR there is no need to setup an actual effect, and it should be noop @@ -278,16 +279,11 @@ function doWatch( } } - const runner = effect( - getter, - { - onTrack, - onTrigger, - scheduler - }, - false, - true - ) + let options: ReactiveEffectOptions | undefined = undefined + if (onTrack || onTrigger) { + options = { onTrack, onTrigger } + } + const runner = effect(getter, scheduler, false, true, options) recordInstanceBoundEffect(runner) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d8707578bf9..50eba3c4db2 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -265,15 +265,10 @@ export const enum MoveType { REORDER } -const prodEffectOptions = { - scheduler: queueJob -} - function createDevEffectOptions( instance: ComponentInternalInstance ): ReactiveEffectOptions { return { - scheduler: queueJob, onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0, onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0 } @@ -1503,8 +1498,10 @@ function baseCreateRenderer( } } }, - __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions, - true // #1801, #2043 component render effects should allow recursive updates + queueJob, + true, // #1801, #2043 component render effects should allow recursive updates + false, + __DEV__ ? createDevEffectOptions(instance) : undefined ) } From d649a4d75d095719838fb20ce8f0e6f0ade831cc Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Mon, 12 Oct 2020 11:39:18 +0200 Subject: [PATCH 05/11] feat(effect): rename _effect to effect There's no point in underscoring the effect property of the runner. --- packages/reactivity/src/effect.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 1f7ee4f8105..1deec371bd8 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -61,7 +61,7 @@ export class ReactiveEffect { const runner = () => { return this.run() } - runner._effect = this + runner.effect = this runner.allowRecurse = this.allowRecurse this.runner = runner } @@ -82,7 +82,7 @@ ReactiveEffect.prototype.options = EMPTY_OBJ export interface ReactiveEffectFunction { (): T | undefined - _effect: ReactiveEffect + effect: ReactiveEffect allowRecurse: boolean } @@ -121,7 +121,7 @@ export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') export function isEffect(fn: any): fn is ReactiveEffectFunction { - return fn && !!fn._effect + return fn && !!fn.effect } export function effect( @@ -132,7 +132,7 @@ export function effect( options: ReactiveEffectOptions | undefined = undefined ): ReactiveEffect { if (isEffect(fn)) { - fn = fn._effect.raw + fn = fn.effect.raw } const effect = createReactiveEffect(fn, allowRecurse, scheduler, options) From 9bd002b1f3301383e90811d60850489738152522 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Tue, 13 Oct 2020 08:49:30 +0200 Subject: [PATCH 06/11] feat(apiWatch): extract function that doesn't need to be in function body 1. One variable less on the stack 2. Smaller functions means less time to optimize --- packages/runtime-core/src/apiWatch.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 9224fca040e..1eb60d1f5f7 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -145,15 +145,6 @@ function doWatch( } } - const warnInvalidSource = (s: unknown) => { - warn( - `Invalid watch source: `, - s, - `A watch source can only be a getter/effect function, a ref, ` + - `a reactive object, or an array of these types.` - ) - } - let getter: () => any let forceTrigger = false if (isRef(source)) { @@ -308,6 +299,15 @@ function doWatch( } } +function warnInvalidSource(s: unknown) { + warn( + `Invalid watch source: `, + s, + `A watch source can only be a getter/effect function, a ref, ` + + `a reactive object, or an array of these types.` + ) +} + // this.$watch export function instanceWatch( this: ComponentInternalInstance, From 3981a86db9cb2787631ba044018adab8a20f1bed Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Tue, 13 Oct 2020 12:22:38 +0200 Subject: [PATCH 07/11] feat(effect): implement ref-specific high-performance track/triggering --- packages/reactivity/src/computed.ts | 14 +++--- packages/reactivity/src/effect.ts | 66 +++++++++++++++++++++++++++++ packages/reactivity/src/ref.ts | 13 +++--- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 0430e5f8dd4..8c32d353e4e 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,8 +1,12 @@ -import { effect, ReactiveEffect, trigger, track } from './effect' -import { TriggerOpTypes, TrackOpTypes } from './operations' +import { + effect, + ReactiveEffect, + trackRefTarget, + triggerRefTarget +} from './effect' import { Ref } from './ref' import { isFunction, NOOP } from '@vue/shared' -import { ReactiveFlags, toRaw } from './reactive' +import { ReactiveFlags } from './reactive' export interface ComputedRef extends WritableComputedRef { readonly value: T @@ -39,7 +43,7 @@ class ComputedRefImpl { () => { if (!this._dirty) { this._dirty = true - trigger(toRaw(this), TriggerOpTypes.SET, 'value') + triggerRefTarget(this) } }, false, @@ -54,7 +58,7 @@ class ComputedRefImpl { this._value = this.effect.run() as T this._dirty = false } - track(toRaw(this), TrackOpTypes.GET, 'value') + trackRefTarget(this) return this._value } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 1deec371bd8..62266bf2eec 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,5 +1,6 @@ import { TrackOpTypes, TriggerOpTypes } from './operations' import { EMPTY_OBJ, isArray, isIntegerKey, isMap } from '@vue/shared' +import { toRaw } from './reactive' // The main WeakMap that stores {target -> key -> dep} connections. // Conceptually, it's easier to think of a dependency as a Dep class @@ -208,6 +209,32 @@ export function track(target: object, type: TrackOpTypes, key: unknown) { } } +export function trackRefTarget(ref: any) { + if (!shouldTrack || activeEffect === undefined) { + return + } + + ref = toRaw(ref) + + if (!ref.dep) { + ref.dep = new Set() + } + + const dep = ref.dep + if (!dep.has(activeEffect)) { + dep.add(activeEffect) + activeEffect.deps.push(dep) + if (__DEV__ && activeEffect.options.onTrack) { + activeEffect.options.onTrack({ + effect: activeEffect, + target: ref, + type: TrackOpTypes.GET, + key: 'value' + }) + } + } +} + export function trigger( target: object, type: TriggerOpTypes, @@ -299,3 +326,42 @@ export function trigger( effects.forEach(run) } + +export function triggerRefTarget( + ref: any, + newValue?: unknown, + oldValue?: unknown, + oldTarget?: Map | Set +) { + ref = toRaw(ref) + + if (!ref.dep) { + return + } + + const run = (effect: ReactiveEffect) => { + if (__DEV__ && effect.options.onTrigger) { + effect.options.onTrigger({ + effect, + target: ref, + key: 'value', + type: TriggerOpTypes.SET, + newValue, + oldValue, + oldTarget + }) + } + if (effect.scheduler) { + effect.scheduler(effect.func) + } else { + effect.run() + } + } + + const immutableDeps = [...ref.dep] + immutableDeps.forEach(effect => { + if (effect !== activeEffect || effect.allowRecurse) { + run(effect) + } + }) +} diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index e3633f982e0..b6ed7b08e68 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -1,5 +1,4 @@ -import { track, trigger } from './effect' -import { TrackOpTypes, TriggerOpTypes } from './operations' +import { trackRefTarget, triggerRefTarget } from './effect' import { isArray, isObject, hasChanged } from '@vue/shared' import { reactive, isProxy, toRaw, isReactive } from './reactive' import { CollectionTypes } from './collectionHandlers' @@ -57,7 +56,7 @@ class RefImpl { } get value() { - track(toRaw(this), TrackOpTypes.GET, 'value') + trackRefTarget(this) return this._value } @@ -65,7 +64,7 @@ class RefImpl { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : convert(newVal) - trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal) + triggerRefTarget(this, newVal) } } } @@ -78,7 +77,7 @@ function createRef(rawValue: unknown, shallow = false) { } export function triggerRef(ref: Ref) { - trigger(toRaw(ref), TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0) + triggerRefTarget(ref, __DEV__ ? ref.value : void 0) } export function unref(ref: T): T extends Ref ? V : T { @@ -122,8 +121,8 @@ class CustomRefImpl { constructor(factory: CustomRefFactory) { const { get, set } = factory( - () => track(this, TrackOpTypes.GET, 'value'), - () => trigger(this, TriggerOpTypes.SET, 'value') + () => trackRefTarget(this), + () => triggerRefTarget(this) ) this._get = get this._set = set From de906b77bd2823be281d096eabe75372dbdfdf1d Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Tue, 13 Oct 2020 12:25:20 +0200 Subject: [PATCH 08/11] feat(effect): remove out-of-bounds check Out-of-bounds should be avoided always, for performance reasons. In practice, some benchmarks (especially 'write ref, read 1000 computeds') showed significant (30%) improvement after this change. --- packages/reactivity/src/effect.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 62266bf2eec..1984ac20fd7 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -52,7 +52,8 @@ export class ReactiveEffect { } finally { effectStack.pop() resetTracking() - activeEffect = effectStack[effectStack.length - 1] + const n = effectStack.length + activeEffect = n ? effectStack[n - 1] : undefined } } } From 295e46cf5b1d020e6fdf263e36e06747ec4b3585 Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Tue, 13 Oct 2020 12:32:58 +0200 Subject: [PATCH 09/11] chore(effect): add comment to targetMap Explain that refs store their deps in a local property. --- packages/reactivity/src/effect.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 1984ac20fd7..2f2c0e96fd8 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -6,6 +6,9 @@ import { toRaw } from './reactive' // Conceptually, it's easier to think of a dependency as a Dep class // which maintains a Set of subscribers, but we simply store them as // raw Sets to reduce memory overhead. +// +// Notice that refs store their deps in a local property for +// performance reasons. type Dep = Set type KeyToDepMap = Map const targetMap = new WeakMap() From 4ed374cbf95e635e6458b22c9dfaaa2878deef5a Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Thu, 15 Oct 2020 17:08:21 +0200 Subject: [PATCH 10/11] feat(effect): refactor triggering for non-refs Improves performance. --- packages/reactivity/src/computed.ts | 19 ++-- packages/reactivity/src/effect.ts | 157 +++++++++++----------------- packages/reactivity/src/reactive.ts | 5 +- packages/reactivity/src/ref.ts | 60 +++++++++-- 4 files changed, 121 insertions(+), 120 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 8c32d353e4e..80242e246ad 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,10 +1,5 @@ -import { - effect, - ReactiveEffect, - trackRefTarget, - triggerRefTarget -} from './effect' -import { Ref } from './ref' +import { effect, ReactiveEffect } from './effect' +import { Ref, trackRefValue, triggerRefValue } from './ref' import { isFunction, NOOP } from '@vue/shared' import { ReactiveFlags } from './reactive' @@ -28,9 +23,8 @@ class ComputedRefImpl { private _value!: T private _dirty = true - public readonly effect: ReactiveEffect + public readonly effect: ReactiveEffect; - public readonly __v_isRef = true; public readonly [ReactiveFlags.IS_READONLY]: boolean constructor( @@ -43,7 +37,7 @@ class ComputedRefImpl { () => { if (!this._dirty) { this._dirty = true - triggerRefTarget(this) + triggerRefValue(this) } }, false, @@ -58,7 +52,7 @@ class ComputedRefImpl { this._value = this.effect.run() as T this._dirty = false } - trackRefTarget(this) + trackRefValue(this) return this._value } @@ -67,6 +61,9 @@ class ComputedRefImpl { } } +ComputedRefImpl.prototype.__v_isRef = true +interface ComputedRefImpl extends Ref {} + export function computed(getter: ComputedGetter): ComputedRef export function computed( options: WritableComputedOptions diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 2f2c0e96fd8..ad703e618bb 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,6 +1,5 @@ import { TrackOpTypes, TriggerOpTypes } from './operations' import { EMPTY_OBJ, isArray, isIntegerKey, isMap } from '@vue/shared' -import { toRaw } from './reactive' // The main WeakMap that stores {target -> key -> dep} connections. // Conceptually, it's easier to think of a dependency as a Dep class @@ -108,12 +107,12 @@ export interface ReactiveEffectOptions { export type DebuggerEvent = { effect: ReactiveEffect +} & DebuggerEventExtraInfo + +export type DebuggerEventExtraInfo = { target: object type: TrackOpTypes | TriggerOpTypes key: any -} & DebuggerEventExtraInfo - -export interface DebuggerEventExtraInfo { newValue?: any oldValue?: any oldTarget?: Map | Set @@ -188,7 +187,7 @@ export function resetTracking() { } export function track(target: object, type: TrackOpTypes, key: unknown) { - if (!shouldTrack || activeEffect === undefined) { + if (!isTracking()) { return } let depsMap = targetMap.get(target) @@ -199,42 +198,28 @@ export function track(target: object, type: TrackOpTypes, key: unknown) { if (!dep) { depsMap.set(key, (dep = new Set())) } - if (!dep.has(activeEffect)) { - dep.add(activeEffect) - activeEffect.deps.push(dep) - if (__DEV__ && activeEffect.options.onTrack) { - activeEffect.options.onTrack({ - effect: activeEffect, - target, - type, - key - }) - } - } -} -export function trackRefTarget(ref: any) { - if (!shouldTrack || activeEffect === undefined) { - return - } + const eventInfo = __DEV__ + ? { effect: activeEffect, target, type, key } + : undefined - ref = toRaw(ref) + trackEffects(dep, eventInfo) +} - if (!ref.dep) { - ref.dep = new Set() - } +export function isTracking() { + return shouldTrack && activeEffect !== undefined +} - const dep = ref.dep - if (!dep.has(activeEffect)) { - dep.add(activeEffect) - activeEffect.deps.push(dep) - if (__DEV__ && activeEffect.options.onTrack) { - activeEffect.options.onTrack({ - effect: activeEffect, - target: ref, - type: TrackOpTypes.GET, - key: 'value' - }) +export function trackEffects( + dep: Set, + debuggerEventExtraInfo?: DebuggerEventExtraInfo +) { + const effect = activeEffect! + if (!dep.has(effect)) { + dep.add(effect) + effect.deps.push(dep) + if (__DEV__ && effect.options.onTrack) { + effect.options.onTrack(Object.assign({ effect }, debuggerEventExtraInfo)) } } } @@ -253,107 +238,87 @@ export function trigger( return } - const effects = new Set() - const add = (effectsToAdd: Set | undefined) => { - if (effectsToAdd) { - effectsToAdd.forEach(effect => { - if (effect !== activeEffect || effect.allowRecurse) { - effects.add(effect) - } - }) - } - } - + let sets: DepSets = [] if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target - depsMap.forEach(add) + sets = [...depsMap.values()] } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { - add(dep) + sets.push(dep) } }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { - add(depsMap.get(key)) + sets.push(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { - add(depsMap.get(ITERATE_KEY)) + sets.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { - add(depsMap.get(MAP_KEY_ITERATE_KEY)) + sets.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes - add(depsMap.get('length')) + sets.push(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { - add(depsMap.get(ITERATE_KEY)) + sets.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { - add(depsMap.get(MAP_KEY_ITERATE_KEY)) + sets.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { - add(depsMap.get(ITERATE_KEY)) + sets.push(depsMap.get(ITERATE_KEY)) } break } } - const run = (effect: ReactiveEffect) => { - if (__DEV__ && effect.options.onTrigger) { - effect.options.onTrigger({ - effect, - target, - key, - type, - newValue, - oldValue, - oldTarget - }) - } - if (effect.scheduler) { - effect.scheduler(effect.func) - } else { - effect.run() - } - } - - effects.forEach(run) + const eventInfo = __DEV__ + ? { target, type, key, newValue, oldValue, oldTarget } + : undefined + triggerMultiEffects(sets, eventInfo) } -export function triggerRefTarget( - ref: any, - newValue?: unknown, - oldValue?: unknown, - oldTarget?: Map | Set -) { - ref = toRaw(ref) +type DepSets = (Dep | undefined)[] - if (!ref.dep) { - return +export function triggerMultiEffects( + depSets: DepSets, + debuggerEventExtraInfo?: DebuggerEventExtraInfo +) { + const sets = depSets.filter(set => !!set) as Dep[] + if (sets.length > 1) { + triggerEffects(concatSets(sets), debuggerEventExtraInfo) + } else if (sets.length === 1) { + triggerEffects(sets[0], debuggerEventExtraInfo) } +} + +function concatSets(sets: Set[]): Set { + const all = ([] as T[]).concat(...sets.map(s => [...s!])) + const deduped = new Set(all) + return deduped +} +export function triggerEffects( + dep: Dep, + debuggerEventExtraInfo?: DebuggerEventExtraInfo +) { const run = (effect: ReactiveEffect) => { if (__DEV__ && effect.options.onTrigger) { - effect.options.onTrigger({ - effect, - target: ref, - key: 'value', - type: TriggerOpTypes.SET, - newValue, - oldValue, - oldTarget - }) + effect.options.onTrigger( + Object.assign({ effect }, debuggerEventExtraInfo) + ) } if (effect.scheduler) { effect.scheduler(effect.func) @@ -362,7 +327,7 @@ export function triggerRefTarget( } } - const immutableDeps = [...ref.dep] + const immutableDeps = [...dep] immutableDeps.forEach(effect => { if (effect !== activeEffect || effect.allowRecurse) { run(effect) diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index cea41a191ae..fd76de9b5f2 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -218,9 +218,8 @@ export function isProxy(value: unknown): boolean { } export function toRaw(observed: T): T { - return ( - (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed - ) + const raw = observed && (observed as Target)[ReactiveFlags.RAW] + return raw ? toRaw(raw) : observed } export function markRaw(value: T): T { diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index b6ed7b08e68..1234ddca79c 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -1,7 +1,13 @@ -import { trackRefTarget, triggerRefTarget } from './effect' +import { + isTracking, + ReactiveEffect, + trackEffects, + triggerEffects +} from './effect' import { isArray, isObject, hasChanged } from '@vue/shared' import { reactive, isProxy, toRaw, isReactive } from './reactive' import { CollectionTypes } from './collectionHandlers' +import { TrackOpTypes, TriggerOpTypes } from './operations' declare const RefSymbol: unique symbol @@ -17,8 +23,40 @@ export interface Ref { * @internal */ _shallow?: boolean + + dep?: Set + + __v_isRef: true } +export function trackRefValue(ref: Ref) { + if (isTracking()) { + ref = toRaw(ref) + const eventInfo = __DEV__ + ? { target: ref, type: TrackOpTypes.GET, key: 'value' } + : undefined + if (!ref.dep) { + // This ref could be wrapped in a readonly proxy. + ref.dep = new Set() + } + trackEffects(ref.dep, eventInfo) + } +} + +export function triggerRefValue(ref: Ref, newVal?: any) { + ref = toRaw(ref) + if (ref.dep) { + const eventInfo = __DEV__ + ? { + target: ref, + type: TriggerOpTypes.SET, + key: 'value', + newValue: newVal + } + : undefined + triggerEffects(ref.dep, eventInfo) + } +} export type ToRef = T extends Ref ? T : Ref> export type ToRefs = { [K in keyof T]: ToRef } @@ -49,14 +87,12 @@ export function shallowRef(value?: unknown) { class RefImpl { private _value: T - public readonly __v_isRef = true - constructor(private _rawValue: T, public readonly _shallow = false) { this._value = _shallow ? _rawValue : convert(_rawValue) } get value() { - trackRefTarget(this) + trackRefValue(this) return this._value } @@ -64,11 +100,14 @@ class RefImpl { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : convert(newVal) - triggerRefTarget(this, newVal) + triggerRefValue(this, newVal) } } } +RefImpl.prototype.__v_isRef = true +interface RefImpl extends Ref {} + function createRef(rawValue: unknown, shallow = false) { if (isRef(rawValue)) { return rawValue @@ -77,7 +116,7 @@ function createRef(rawValue: unknown, shallow = false) { } export function triggerRef(ref: Ref) { - triggerRefTarget(ref, __DEV__ ? ref.value : void 0) + triggerRefValue(ref, __DEV__ ? ref.value : void 0) } export function unref(ref: T): T extends Ref ? V : T { @@ -117,12 +156,10 @@ class CustomRefImpl { private readonly _get: ReturnType>['get'] private readonly _set: ReturnType>['set'] - public readonly __v_isRef = true - constructor(factory: CustomRefFactory) { const { get, set } = factory( - () => trackRefTarget(this), - () => triggerRefTarget(this) + () => trackRefValue(this), + () => triggerRefValue(this) ) this._get = get this._set = set @@ -137,6 +174,9 @@ class CustomRefImpl { } } +CustomRefImpl.prototype.__v_isRef = true +interface CustomRefImpl extends Ref {} + export function customRef(factory: CustomRefFactory): Ref { return new CustomRefImpl(factory) as any } From 911fde1749c5100d10274ce6cd3d149652cbe1fa Mon Sep 17 00:00:00 2001 From: Bas van Meurs Date: Mon, 16 Nov 2020 13:47:52 +0100 Subject: [PATCH 11/11] chore(resolve): rebase on master --- packages/reactivity/src/effect.ts | 2 +- packages/reactivity/src/index.ts | 1 + packages/runtime-core/src/apiWatch.ts | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index ad703e618bb..c19334edf4c 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -12,7 +12,7 @@ type Dep = Set type KeyToDepMap = Map const targetMap = new WeakMap() -type EffectScheduler = (job: () => void) => void +export type EffectScheduler = (job: () => void) => void export class ReactiveEffect { public id = uid++ diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index b03e916d14a..72e0be497da 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -46,6 +46,7 @@ export { ITERATE_KEY, ReactiveEffect, ReactiveEffectOptions, + EffectScheduler, DebuggerEvent } from './effect' export { TrackOpTypes, TriggerOpTypes } from './operations' diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 1eb60d1f5f7..fe9d4385ff7 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -5,7 +5,8 @@ import { Ref, ComputedRef, ReactiveEffectOptions, - isReactive + isReactive, + EffectScheduler } from '@vue/reactivity' import { SchedulerJob, queuePreFlushCb } from './scheduler' import { @@ -252,7 +253,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 } else if (flush === 'post') {