diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 6be4d8e25dd..2a56aab8778 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.func) 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) @@ -578,10 +578,10 @@ 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), undefined, false, 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) @@ -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 } ) @@ -703,7 +706,7 @@ describe('reactivity/effect', () => { expect(dummy).toBe(2) // stopped effect should still be manually callable - runner() + runner.run() expect(dummy).toBe(3) }) @@ -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 }) @@ -752,7 +753,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/__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 7f2c5b50d80..80242e246ad 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,8 +1,7 @@ -import { effect, ReactiveEffect, trigger, track } from './effect' -import { TriggerOpTypes, TrackOpTypes } from './operations' -import { Ref } from './ref' +import { effect, ReactiveEffect } from './effect' +import { Ref, trackRefValue, triggerRefValue } 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 @@ -24,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( @@ -34,25 +32,27 @@ class ComputedRefImpl { private readonly _setter: ComputedSetter, isReadonly: boolean ) { - this.effect = effect(getter, { - lazy: true, - scheduler: () => { + this.effect = effect( + getter, + () => { if (!this._dirty) { this._dirty = true - trigger(toRaw(this), TriggerOpTypes.SET, 'value') + triggerRefValue(this) } - } - }) + }, + false, + true + ) this[ReactiveFlags.IS_READONLY] = isReadonly } 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') + trackRefValue(this) return this._value } @@ -61,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 683f8fa9e3c..c19334edf4c 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -5,38 +5,114 @@ import { EMPTY_OBJ, isArray, isIntegerKey, isMap } from '@vue/shared' // 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() -export interface ReactiveEffect { - (): T - _isEffect: true - id: number +export type EffectScheduler = (job: () => void) => void + +export class ReactiveEffect { + public id = uid++ + public deps: Dep[] = [] + private runner?: ReactiveEffectFunction + + constructor( + public raw: () => T, + 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.scheduler ? undefined : this.raw() + } + if (!effectStack.includes(this)) { + cleanup(this) + try { + enableTracking() + effectStack.push(this) + activeEffect = this + return this.raw() + } finally { + effectStack.pop() + resetTracking() + const n = effectStack.length + activeEffect = n ? effectStack[n - 1] : undefined + } + } + } + + public get func(): ReactiveEffectFunction { + if (!this.runner) { + const runner = () => { + return this.run() + } + runner.effect = this + runner.allowRecurse = this.allowRecurse + this.runner = runner + } + return this.runner + } +} + +// Use prototype for optional properties to minimize memory usage. +export interface ReactiveEffect { active: boolean - raw: () => T - deps: Array + allowRecurse: boolean options: ReactiveEffectOptions +} + +ReactiveEffect.prototype.active = true +ReactiveEffect.prototype.allowRecurse = false +ReactiveEffect.prototype.options = EMPTY_OBJ + +export interface ReactiveEffectFunction { + (): T | undefined + effect: ReactiveEffect allowRecurse: boolean } +function createReactiveEffect( + fn: () => T, + allowRecurse: boolean, + scheduler: EffectScheduler | undefined, + options: ReactiveEffectOptions | undefined +): ReactiveEffect { + return new ReactiveEffect(fn, allowRecurse, scheduler, options) +} + export interface ReactiveEffectOptions { - lazy?: boolean - scheduler?: (job: ReactiveEffect) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void - allowRecurse?: boolean } 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 @@ -48,20 +124,24 @@ 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( fn: () => T, - options: ReactiveEffectOptions = EMPTY_OBJ + scheduler: EffectScheduler | undefined = undefined, + allowRecurse: boolean = false, + lazy: boolean = false, + options: ReactiveEffectOptions | undefined = undefined ): ReactiveEffect { if (isEffect(fn)) { - fn = fn.raw + fn = fn.effect.raw } - const effect = createReactiveEffect(fn, options) - if (!options.lazy) { - effect() + const effect = createReactiveEffect(fn, allowRecurse, scheduler, options) + + if (!lazy) { + effect.run() } return effect } @@ -78,38 +158,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) { @@ -139,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) @@ -150,16 +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 - }) + + const eventInfo = __DEV__ + ? { effect: activeEffect, target, type, key } + : undefined + + trackEffects(dep, eventInfo) +} + +export function isTracking() { + return shouldTrack && activeEffect !== undefined +} + +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)) } } } @@ -178,80 +238,99 @@ 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 eventInfo = __DEV__ + ? { target, type, key, newValue, oldValue, oldTarget } + : undefined + triggerMultiEffects(sets, eventInfo) +} + +type DepSets = (Dep | undefined)[] + +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, - key, - type, - newValue, - oldValue, - oldTarget - }) + effect.options.onTrigger( + Object.assign({ effect }, debuggerEventExtraInfo) + ) } - if (effect.options.scheduler) { - effect.options.scheduler(effect) + if (effect.scheduler) { + effect.scheduler(effect.func) } else { - effect() + effect.run() } } - effects.forEach(run) + const immutableDeps = [...dep] + immutableDeps.forEach(effect => { + if (effect !== activeEffect || effect.allowRecurse) { + run(effect) + } + }) } 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/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 e3633f982e0..1234ddca79c 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -1,8 +1,13 @@ -import { track, trigger } from './effect' -import { TrackOpTypes, TriggerOpTypes } from './operations' +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 @@ -18,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 } @@ -50,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() { - track(toRaw(this), TrackOpTypes.GET, 'value') + trackRefValue(this) return this._value } @@ -65,11 +100,14 @@ 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) + triggerRefValue(this, newVal) } } } +RefImpl.prototype.__v_isRef = true +interface RefImpl extends Ref {} + function createRef(rawValue: unknown, shallow = false) { if (isRef(rawValue)) { return rawValue @@ -78,7 +116,7 @@ function createRef(rawValue: unknown, shallow = false) { } export function triggerRef(ref: Ref) { - trigger(toRaw(ref), TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0) + triggerRefValue(ref, __DEV__ ? ref.value : void 0) } export function unref(ref: T): T extends Ref ? V : T { @@ -118,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( - () => track(this, TrackOpTypes.GET, 'value'), - () => trigger(this, TriggerOpTypes.SET, 'value') + () => trackRefValue(this), + () => triggerRefValue(this) ) this._get = get this._set = set @@ -138,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 } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 62592d3938c..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 { @@ -145,15 +146,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)) { @@ -209,9 +201,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 @@ -236,7 +229,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 +245,7 @@ function doWatch( } } else { // watchEffect - runner() + runner.run() } } @@ -260,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') { @@ -278,12 +271,11 @@ function doWatch( } } - const runner = effect(getter, { - lazy: true, - onTrack, - onTrigger, - scheduler - }) + let options: ReactiveEffectOptions | undefined = undefined + if (onTrack || onTrigger) { + options = { onTrack, onTrigger } + } + const runner = effect(getter, scheduler, false, true, options) recordInstanceBoundEffect(runner) @@ -292,12 +284,12 @@ function doWatch( if (immediate) { job() } else { - oldValue = runner() + oldValue = runner.run() } } else if (flush === 'post') { - queuePostRenderEffect(runner, instance && instance.suspense) + queuePostRenderEffect(runner.func, instance && instance.suspense) } else { - runner() + runner.run() } return () => { @@ -308,6 +300,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, diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 2ac9b8a47a1..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), + $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/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..0603bbaff1c 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.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 f0182c16f64..50eba3c4db2 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -265,18 +265,10 @@ 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 } @@ -1324,9 +1316,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.func) // instance.update is the reactive effect runner. - instance.update() + instance.update.run() } } else { // no update needed. just copy over properties @@ -1346,53 +1338,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 +1471,38 @@ 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) + }, + queueJob, + true, // #1801, #2043 component render effects should allow recursive updates + false, + __DEV__ ? createDevEffectOptions(instance) : undefined + ) } const updateComponentPreRender = ( @@ -1521,7 +1519,7 @@ 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.func : 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)