Bläddra i källkod

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

close #11956
skirtle 1 år sedan
förälder
incheckning
b094c72b3d

+ 11 - 5
packages/runtime-core/src/apiWatch.ts

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

+ 173 - 1
packages/server-renderer/__tests__/ssrWatch.spec.ts

@@ -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'
 
 describe('ssr: watch', () => {
@@ -27,4 +35,168 @@ describe('ssr: watch', () => {
 
     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')
+  })
 })