]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): createIf (#95)
authorRizumu Ayaka <rizumu@ayaka.moe>
Fri, 19 Jan 2024 08:38:41 +0000 (16:38 +0800)
committerGitHub <noreply@github.com>
Fri, 19 Jan 2024 08:38:41 +0000 (16:38 +0800)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
packages/runtime-vapor/__tests__/if.spec.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiLifecycle.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/dom.ts
packages/runtime-vapor/src/if.ts [new file with mode: 0644]
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/render.ts
packages/runtime-vapor/src/renderWatch.ts
packages/runtime-vapor/src/template.ts

diff --git a/packages/runtime-vapor/__tests__/if.spec.ts b/packages/runtime-vapor/__tests__/if.spec.ts
new file mode 100644 (file)
index 0000000..790e36c
--- /dev/null
@@ -0,0 +1,112 @@
+import { defineComponent } from 'vue'
+import {
+  children,
+  createIf,
+  insert,
+  nextTick,
+  ref,
+  render,
+  renderEffect,
+  setText,
+  template,
+} from '../src'
+import { NOOP } from '@vue/shared'
+import type { Mock } from 'vitest'
+
+let host: HTMLElement
+
+const initHost = () => {
+  host = document.createElement('div')
+  host.setAttribute('id', 'host')
+  document.body.appendChild(host)
+}
+beforeEach(() => {
+  initHost()
+})
+afterEach(() => {
+  host.remove()
+})
+
+describe('createIf', () => {
+  test('basic', async () => {
+    // mock this template:
+    //  <div>
+    //    <p v-if="counter">{{counter}}</p>
+    //    <p v-else>zero</p>
+    //  </div>
+
+    let spyIfFn: Mock<any, any>
+    let spyElseFn: Mock<any, any>
+
+    let add = NOOP
+    let reset = NOOP
+
+    // templates can be reused through caching.
+    const t0 = template('<div></div>')
+    const t1 = template('<p></p>')
+    const t2 = template('<p>zero</p>')
+
+    const component = defineComponent({
+      setup() {
+        const counter = ref(0)
+        add = () => counter.value++
+        reset = () => (counter.value = 0)
+
+        // render
+        return (() => {
+          const n0 = t0()
+          const {
+            0: [n1],
+          } = children(n0)
+
+          insert(
+            createIf(
+              () => counter.value,
+              // v-if
+              (spyIfFn ||= vi.fn(() => {
+                const n2 = t1()
+                const {
+                  0: [n3],
+                } = children(n2)
+                renderEffect(() => {
+                  setText(n3, void 0, counter.value)
+                })
+                return n2
+              })),
+              // v-else
+              (spyElseFn ||= vi.fn(() => {
+                const n4 = t2()
+                return n4
+              })),
+            ),
+            n1,
+          )
+          return n0
+        })()
+      },
+    })
+    render(component as any, {}, '#host')
+
+    expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
+    expect(spyIfFn!).toHaveBeenCalledTimes(0)
+    expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+    add()
+    await nextTick()
+    expect(host.innerHTML).toBe('<div><p>1</p><!--if--></div>')
+    expect(spyIfFn!).toHaveBeenCalledTimes(1)
+    expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+    add()
+    await nextTick()
+    expect(host.innerHTML).toBe('<div><p>2</p><!--if--></div>')
+    expect(spyIfFn!).toHaveBeenCalledTimes(1)
+    expect(spyElseFn!).toHaveBeenCalledTimes(1)
+
+    reset()
+    await nextTick()
+    expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
+    expect(spyIfFn!).toHaveBeenCalledTimes(1)
+    expect(spyElseFn!).toHaveBeenCalledTimes(2)
+  })
+})
index 5c270d1ca94871ab4d0e2b6ec7972ff551afa43b..831a94b3820e441d72f15438377bc9eacae0b008 100644 (file)
@@ -2,7 +2,6 @@ import {
   type ComponentInternalInstance,
   currentInstance,
   setCurrentInstance,
-  unsetCurrentInstance,
 } from './component'
 import { warn } from './warning'
 import { pauseTracking, resetTracking } from '@vue/reactivity'
@@ -25,9 +24,9 @@ export const injectHook = (
           return
         }
         pauseTracking()
-        setCurrentInstance(target)
+        const reset = setCurrentInstance(target)
         const res = callWithAsyncErrorHandling(hook, target, type, args)
-        unsetCurrentInstance()
+        reset()
         resetTracking()
         return res
       })
