fix(watch): watchEffect clean-up with SSR (#12097)

close #11956
This commit is contained in:
skirtle 2024-10-04 09:09:23 +01:00 committed by GitHub
parent 6e4de8d75e
commit b094c72b3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 184 additions and 6 deletions

View File

@ -170,15 +170,14 @@ function doWatch(
if (__DEV__) baseWatchOptions.onWarn = warn if (__DEV__) baseWatchOptions.onWarn = warn
// immediate watcher or watchEffect
const runsImmediately = (cb && immediate) || (!cb && flush !== 'post')
let ssrCleanup: (() => void)[] | undefined let ssrCleanup: (() => void)[] | undefined
if (__SSR__ && isInSSRComponentSetup) { if (__SSR__ && isInSSRComponentSetup) {
if (flush === 'sync') { if (flush === 'sync') {
const ctx = useSSRContext()! const ctx = useSSRContext()!
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
} else if (!cb || immediate) { } else if (!runsImmediately) {
// immediately watch or watchEffect
baseWatchOptions.once = true
} else {
const watchStopHandle = () => {} const watchStopHandle = () => {}
watchStopHandle.stop = NOOP watchStopHandle.stop = NOOP
watchStopHandle.resume = NOOP watchStopHandle.resume = NOOP
@ -226,7 +225,14 @@ function doWatch(
const watchHandle = baseWatch(source, cb, baseWatchOptions) const watchHandle = baseWatch(source, cb, baseWatchOptions)
if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle) if (__SSR__ && isInSSRComponentSetup) {
if (ssrCleanup) {
ssrCleanup.push(watchHandle)
} else if (runsImmediately) {
watchHandle()
}
}
return watchHandle return watchHandle
} }

View File

@ -1,4 +1,12 @@
import { createSSRApp, defineComponent, h, ref, watch } from 'vue' import {
createSSRApp,
defineComponent,
h,
nextTick,
ref,
watch,
watchEffect,
} from 'vue'
import { type SSRContext, renderToString } from '../src' import { type SSRContext, renderToString } from '../src'
describe('ssr: watch', () => { describe('ssr: watch', () => {
@ -27,4 +35,168 @@ describe('ssr: watch', () => {
expect(html).toMatch('hello world') expect(html).toMatch('hello world')
}) })
test('should work with flush: sync and immediate: true', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watch(
text,
() => {
msg = text.value
},
{ flush: 'sync', immediate: true },
)
expect(msg).toBe('start')
text.value = 'changed'
expect(msg).toBe('changed')
text.value = 'changed again'
expect(msg).toBe('changed again')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles!.length).toBe(1)
expect(html).toMatch('changed again')
await nextTick()
expect(msg).toBe('changed again')
})
test('should run once with immediate: true', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watch(
text,
() => {
msg = String(text.value)
},
{ immediate: true },
)
text.value = 'changed'
expect(msg).toBe('start')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles).toBeUndefined()
expect(html).toMatch('start')
await nextTick()
expect(msg).toBe('start')
})
test('should run once with immediate: true and flush: post', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watch(
text,
() => {
msg = String(text.value)
},
{ immediate: true, flush: 'post' },
)
text.value = 'changed'
expect(msg).toBe('start')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles).toBeUndefined()
expect(html).toMatch('start')
await nextTick()
expect(msg).toBe('start')
})
})
describe('ssr: watchEffect', () => {
test('should run with flush: sync', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watchEffect(
() => {
msg = text.value
},
{ flush: 'sync' },
)
expect(msg).toBe('start')
text.value = 'changed'
expect(msg).toBe('changed')
text.value = 'changed again'
expect(msg).toBe('changed again')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles!.length).toBe(1)
expect(html).toMatch('changed again')
await nextTick()
expect(msg).toBe('changed again')
})
test('should run once with default flush (pre)', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watchEffect(() => {
msg = text.value
})
text.value = 'changed'
expect(msg).toBe('start')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles).toBeUndefined()
expect(html).toMatch('start')
await nextTick()
expect(msg).toBe('start')
})
test('should not run for flush: post', async () => {
const text = ref('start')
let msg = 'unchanged'
const App = defineComponent(() => {
watchEffect(
() => {
msg = text.value
},
{ flush: 'post' },
)
text.value = 'changed'
expect(msg).toBe('unchanged')
return () => h('div', null, msg)
})
const app = createSSRApp(App)
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
expect(ctx.__watcherHandles).toBeUndefined()
expect(html).toMatch('unchanged')
await nextTick()
expect(msg).toBe('unchanged')
})
}) })