Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runtime-core): api to detach watchers and computed values from component lifecycle #2185

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions packages/runtime-core/__tests__/apiComputed.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
63 changes: 63 additions & 0 deletions packages/runtime-core/__tests__/apiWatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
37 changes: 35 additions & 2 deletions packages/runtime-core/src/apiComputed.ts
Original file line number Diff line number Diff line change
@@ -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<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
Expand All @@ -18,3 +20,34 @@ export function computed<T>(
recordInstanceBoundEffect(c.effect)
return c
}

export interface StaticComputedRef<T> extends ComputedRef<T> {
stop: () => void
}

export interface StaticWritableComputedRef<T> extends WritableComputedRef<T> {
stop: () => void
}

export function staticComputed<T>(
getter: ComputedGetter<T>
): StaticComputedRef<T>
export function staticComputed<T>(
options: WritableComputedOptions<T>
): StaticWritableComputedRef<T>
export function staticComputed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
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
}
25 changes: 23 additions & 2 deletions packages/runtime-core/src/apiWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface WatchOptionsBase {
flush?: 'pre' | 'post' | 'sync'
onTrack?: ReactiveEffectOptions['onTrack']
onTrigger?: ReactiveEffectOptions['onTrigger']
lifecycle?: 'static' | 'instance'
}

export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
Expand Down Expand Up @@ -133,7 +134,14 @@ export function watch<T = any>(
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) {
Expand All @@ -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: `,
Expand Down Expand Up @@ -290,7 +309,9 @@ function doWatch(
scheduler
})

recordInstanceBoundEffect(runner)
if (!isStatic) {
recordInstanceBoundEffect(runner)
}

// initial run
if (cb) {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down