Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runtime-vapor): createIf #95

Merged
merged 3 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 传入
LittleSound marked this conversation as resolved.
Show resolved Hide resolved
b2?: BlockFn,
hydrationNode?: Node,
LittleSound marked this conversation as resolved.
Show resolved Hide resolved
): 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()),
LittleSound marked this conversation as resolved.
Show resolved Hide resolved
(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