mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): createIf (#95)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
15e0419106
commit
1e0070c208
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,7 +2,6 @@ import {
|
||||||
type ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
currentInstance,
|
currentInstance,
|
||||||
setCurrentInstance,
|
setCurrentInstance,
|
||||||
unsetCurrentInstance,
|
|
||||||
} from './component'
|
} from './component'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { pauseTracking, resetTracking } from '@vue/reactivity'
|
import { pauseTracking, resetTracking } from '@vue/reactivity'
|
||||||
|
@ -25,9 +24,9 @@ export const injectHook = (
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pauseTracking()
|
pauseTracking()
|
||||||
setCurrentInstance(target)
|
const reset = setCurrentInstance(target)
|
||||||
const res = callWithAsyncErrorHandling(hook, target, type, args)
|
const res = callWithAsyncErrorHandling(hook, target, type, args)
|
||||||
unsetCurrentInstance()
|
reset()
|
||||||
resetTracking()
|
resetTracking()
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
|
|
|
@ -122,10 +122,17 @@ export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
||||||
currentInstance
|
currentInstance
|
||||||
|
|
||||||
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
|
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
|
||||||
|
const prev = currentInstance
|
||||||
currentInstance = instance
|
currentInstance = instance
|
||||||
|
instance.scope.on()
|
||||||
|
return () => {
|
||||||
|
instance.scope.off()
|
||||||
|
currentInstance = prev
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unsetCurrentInstance = () => {
|
export const unsetCurrentInstance = () => {
|
||||||
|
currentInstance?.scope.off()
|
||||||
currentInstance = null
|
currentInstance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,7 @@ import {
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import type { Block, ParentBlock } from './render'
|
import type { Block, ParentBlock } from './render'
|
||||||
|
|
||||||
export function insert(
|
export function insert(block: Block, parent: Node, anchor: Node | null = null) {
|
||||||
block: Block,
|
|
||||||
parent: ParentNode,
|
|
||||||
anchor: Node | null = null,
|
|
||||||
) {
|
|
||||||
// if (!isHydrating) {
|
// if (!isHydrating) {
|
||||||
if (block instanceof Node) {
|
if (block instanceof Node) {
|
||||||
parent.insertBefore(block, anchor)
|
parent.insertBefore(block, anchor)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -50,3 +50,4 @@ export * from './dom'
|
||||||
export * from './directives/vShow'
|
export * from './directives/vShow'
|
||||||
export * from './apiLifecycle'
|
export * from './apiLifecycle'
|
||||||
export { getCurrentInstance, type ComponentInternalInstance } from './component'
|
export { getCurrentInstance, type ComponentInternalInstance } from './component'
|
||||||
|
export * from './if'
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { insert, remove } from './dom'
|
||||||
export type Block = Node | Fragment | Block[]
|
export type Block = Node | Fragment | Block[]
|
||||||
export type ParentBlock = ParentNode | Node[]
|
export type ParentBlock = ParentNode | Node[]
|
||||||
export type Fragment = { nodes: Block; anchor: 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
|
let isRenderingActivity = false
|
||||||
export function getIsRendering() {
|
export function getIsRendering() {
|
||||||
|
@ -44,7 +44,7 @@ export function mountComponent(
|
||||||
) {
|
) {
|
||||||
instance.container = container
|
instance.container = container
|
||||||
|
|
||||||
setCurrentInstance(instance)
|
const reset = setCurrentInstance(instance)
|
||||||
const block = instance.scope.run(() => {
|
const block = instance.scope.run(() => {
|
||||||
const { component, props } = instance
|
const { component, props } = instance
|
||||||
const ctx = { expose: () => {} }
|
const ctx = { expose: () => {} }
|
||||||
|
@ -82,7 +82,7 @@ export function mountComponent(
|
||||||
// hook: mounted
|
// hook: mounted
|
||||||
invokeDirectiveHook(instance, 'mounted')
|
invokeDirectiveHook(instance, 'mounted')
|
||||||
m && invokeArrayFns(m)
|
m && invokeArrayFns(m)
|
||||||
unsetCurrentInstance()
|
reset()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,13 @@ import {
|
||||||
type BaseWatchMiddleware,
|
type BaseWatchMiddleware,
|
||||||
type BaseWatchOptions,
|
type BaseWatchOptions,
|
||||||
baseWatch,
|
baseWatch,
|
||||||
getCurrentScope,
|
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { NOOP, invokeArrayFns, remove } from '@vue/shared'
|
import { NOOP, extend, invokeArrayFns, remove } from '@vue/shared'
|
||||||
import { type ComponentInternalInstance, currentInstance } from './component'
|
import {
|
||||||
|
type ComponentInternalInstance,
|
||||||
|
getCurrentInstance,
|
||||||
|
setCurrentInstance,
|
||||||
|
} from './component'
|
||||||
import {
|
import {
|
||||||
createVaporRenderingScheduler,
|
createVaporRenderingScheduler,
|
||||||
queuePostRenderEffect,
|
queuePostRenderEffect,
|
||||||
|
@ -15,6 +18,12 @@ import { handleError as handleErrorWithInstance } from './errorHandling'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { invokeDirectiveHook } from './directive'
|
import { invokeDirectiveHook } from './directive'
|
||||||
|
|
||||||
|
interface RenderWatchOptions {
|
||||||
|
immediate?: boolean
|
||||||
|
deep?: boolean
|
||||||
|
once?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type WatchStopHandle = () => void
|
type WatchStopHandle = () => void
|
||||||
|
|
||||||
export function renderEffect(effect: () => void): WatchStopHandle {
|
export function renderEffect(effect: () => void): WatchStopHandle {
|
||||||
|
@ -24,20 +33,25 @@ export function renderEffect(effect: () => void): WatchStopHandle {
|
||||||
export function renderWatch(
|
export function renderWatch(
|
||||||
source: any,
|
source: any,
|
||||||
cb: (value: any, oldValue: any) => void,
|
cb: (value: any, oldValue: any) => void,
|
||||||
|
options?: RenderWatchOptions,
|
||||||
): WatchStopHandle {
|
): WatchStopHandle {
|
||||||
return doWatch(source as any, cb)
|
return doWatch(source as any, cb, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
function doWatch(source: any, cb?: any): WatchStopHandle {
|
function doWatch(
|
||||||
const extendOptions: BaseWatchOptions = {}
|
source: any,
|
||||||
|
cb?: any,
|
||||||
|
options?: RenderWatchOptions,
|
||||||
|
): WatchStopHandle {
|
||||||
|
const extendOptions: BaseWatchOptions =
|
||||||
|
cb && options ? extend({}, options) : {}
|
||||||
|
|
||||||
if (__DEV__) extendOptions.onWarn = warn
|
if (__DEV__) extendOptions.onWarn = warn
|
||||||
|
|
||||||
// TODO: SSR
|
// TODO: SSR
|
||||||
// if (__SSR__) {}
|
// if (__SSR__) {}
|
||||||
|
|
||||||
const instance =
|
const instance = getCurrentInstance()
|
||||||
getCurrentScope() === currentInstance?.scope ? currentInstance : null
|
|
||||||
|
|
||||||
extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) =>
|
extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) =>
|
||||||
handleErrorWithInstance(err, instance, type)
|
handleErrorWithInstance(err, instance, type)
|
||||||
|
@ -78,8 +92,10 @@ const createMiddleware =
|
||||||
instance.isUpdating = true
|
instance.isUpdating = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reset = setCurrentInstance(instance)
|
||||||
// run callback
|
// run callback
|
||||||
value = next()
|
value = next()
|
||||||
|
reset()
|
||||||
|
|
||||||
if (isFirstEffect) {
|
if (isFirstEffect) {
|
||||||
queuePostRenderEffect(() => {
|
queuePostRenderEffect(() => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const template = (str: string): (() => DocumentFragment) => {
|
||||||
// first render: insert the node directly.
|
// first render: insert the node directly.
|
||||||
// this removes it from the template fragment to avoid keeping two copies
|
// 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.
|
// 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 {
|
} else {
|
||||||
// repeated renders: clone from cache. This is more performant and
|
// repeated renders: clone from cache. This is more performant and
|
||||||
// efficient when dealing with big lists where the template is repeated
|
// efficient when dealing with big lists where the template is repeated
|
||||||
|
|
Loading…
Reference in New Issue