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

This commit is contained in:
Rizumu Ayaka 2024-03-18 20:13:40 +08:00 committed by GitHub
parent 46761880e9
commit 64e83689a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 86 additions and 170 deletions

View File

@ -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',

View File

@ -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'

View File

@ -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) {

View File

@ -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)
}
}

View File

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

View File

@ -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()
}

View File

@ -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
}

View File

@ -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)