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,
|
||||
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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 './apiLifecycle'
|
||||
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 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
|
||||
}
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue