From 4ef117cde10c381fc3e82d36bf9d5be04c3c223a Mon Sep 17 00:00:00 2001 From: unbyte Date: Mon, 21 Sep 2020 17:31:34 +0800 Subject: [PATCH] feat(runtime-core): api to detach watchers and computed values from component lifecycle --- .../__tests__/apiComputed.spec.ts | 61 ++++++++++++++++++ .../runtime-core/__tests__/apiWatch.spec.ts | 63 +++++++++++++++++++ packages/runtime-core/src/apiComputed.ts | 37 ++++++++++- packages/runtime-core/src/apiWatch.ts | 25 +++++++- packages/runtime-core/src/index.ts | 2 +- 5 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 packages/runtime-core/__tests__/apiComputed.spec.ts diff --git a/packages/runtime-core/__tests__/apiComputed.spec.ts b/packages/runtime-core/__tests__/apiComputed.spec.ts new file mode 100644 index 00000000000..748ae55255f --- /dev/null +++ b/packages/runtime-core/__tests__/apiComputed.spec.ts @@ -0,0 +1,61 @@ +import { ref } from '@vue/reactivity' +import { h, nextTick, staticComputed } from '@vue/runtime-core' +import { nodeOps, render } from '@vue/runtime-test' + +it('detach staticComputed from components and stop them manually', async () => { + const toggle = ref(true) + + class Service { + counter = ref(0) + counterComputed = staticComputed(() => this.counter.value + 1) + } + + let singleton: Service + + function makeService() { + if (!singleton) { + singleton = new Service() + } + return singleton + } + + const Child = { + setup() { + makeService() + }, + render() {} + } + + const App = { + setup() { + return () => { + return toggle.value ? h(Child) : null + } + } + } + + render(h(App), nodeOps.createElement('div')) + + expect(`must be stopped manually`).toHaveBeenWarned() + + await nextTick() + expect(singleton!.counterComputed.value).toBe(1) + + singleton!.counter.value++ + await nextTick() + expect(singleton!.counterComputed.value).toBe(2) + + toggle.value = false + await nextTick() + toggle.value = true + await nextTick() + + singleton!.counter.value++ + await nextTick() + expect(singleton!.counterComputed.value).toBe(3) + + singleton!.counterComputed.stop() + singleton!.counter.value++ + await nextTick() + expect(singleton!.counterComputed.value).toBe(3) +}) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 31bca6bed3f..a033d60262e 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -523,6 +523,69 @@ describe('api: watch', () => { expect(dom!.tag).toBe('p') }) + it('detach watchers of static lifecycle from components and stop them manually', async () => { + const toggle = ref(false) + const val = ref(0) + const valPlus = ref(0) + + let stopEffect: () => void + + const newEffect = () => { + if (!stopEffect) { + stopEffect = watchEffect( + () => { + valPlus.value = val.value + 1 + }, + { + lifecycle: 'static' + } + ) + } + return stopEffect + } + + const Child = { + setup() { + newEffect() + }, + render() {} + } + + const App = { + setup() { + return () => { + return toggle.value ? h(Child) : null + } + } + } + + toggle.value = true + render(h(App), nodeOps.createElement('div')) + + expect(`must be stopped manually`).toHaveBeenWarned() + + await nextTick() + expect(valPlus.value).toBe(1) + + val.value++ + await nextTick() + expect(valPlus.value).toBe(2) + + toggle.value = false + await nextTick() + toggle.value = true + await nextTick() + + val.value++ + await nextTick() + expect(valPlus.value).toBe(3) + + stopEffect!() + val.value++ + await nextTick() + expect(valPlus.value).toBe(3) + }) + it('deep', async () => { const state = reactive({ nested: { diff --git a/packages/runtime-core/src/apiComputed.ts b/packages/runtime-core/src/apiComputed.ts index 02b0ab88aeb..b4ff4b71fa3 100644 --- a/packages/runtime-core/src/apiComputed.ts +++ b/packages/runtime-core/src/apiComputed.ts @@ -1,11 +1,13 @@ import { computed as _computed, + ComputedGetter, ComputedRef, + stop, WritableComputedOptions, - WritableComputedRef, - ComputedGetter + WritableComputedRef } from '@vue/reactivity' import { recordInstanceBoundEffect } from './component' +import { warn } from './warning' export function computed(getter: ComputedGetter): ComputedRef export function computed( @@ -18,3 +20,34 @@ export function computed( recordInstanceBoundEffect(c.effect) return c } + +export interface StaticComputedRef extends ComputedRef { + stop: () => void +} + +export interface StaticWritableComputedRef extends WritableComputedRef { + stop: () => void +} + +export function staticComputed( + getter: ComputedGetter +): StaticComputedRef +export function staticComputed( + options: WritableComputedOptions +): StaticWritableComputedRef +export function staticComputed( + getterOrOptions: ComputedGetter | WritableComputedOptions +) { + if (__DEV__) { + warn( + 'note that static life computed value must be stopped manually ' + + 'to prevent memory leaks.' + ) + } + + const c = _computed(getterOrOptions as any) as any + c.stop = () => { + stop(c.effect) + } + return c +} diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 14253a2a403..90cfcdf3f91 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -64,6 +64,7 @@ export interface WatchOptionsBase { flush?: 'pre' | 'post' | 'sync' onTrack?: ReactiveEffectOptions['onTrack'] onTrigger?: ReactiveEffectOptions['onTrigger'] + lifecycle?: 'static' | 'instance' } export interface WatchOptions extends WatchOptionsBase { @@ -133,7 +134,14 @@ export function watch( function doWatch( source: WatchSource | WatchSource[] | WatchEffect, cb: WatchCallback | null, - { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ, + { + immediate, + deep, + flush, + onTrack, + onTrigger, + lifecycle + }: WatchOptions = EMPTY_OBJ, instance = currentInstance ): WatchStopHandle { if (__DEV__ && !cb) { @@ -151,6 +159,17 @@ function doWatch( } } + const isStatic = lifecycle === 'static' + if (isStatic) { + if (__DEV__) { + warn( + 'note that watchers of static lifecycle must be stopped manually ' + + 'to prevent memory leaks.' + ) + } + instance = null + } + const warnInvalidSource = (s: unknown) => { warn( `Invalid watch source: `, @@ -290,7 +309,9 @@ function doWatch( scheduler }) - recordInstanceBoundEffect(runner) + if (!isStatic) { + recordInstanceBoundEffect(runner) + } // initial run if (cb) { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 26c27d544e6..b70dd1e9716 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -24,7 +24,7 @@ export { markRaw, toRaw } from '@vue/reactivity' -export { computed } from './apiComputed' +export { computed, staticComputed } from './apiComputed' export { watch, watchEffect } from './apiWatch' export { onBeforeMount,