Browse Source

refactor(runtime-vapor): `renderEffect` based on `ReactiveEffect` + remove `renderWatch` (#155)

Rizumu Ayaka 2 years ago
parent
commit
64e83689a0

+ 7 - 38
packages/runtime-vapor/__tests__/renderWatch.spec.ts → packages/runtime-vapor/__tests__/renderEffect.spec.ts

@@ -1,11 +1,10 @@
+import { onEffectCleanup } from '@vue/reactivity'
 import {
   nextTick,
   onBeforeUpdate,
   onUpdated,
-  onWatcherCleanup,
   ref,
   renderEffect,
-  renderWatch,
   template,
   watchEffect,
   watchPostEffect,
@@ -31,8 +30,8 @@ const createDemo = (setupFn: () => any, renderFn: (ctx: any) => any) =>
     },
   })
 
-describe('renderWatch', () => {
-  test('effect', async () => {
+describe('renderEffect', () => {
+  test('basic', async () => {
     let dummy: any
     const source = ref(0)
     renderEffect(() => {
@@ -58,26 +57,6 @@ describe('renderWatch', () => {
     expect(dummy).toBe(3)
   })
 
-  test('watch', async () => {
-    let dummy: any
-    const source = ref(0)
-    renderWatch(source, () => {
-      dummy = source.value
-    })
-    await nextTick()
-    expect(dummy).toBe(undefined)
-
-    source.value++
-    expect(dummy).toBe(undefined)
-    await nextTick()
-    expect(dummy).toBe(1)
-
-    source.value++
-    expect(dummy).toBe(1)
-    await nextTick()
-    expect(dummy).toBe(2)
-  })
-
   test('should run with the scheduling order', async () => {
     const calls: string[] = []
 
@@ -101,17 +80,17 @@ describe('renderWatch', () => {
         watchPostEffect(() => {
           const current = source.value
           calls.push(`post ${current}`)
-          onWatcherCleanup(() => calls.push(`post cleanup ${current}`))
+          onEffectCleanup(() => calls.push(`post cleanup ${current}`))
         })
         watchEffect(() => {
           const current = source.value
           calls.push(`pre ${current}`)
-          onWatcherCleanup(() => calls.push(`pre cleanup ${current}`))
+          onEffectCleanup(() => calls.push(`pre cleanup ${current}`))
         })
         watchSyncEffect(() => {
           const current = source.value
           calls.push(`sync ${current}`)
-          onWatcherCleanup(() => calls.push(`sync cleanup ${current}`))
+          onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
         })
         return { source, change, renderSource, changeRender }
       },
@@ -121,15 +100,8 @@ describe('renderWatch', () => {
         renderEffect(() => {
           const current = _ctx.renderSource
           calls.push(`renderEffect ${current}`)
-          onWatcherCleanup(() => calls.push(`renderEffect cleanup ${current}`))
+          onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
         })
-        renderWatch(
-          () => _ctx.renderSource,
-          value => {
-            calls.push(`renderWatch ${value}`)
-            onWatcherCleanup(() => calls.push(`renderWatch cleanup ${value}`))
-          },
-        )
       },
     ).render()
     const { change, changeRender } = instance.setupState as any
@@ -151,7 +123,6 @@ describe('renderWatch', () => {
       'beforeUpdate 1',
       'renderEffect cleanup 0',
       'renderEffect 1',
-      'renderWatch 1',
       'post cleanup 0',
       'post 1',
       'updated 1',
@@ -172,8 +143,6 @@ describe('renderWatch', () => {
       'beforeUpdate 2',
       'renderEffect cleanup 1',
       'renderEffect 2',
-      'renderWatch cleanup 1',
-      'renderWatch 2',
       'post cleanup 1',
       'post 2',
       'updated 2',

+ 1 - 1
packages/runtime-vapor/src/apiCreateFor.ts

@@ -1,7 +1,7 @@
 import { type EffectScope, effectScope, isReactive } from '@vue/reactivity'
 import { isArray, isObject, isString } from '@vue/shared'
 import { createComment, createTextNode, insert, remove } from './dom/element'
-import { renderEffect } from './renderWatch'
+import { renderEffect } from './renderEffect'
 import { type Block, type Fragment, fragmentKey } from './apiRender'
 import { warn } from './warning'
 

+ 8 - 8
packages/runtime-vapor/src/apiCreateIf.ts

@@ -1,4 +1,4 @@
-import { renderWatch } from './renderWatch'
+import { renderEffect } from './renderEffect'
 import { type Block, type Fragment, fragmentKey } from './apiRender'
 import { type EffectScope, effectScope } from '@vue/reactivity'
 import { createComment, createTextNode, insert, remove } from './dom/element'
@@ -12,6 +12,8 @@ export const createIf = (
   b2?: BlockFn,
   // hydrationNode?: Node,
 ): Fragment => {
+  let newValue: any
+  let oldValue: any
   let branch: BlockFn | undefined
   let parent: ParentNode | undefined | null
   let block: Block | undefined
@@ -29,15 +31,14 @@ export const createIf = (
   //   setCurrentHydrationNode(hydrationNode!)
   // }
 
-  renderWatch(
-    () => !!condition(),
-    value => {
+  renderEffect(() => {
+    if ((newValue = !!condition()) !== oldValue) {
       parent ||= anchor.parentNode
       if (block) {
         scope!.stop()
         remove(block, parent!)
       }
-      if ((branch = value ? b1 : b2)) {
+      if ((branch = (oldValue = newValue) ? b1 : b2)) {
         scope = effectScope()
         fragment.nodes = block = scope.run(branch)!
         parent && insert(block, parent, anchor)
@@ -45,9 +46,8 @@ export const createIf = (
         scope = block = undefined
         fragment.nodes = []
       }
-    },
-    { immediate: true },
-  )
+    }
+  })
 
   // TODO: SSR
   // if (isHydrating) {

+ 9 - 5
packages/runtime-vapor/src/directives.ts

@@ -1,8 +1,8 @@
-import { NOOP, isFunction } from '@vue/shared'
+import { isFunction } from '@vue/shared'
 import { type ComponentInternalInstance, currentInstance } from './component'
-import { pauseTracking, resetTracking } from '@vue/reactivity'
+import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
 import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
-import { renderWatch } from './renderWatch'
+import { renderEffect } from './renderEffect'
 
 export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
 
@@ -100,8 +100,12 @@ export function withDirectives<T extends Node>(
 
     // register source
     if (source) {
-      // callback will be overridden by middleware
-      renderWatch(source, NOOP, { deep: dir.deep })
+      if (dir.deep) {
+        const deep = dir.deep === true ? undefined : dir.deep
+        const baseSource = source
+        source = () => traverse(baseSource(), deep)
+      }
+      renderEffect(source)
     }
   }
 

+ 1 - 1
packages/runtime-vapor/src/index.ts

@@ -46,7 +46,7 @@ export {
   type FunctionalComponent,
   type SetupFn,
 } from './component'
-export { renderEffect, renderWatch } from './renderWatch'
+export { renderEffect } from './renderEffect'
 export {
   watch,
   watchEffect,

+ 59 - 0
packages/runtime-vapor/src/renderEffect.ts

@@ -0,0 +1,59 @@
+import { EffectFlags, ReactiveEffect, type SchedulerJob } from '@vue/reactivity'
+import { invokeArrayFns } from '@vue/shared'
+import { getCurrentInstance, setCurrentInstance } from './component'
+import { queueJob, queuePostRenderEffect } from './scheduler'
+import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
+import { invokeDirectiveHook } from './directives'
+
+export function renderEffect(cb: () => void) {
+  const instance = getCurrentInstance()
+
+  let effect: ReactiveEffect
+
+  const job: SchedulerJob = () => {
+    if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) {
+      return
+    }
+
+    if (instance?.isMounted && !instance.isUpdating) {
+      instance.isUpdating = true
+
+      const { bu, u, dirs } = instance
+      // beforeUpdate hook
+      if (bu) {
+        invokeArrayFns(bu)
+      }
+      if (dirs) {
+        invokeDirectiveHook(instance, 'beforeUpdate')
+      }
+
+      effect.run()
+
+      queuePostRenderEffect(() => {
+        instance.isUpdating = false
+        if (dirs) {
+          invokeDirectiveHook(instance, 'updated')
+        }
+        // updated hook
+        if (u) {
+          queuePostRenderEffect(u)
+        }
+      })
+    } else {
+      effect.run()
+    }
+  }
+
+  effect = new ReactiveEffect(() => {
+    const reset = instance && setCurrentInstance(instance)
+    callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION)
+    reset?.()
+  })
+
+  effect.scheduler = () => {
+    if (instance) job.id = instance.uid
+    queueJob(job)
+  }
+
+  effect.run()
+}

+ 0 - 116
packages/runtime-vapor/src/renderWatch.ts

@@ -1,116 +0,0 @@
-import {
-  type BaseWatchErrorCodes,
-  type BaseWatchMiddleware,
-  type BaseWatchOptions,
-  baseWatch,
-} from '@vue/reactivity'
-import { NOOP, extend, invokeArrayFns, remove } from '@vue/shared'
-import {
-  type ComponentInternalInstance,
-  getCurrentInstance,
-  setCurrentInstance,
-} from './component'
-import {
-  createVaporRenderingScheduler,
-  queuePostRenderEffect,
-} from './scheduler'
-import { handleError as handleErrorWithInstance } from './errorHandling'
-import { warn } from './warning'
-import { invokeDirectiveHook } from './directives'
-
-interface RenderWatchOptions {
-  immediate?: boolean
-  deep?: boolean
-  once?: boolean
-}
-
-type WatchStopHandle = () => void
-
-export function renderEffect(effect: () => void): WatchStopHandle {
-  return doWatch(effect)
-}
-
-export function renderWatch(
-  source: any,
-  cb: (value: any, oldValue: any) => void,
-  options?: RenderWatchOptions,
-): WatchStopHandle {
-  return doWatch(source as any, cb, options)
-}
-
-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 = getCurrentInstance()
-  extend(extendOptions, {
-    onError: (err: unknown, type: BaseWatchErrorCodes) =>
-      handleErrorWithInstance(err, instance, type),
-    scheduler: createVaporRenderingScheduler(instance),
-    middleware: createMiddleware(instance),
-  })
-  let effect = baseWatch(source, cb, extendOptions)
-
-  const unwatch = !effect
-    ? NOOP
-    : () => {
-        effect!.stop()
-        if (instance && instance.scope) {
-          remove(instance.scope.effects!, effect)
-        }
-      }
-
-  return unwatch
-}
-
-const createMiddleware =
-  (instance: ComponentInternalInstance | null): BaseWatchMiddleware =>
-  next => {
-    let value: unknown
-    // with lifecycle
-    if (instance && instance.isMounted) {
-      const { bu, u, dirs } = instance
-      // beforeUpdate hook
-      const isFirstEffect = !instance.isUpdating
-      if (isFirstEffect) {
-        if (bu) {
-          invokeArrayFns(bu)
-        }
-        if (dirs) {
-          invokeDirectiveHook(instance, 'beforeUpdate')
-        }
-        instance.isUpdating = true
-      }
-
-      const reset = setCurrentInstance(instance)
-      // run callback
-      value = next()
-      reset()
-
-      if (isFirstEffect) {
-        queuePostRenderEffect(() => {
-          instance.isUpdating = false
-          if (dirs) {
-            invokeDirectiveHook(instance, 'updated')
-          }
-          // updated hook
-          if (u) {
-            queuePostRenderEffect(u)
-          }
-        })
-      }
-    } else {
-      // is not mounted
-      value = next()
-    }
-    return value
-  }

+ 1 - 1
packages/runtime-vapor/src/scheduler.ts

@@ -32,7 +32,7 @@ let postFlushIndex = 0
 const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
 let currentFlushPromise: Promise<void> | null = null
 
-function queueJob(job: SchedulerJob) {
+export function queueJob(job: SchedulerJob) {
   if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
     if (job.id == null) {
       queue.push(job)