Skip to content

Commit

Permalink
feat(runtime-vapor): createIf
Browse files Browse the repository at this point in the history
  • Loading branch information
LittleSound committed Jan 16, 2024
1 parent af9f892 commit 8b504c8
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 20 deletions.
112 changes: 112 additions & 0 deletions packages/runtime-vapor/__tests__/if.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
5 changes: 2 additions & 3 deletions packages/runtime-vapor/src/apiLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
type ComponentInternalInstance,
currentInstance,
setCurrentInstance,
unsetCurrentInstance,
} from './component'
import { warn } from './warning'
import { pauseTracking, resetTracking } from '@vue/reactivity'
Expand All @@ -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
})
Expand Down
7 changes: 7 additions & 0 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 1 addition & 5 deletions packages/runtime-vapor/src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
61 changes: 61 additions & 0 deletions packages/runtime-vapor/src/if.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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,
// 如果是 v-else-if 就把 () => createIf 作为 b2 传入
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(
() => Boolean(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
}
1 change: 1 addition & 0 deletions packages/runtime-vapor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ export * from './dom'
export * from './directives/vShow'
export * from './apiLifecycle'
export { getCurrentInstance, type ComponentInternalInstance } from './component'
export * from './if'
6 changes: 3 additions & 3 deletions packages/runtime-vapor/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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: () => {} }
Expand Down Expand Up @@ -82,7 +82,7 @@ export function mountComponent(
// hook: mounted
invokeDirectiveHook(instance, 'mounted')
m && invokeArrayFns(m)
unsetCurrentInstance()
reset

return instance
}
Expand Down
32 changes: 24 additions & 8 deletions packages/runtime-vapor/src/renderWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -78,8 +92,10 @@ const createMiddleware =
instance.isUpdating = true
}

const reset = setCurrentInstance(instance)
// run callback
value = next()
reset()

if (isFirstEffect) {
queuePostRenderEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-vapor/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8b504c8

Please sign in to comment.