feat(runtime-vapor): createIf (#95)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Rizumu Ayaka 2024-01-19 16:38:41 +08:00 committed by GitHub
parent 15e0419106
commit 1e0070c208
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 211 additions and 20 deletions

View File

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

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

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

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

View File

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

View File

@ -50,3 +50,4 @@ export * from './dom'
export * from './directives/vShow'
export * from './apiLifecycle'
export { getCurrentInstance, type ComponentInternalInstance } from './component'
export * from './if'

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

View 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(() => {

View 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