index ab2e49c3af1731bbdc39600c09562c8b75f10121..51be7f4ba1b80f65390a59920108fb103c1350f2 100644 (file)
@@ -122,10 +122,17 @@ export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
   currentInstance
 
 export const setCurrentInstance = (instance: ComponentInternalInstance) => {
+  const prev = currentInstance
   currentInstance = instance
+  instance.scope.on()
+  return () => {
+    instance.scope.off()
+    currentInstance = prev
+  }
 }
 
 export const unsetCurrentInstance = () => {
+  currentInstance?.scope.off()
   currentInstance = null
 }
 
index 47d888fae4c552e07ef1795d62fc376be1409976..1dda46f2170a77f62c77675ae8ce8c0fc276c2f8 100644 (file)
@@ -6,11 +6,7 @@ import {
 } from '@vue/shared'
 import type { Block, ParentBlock } from './render'
 
-export function insert(
-  block: Block,
-  parent: ParentNode,
-  anchor: Node | null = null,
-) {
+export function insert(block: Block, parent: Node, anchor: Node | null = null) {
   // if (!isHydrating) {
   if (block instanceof Node) {
     parent.insertBefore(block, anchor)
diff --git a/packages/runtime-vapor/src/if.ts b/packages/runtime-vapor/src/if.ts
new file mode 100644 (file)
index 0000000..155658c
--- /dev/null
@@ -0,0 +1,60 @@
+import { renderWatch } from './renderWatch'
+import type { BlockFn, Fragment } from './render'
+import { effectScope, onEffectCleanup } from '@vue/reactivity'
+import { insert, remove } from './dom'
+
+export const createIf = (
+  condition: () => any,
+  b1: BlockFn,
+  b2?: BlockFn,
+  // hydrationNode?: Node,
+): Fragment => {
+  let branch: BlockFn | undefined
+  let parent: ParentNode | undefined | null
+  const anchor = __DEV__
+    ? // eslint-disable-next-line no-restricted-globals
+      document.createComment('if')
+    : // eslint-disable-next-line no-restricted-globals
+      document.createTextNode('')
+  const fragment: Fragment = { nodes: [], anchor }
+
+  // TODO: SSR
+  // if (isHydrating) {
+  //   parent = hydrationNode!.parentNode
+  //   setCurrentHydrationNode(hydrationNode!)
+  // }
+
+  renderWatch(
+    () => !!condition(),
+    (value) => {
+      parent ||= anchor.parentNode
+      if ((branch = value ? b1 : b2)) {
+        let scope = effectScope()
+        let block = scope.run(branch)!
+
+        if (block instanceof DocumentFragment) {
+          block = Array.from(block.childNodes)
+        }
+        fragment.nodes = block
+
+        parent && insert(block, parent, anchor)
+
+        onEffectCleanup(() => {
+          parent ||= anchor.parentNode
+          scope.stop()
+          remove(block, parent!)
+        })
+      } else {
+        fragment.nodes = []
+      }
+    },
+    { immediate: true },
+  )
+
+  // TODO: SSR
+  // if (isHydrating) {
+  //   parent!.insertBefore(anchor, currentHydrationNode)
+  // }
+
+  return fragment
+}
index 805fbae0eb995381ae890381614cbd359a16200a..0fc7be64a1296b1cc1a42f15c60c22d49d111f03 100644 (file)
@@ -50,3 +50,4 @@ export * from './dom'
 export * from './directives/vShow'
 export * from './apiLifecycle'
 export { getCurrentInstance, type ComponentInternalInstance } from './component'
+export * from './if'
index bce3a2be75f6ee48b76c6882e7140b10b527c537..22c3b7fbfd5be4d05cac61100517ab038277caba 100644 (file)
@@ -14,7 +14,7 @@ import { insert, remove } from './dom'
 export type Block = Node | Fragment | Block[]
 export type ParentBlock = ParentNode | Node[]
 export type Fragment = { nodes: Block; anchor: Node }
-export type BlockFn = (props: any, ctx: any) => Block
+export type BlockFn = (props?: any) => Block
 
 let isRenderingActivity = false
 export function getIsRendering() {
@@ -44,7 +44,7 @@ export function mountComponent(
 ) {
   instance.container = container
 
-  setCurrentInstance(instance)
+  const reset = setCurrentInstance(instance)
   const block = instance.scope.run(() => {
     const { component, props } = instance
     const ctx = { expose: () => {} }
@@ -82,7 +82,7 @@ export function mountComponent(
   // hook: mounted
   invokeDirectiveHook(instance, 'mounted')
   m && invokeArrayFns(m)
-  unsetCurrentInstance()
+  reset()
 
   return instance
 }
index e5103d716fe7ab59856a5307183e1adc308ae873..c0167cdf51c6f5a85ad665929d9d165a0fe90d6e 100644 (file)
@@ -3,10 +3,13 @@ import {
   type BaseWatchMiddleware,
   type BaseWatchOptions,
   baseWatch,
-  getCurrentScope,
 } from '@vue/reactivity'
-import { NOOP, invokeArrayFns, remove } from '@vue/shared'
-import { type ComponentInternalInstance, currentInstance } from './component'
+import { NOOP, extend, invokeArrayFns, remove } from '@vue/shared'
+import {
+  type ComponentInternalInstance,
+  getCurrentInstance,
+  setCurrentInstance,
+} from './component'
 import {
   createVaporRenderingScheduler,
   queuePostRenderEffect,
@@ -15,6 +18,12 @@ import { handleError as handleErrorWithInstance } from './errorHandling'
 import { warn } from './warning'
 import { invokeDirectiveHook } from './directive'
 
+interface RenderWatchOptions {
+  immediate?: boolean
+  deep?: boolean
+  once?: boolean
+}
+
 type WatchStopHandle = () => void
 
 export function renderEffect(effect: () => void): WatchStopHandle {
@@ -24,20 +33,25 @@ export function renderEffect(effect: () => void): WatchStopHandle {
 export function renderWatch(
   source: any,
   cb: (value: any, oldValue: any) => void,
+  options?: RenderWatchOptions,
 ): WatchStopHandle {
-  return doWatch(source as any, cb)
+  return doWatch(source as any, cb, options)
 }
 
-function doWatch(source: any, cb?: any): WatchStopHandle {
-  const extendOptions: BaseWatchOptions = {}
+function doWatch(
+  source: any,
+  cb?: any,
+  options?: RenderWatchOptions,
+): WatchStopHandle {
+  const extendOptions: BaseWatchOptions =
+    cb && options ? extend({}, options) : {}
 
   if (__DEV__) extendOptions.onWarn = warn
 
   // TODO: SSR
   // if (__SSR__) {}
 
-  const instance =
-    getCurrentScope() === currentInstance?.scope ? currentInstance : null
+  const instance = getCurrentInstance()
 
   extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) =>
     handleErrorWithInstance(err, instance, type)
@@ -78,8 +92,10 @@ const createMiddleware =
         instance.isUpdating = true
       }
 
+      const reset = setCurrentInstance(instance)
       // run callback
       value = next()
+      reset()
 
       if (isFirstEffect) {
         queuePostRenderEffect(() => {
index f75ab8699a411f93493fca9eaa072c88acf9d2cb..8b505a6ec88f317c6c3f218c8254c4227817612f 100644 (file)
@@ -10,7 +10,7 @@ export const template = (str: string): (() => DocumentFragment) => {
       // first render: insert the node directly.
       // this removes it from the template fragment to avoid keeping two copies
       // of the inserted tree in memory, even if the template is used only once.
-      return (node = t.content)
+      return (node = t.content).cloneNode(true) as DocumentFragment
     } else {
       // repeated renders: clone from cache. This is more performant and
       // efficient when dealing with big lists where the template is repeated