]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(vapor): apiWatch
authorEvan You <evan@vuejs.org>
Tue, 10 Dec 2024 10:43:26 +0000 (18:43 +0800)
committerEvan You <evan@vuejs.org>
Tue, 10 Dec 2024 10:43:26 +0000 (18:43 +0800)
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/scheduler.ts
packages/runtime-vapor/__tests__/apiWatch.spec.ts
packages/runtime-vapor/__tests__/block.spec.ts
packages/runtime-vapor/src/apiCreateApp.ts
packages/runtime-vapor/src/component.ts

index d6a56d5876e03424c4a1f55205e5dfc05467f3e1..f9285005c2885a0f6337d1c24e97a98243021df0 100644 (file)
@@ -494,7 +494,7 @@ export {
   validateProps,
 } from './componentProps'
 export { baseEmit, isEmitListener } from './componentEmits'
-export { type SchedulerJob, queueJob } from './scheduler'
+export { type SchedulerJob, queueJob, flushOnAppMount } from './scheduler'
 export {
   type ComponentInternalOptions,
   type GenericComponentInstance,
index a21476de2cb48cda994220d3ef2ea7a4f13744e3..0bedefd5d5d1975f4cf01d24b43f768cae1b26c0 100644 (file)
@@ -45,7 +45,7 @@ import {
   type SchedulerJob,
   SchedulerJobFlags,
   type SchedulerJobs,
-  flushPostFlushCbs,
+  flushOnAppMount,
   flushPreFlushCbs,
   queueJob,
   queuePostFlushCb,
@@ -2357,7 +2357,6 @@ function baseCreateRenderer(
     return teleportEnd ? hostNextSibling(teleportEnd) : el
   }
 
-  let isFlushing = false
   const render: RootRenderFunction = (vnode, container, namespace) => {
     if (vnode == null) {
       if (container._vnode) {
@@ -2375,12 +2374,7 @@ function baseCreateRenderer(
       )
     }
     container._vnode = vnode
-    if (!isFlushing) {
-      isFlushing = true
-      flushPreFlushCbs()
-      flushPostFlushCbs()
-      isFlushing = false
-    }
+    flushOnAppMount()
   }
 
   const internals: RendererInternals = {
@@ -2449,7 +2443,7 @@ function baseCreateRenderer(
     createApp: createAppAPI(
       mountApp,
       unmountApp,
-      getComponentPublicInstance,
+      getComponentPublicInstance as any,
       render,
     ),
   }
index d657f56e55ae568e1fc50372f6476e769d219d2a..a407df467f40ede2e2e6bc9080afc2b1bd63fb00 100644 (file)
@@ -217,6 +217,19 @@ export function flushPostFlushCbs(seen?: CountMap): void {
   }
 }
 
+let isFlushing = false
+/**
+ * @internal
+ */
+export function flushOnAppMount(): void {
+  if (!isFlushing) {
+    isFlushing = true
+    flushPreFlushCbs()
+    flushPostFlushCbs()
+    isFlushing = false
+  }
+}
+
 const getId = (job: SchedulerJob): number =>
   job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
 
index 0ed45e46aacc3d33d7410b4e0c26e3bf24bad552..c76ce2f2cf22b8b461d59befaf52b059aed7019c 100644 (file)
-import type { Ref } from '@vue/reactivity'
 import {
-  EffectScope,
+  currentInstance,
+  effectScope,
   nextTick,
-  onWatcherCleanup,
+  onMounted,
+  onUpdated,
   ref,
+  watch,
   watchEffect,
-  watchSyncEffect,
-} from '../src'
-
-describe.todo('watchEffect and onWatcherCleanup', () => {
-  test('basic', async () => {
-    let dummy = 0
-    let source: Ref<number>
-    const scope = new EffectScope()
-
-    scope.run(() => {
-      source = ref(0)
-      watchEffect(onCleanup => {
-        source.value
-
-        onCleanup(() => (dummy += 2))
-        onWatcherCleanup(() => (dummy += 3))
-        onWatcherCleanup(() => (dummy += 5))
+} from '@vue/runtime-dom'
+import { createComponent, defineVaporComponent, renderEffect } from '../src'
+import { makeRender } from './_utils'
+import type { VaporComponentInstance } from '../src/component'
+
+const define = makeRender()
+
+// only need to port test cases related to in-component usage
+describe('apiWatch', () => {
+  // #7030
+  it.todo(
+    // need if support
+    'should not fire on child component unmount w/ flush: pre',
+    async () => {
+      const visible = ref(true)
+      const cb = vi.fn()
+      const Parent = defineVaporComponent({
+        props: ['visible'],
+        setup() {
+          // @ts-expect-error
+          return visible.value ? h(Comp) : null
+        },
+      })
+      const Comp = {
+        setup() {
+          watch(visible, cb, { flush: 'pre' })
+          return []
+        },
+      }
+      define(Parent).render({
+        visible: () => visible.value,
       })
+      expect(cb).not.toHaveBeenCalled()
+      visible.value = false
+      await nextTick()
+      expect(cb).not.toHaveBeenCalled()
+    },
+  )
+
+  // #7030
+  it('flush: pre watcher in child component should not fire before parent update', async () => {
+    const b = ref(0)
+    const calls: string[] = []
+
+    const Comp = {
+      setup() {
+        watch(
+          () => b.value,
+          val => {
+            calls.push('watcher child')
+          },
+          { flush: 'pre' },
+        )
+        renderEffect(() => {
+          b.value
+          calls.push('render child')
+        })
+        return []
+      },
+    }
+
+    const Parent = {
+      props: ['a'],
+      setup() {
+        watch(
+          () => b.value,
+          val => {
+            calls.push('watcher parent')
+          },
+          { flush: 'pre' },
+        )
+        renderEffect(() => {
+          b.value
+          calls.push('render parent')
+        })
+
+        return createComponent(Comp)
+      },
+    }
+
+    define(Parent).render({
+      a: () => b.value,
     })
+
+    expect(calls).toEqual(['render parent', 'render child'])
+
+    b.value++
     await nextTick()
-    expect(dummy).toBe(0)
+    expect(calls).toEqual([
+      'render parent',
+      'render child',
+      'watcher parent',
+      'render parent',
+      'watcher child',
+      'render child',
+    ])
+  })
+
+  // #1763
+  it('flush: pre watcher watching props should fire before child update', async () => {
+    const a = ref(0)
+    const b = ref(0)
+    const c = ref(0)
+    const calls: string[] = []
+
+    const Comp = {
+      props: ['a', 'b'],
+      setup(props: any) {
+        watch(
+          () => props.a + props.b,
+          () => {
+            calls.push('watcher 1')
+            c.value++
+          },
+          { flush: 'pre' },
+        )
 
-    scope.run(() => {
-      source.value++
+        // #1777 chained pre-watcher
+        watch(
+          c,
+          () => {
+            calls.push('watcher 2')
+          },
+          { flush: 'pre' },
+        )
+        renderEffect(() => {
+          c.value
+          calls.push('render')
+        })
+        return []
+      },
+    }
+
+    define(Comp).render({
+      a: () => a.value,
+      b: () => b.value,
     })
+
+    expect(calls).toEqual(['render'])
+
+    // both props are updated
+    // should trigger pre-flush watcher first and only once
+    // then trigger child render
+    a.value++
+    b.value++
     await nextTick()
-    expect(dummy).toBe(10)
+    expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
+  })
+
+  // #5721
+  it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
+    const count = ref(0)
+    const calls: string[] = []
+    const App = {
+      setup() {
+        watch(
+          count,
+          () => {
+            calls.push('watch ' + count.value)
+          },
+          { flush: 'pre' },
+        )
+        onMounted(() => {
+          calls.push('mounted')
+        })
+        // mutate multiple times
+        count.value++
+        count.value++
+        count.value++
+        return []
+      },
+    }
+    define(App).render()
+    expect(calls).toMatchObject(['watch 3', 'mounted'])
+  })
+
+  // #1852
+  it.todo(
+    // need if + templateRef
+    'flush: post watcher should fire after template refs updated',
+    async () => {
+      const toggle = ref(false)
+      let dom: Element | null = null
 
-    scope.run(() => {
-      source.value++
+      const App = {
+        setup() {
+          const domRef = ref<Element | null>(null)
+
+          watch(
+            toggle,
+            () => {
+              dom = domRef.value
+            },
+            { flush: 'post' },
+          )
+
+          return () => {
+            // @ts-expect-error
+            return toggle.value ? h('p', { ref: domRef }) : null
+          }
+        },
+      }
+
+      // @ts-expect-error
+      render(h(App), nodeOps.createElement('div'))
+      expect(dom).toBe(null)
+
+      toggle.value = true
+      await nextTick()
+      expect(dom!.tagName).toBe('P')
+    },
+  )
+
+  test('should not leak `this.proxy` to setup()', () => {
+    const source = vi.fn()
+
+    const Comp = defineVaporComponent({
+      setup() {
+        watch(source, () => {})
+        return []
+      },
     })
-    await nextTick()
-    expect(dummy).toBe(20)
 
-    scope.stop()
-    await nextTick()
-    expect(dummy).toBe(30)
+    define(Comp).render()
+
+    // should not have any arguments
+    expect(source.mock.calls[0]).toMatchObject([])
   })
 
-  test('nested call to watchEffect', async () => {
-    let dummy = 0
-    let source: Ref<number>
-    let double: Ref<number>
-    const scope = new EffectScope()
-
-    scope.run(() => {
-      source = ref(0)
-      double = ref(0)
-      watchEffect(() => {
-        double.value = source.value * 2
-        onWatcherCleanup(() => (dummy += 2))
-      })
-      watchSyncEffect(() => {
-        double.value
-        onWatcherCleanup(() => (dummy += 3))
-      })
+  // #2728
+  test('pre watcher callbacks should not track dependencies', async () => {
+    const a = ref(0)
+    const b = ref(0)
+    const updated = vi.fn()
+
+    const Comp = defineVaporComponent({
+      props: ['a'],
+      setup(props) {
+        onUpdated(updated)
+        watch(
+          () => props.a,
+          () => {
+            b.value
+          },
+        )
+        renderEffect(() => {
+          props.a
+        })
+        return []
+      },
     })
+
+    define(Comp).render({
+      a: () => a.value,
+    })
+
+    a.value++
     await nextTick()
-    expect(dummy).toBe(0)
+    expect(updated).toHaveBeenCalledTimes(1)
 
-    scope.run(() => source.value++)
+    b.value++
     await nextTick()
-    expect(dummy).toBe(5)
+    // should not track b as dependency of Child
+    expect(updated).toHaveBeenCalledTimes(1)
+  })
+
+  // #4158
+  test('watch should not register in owner component if created inside detached scope', () => {
+    let instance: VaporComponentInstance
+    const Comp = {
+      setup() {
+        instance = currentInstance as VaporComponentInstance
+        effectScope(true).run(() => {
+          watch(
+            () => 1,
+            () => {},
+          )
+        })
+        return []
+      },
+    }
+    define(Comp).render()
+    // should not record watcher in detached scope
+    expect(instance!.scope.effects.length).toBe(0)
+  })
 
-    scope.run(() => source.value++)
+  test('watchEffect should keep running if created in a detached scope', async () => {
+    const trigger = ref(0)
+    let countWE = 0
+    let countW = 0
+    const Comp = {
+      setup() {
+        effectScope(true).run(() => {
+          watchEffect(() => {
+            trigger.value
+            countWE++
+          })
+          watch(trigger, () => countW++)
+        })
+        return []
+      },
+    }
+    const { app } = define(Comp).render()
+    // only watchEffect as ran so far
+    expect(countWE).toBe(1)
+    expect(countW).toBe(0)
+    trigger.value++
     await nextTick()
-    expect(dummy).toBe(10)
+    // both watchers run while component is mounted
+    expect(countWE).toBe(2)
+    expect(countW).toBe(1)
 
-    scope.stop()
+    app.unmount()
+    await nextTick()
+    trigger.value++
     await nextTick()
-    expect(dummy).toBe(15)
+    // both watchers run again event though component has been unmounted
+    expect(countWE).toBe(3)
+    expect(countW).toBe(2)
   })
 })
index ddd4035f600ae4fc17f53b957681c1304d5eb88f..306a1281010abf30d04f353ecc1b97c2bf5551bf 100644 (file)
@@ -5,7 +5,7 @@ const node2 = document.createTextNode('node2')
 const node3 = document.createTextNode('node3')
 const anchor = document.createTextNode('anchor')
 
-describe('node ops', () => {
+describe('block + node ops', () => {
   test('normalizeBlock', () => {
     expect(normalizeBlock([node1, node2, node3])).toEqual([node1, node2, node3])
     expect(normalizeBlock([node1, [node2, [node3]]])).toEqual([
index 74910af36afd33b58916d6b52807f95e7abcfde8..f88db3d32d44400a7a5ccb422bd1f68af0531938 100644 (file)
@@ -11,6 +11,7 @@ import {
   type AppUnmountFn,
   type CreateAppFunction,
   createAppAPI,
+  flushOnAppMount,
   normalizeContainer,
   warn,
 } from '@vue/runtime-dom'
@@ -23,6 +24,7 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
   if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
     container.textContent = ''
   }
+
   const instance = createComponent(
     app._component,
     app._props as RawProps,
@@ -30,7 +32,10 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
     false,
     app._context,
   )
+
   mountComponent(instance, container)
+  flushOnAppMount()
+
   return instance
 }
 
index a51d0d6923485274d9add871b9ceb714fbbe7aef..2e0a98171670603c2ac906412e40a428117b2d9a 100644 (file)
@@ -18,6 +18,7 @@ import {
   nextUid,
   popWarningContext,
   pushWarningContext,
+  queuePostFlushCb,
   registerHMR,
   simpleSetCurrentInstance,
   startMeasure,
@@ -453,10 +454,8 @@ export function mountComponent(
   if (!instance.isMounted) {
     if (instance.bm) invokeArrayFns(instance.bm)
     insert(instance.block, parent, anchor)
-    // TODO queuePostFlushCb(() => {
-    if (instance.m) invokeArrayFns(instance.m)
+    if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
     instance.isMounted = true
-    // })
   } else {
     insert(instance.block, parent, anchor)
   }
@@ -479,10 +478,10 @@ export function unmountComponent(
       unmountComponent(c)
     }
     if (parent) remove(instance.block, parent)
-    // TODO queuePostFlushCb(() => {
-    if (instance.um) invokeArrayFns(instance.um)
+    if (instance.um) {
+      queuePostFlushCb(() => invokeArrayFns(instance.um!))
+    }
     instance.isUnmounted = true
-    // })
   } else if (parent) {
     remove(instance.block, parent)
   }