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

View File

@ -1,7 +1,7 @@
import { type EffectScope, effectScope, isReactive } from '@vue/reactivity' import { type EffectScope, effectScope, isReactive } from '@vue/reactivity'
import { isArray, isObject, isString } from '@vue/shared' import { isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode, insert, remove } from './dom/element' 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 { type Block, type Fragment, fragmentKey } from './apiRender'
import { warn } from './warning' 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 Block, type Fragment, fragmentKey } from './apiRender'
import { type EffectScope, effectScope } from '@vue/reactivity' import { type EffectScope, effectScope } from '@vue/reactivity'
import { createComment, createTextNode, insert, remove } from './dom/element' import { createComment, createTextNode, insert, remove } from './dom/element'
@ -12,6 +12,8 @@ export const createIf = (
b2?: BlockFn, b2?: BlockFn,
// hydrationNode?: Node, // hydrationNode?: Node,
): Fragment => { ): Fragment => {
let newValue: any
let oldValue: any
let branch: BlockFn | undefined let branch: BlockFn | undefined
let parent: ParentNode | undefined | null let parent: ParentNode | undefined | null
let block: Block | undefined let block: Block | undefined
@ -29,15 +31,14 @@ export const createIf = (
// setCurrentHydrationNode(hydrationNode!) // setCurrentHydrationNode(hydrationNode!)
// } // }
renderWatch( renderEffect(() => {
() => !!condition(), if ((newValue = !!condition()) !== oldValue) {
value => {
parent ||= anchor.parentNode parent ||= anchor.parentNode
if (block) { if (block) {
scope!.stop() scope!.stop()
remove(block, parent!) remove(block, parent!)
} }
if ((branch = value ? b1 : b2)) { if ((branch = (oldValue = newValue) ? b1 : b2)) {
scope = effectScope() scope = effectScope()
fragment.nodes = block = scope.run(branch)! fragment.nodes = block = scope.run(branch)!
parent && insert(block, parent, anchor) parent && insert(block, parent, anchor)
@ -45,9 +46,8 @@ export const createIf = (
scope = block = undefined scope = block = undefined
fragment.nodes = [] fragment.nodes = []
} }
}, }
{ immediate: true }, })
)
// TODO: SSR // TODO: SSR
// if (isHydrating) { // 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 { type ComponentInternalInstance, currentInstance } from './component'
import { pauseTracking, resetTracking } from '@vue/reactivity' import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling' import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import { renderWatch } from './renderWatch' import { renderEffect } from './renderEffect'
export type DirectiveModifiers<M extends string = string> = Record<M, boolean> export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
@ -100,8 +100,12 @@ export function withDirectives<T extends Node>(
// register source // register source
if (source) { if (source) {
// callback will be overridden by middleware if (dir.deep) {
renderWatch(source, NOOP, { deep: 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 FunctionalComponent,
type SetupFn, type SetupFn,
} from './component' } from './component'
export { renderEffect, renderWatch } from './renderWatch' export { renderEffect } from './renderEffect'
export { export {
watch, watch,
watchEffect, 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> const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null let currentFlushPromise: Promise<void> | null = null
function queueJob(job: SchedulerJob) { export function queueJob(job: SchedulerJob) {
if (!(job.flags! & SchedulerJobFlags.QUEUED)) { if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
if (job.id == null) { if (job.id == null) {
queue.push(job) queue.push(job)