"paths": {
"vue": ["../packages/vue/src"],
"@vue/vapor": ["../packages/vue-vapor/src"],
+ "vue/vapor": ["../packages/vue-vapor/src"],
"@vue/*": ["../packages/*/src"]
}
},
expect(host.innerHTML).toBe(`foobar!barbaz!`)
})
- test('directive', () => {
+ test.todo('directive', () => {
const spy1 = vi.fn()
const spy2 = vi.fn()
const define = makeRender()
-describe('directives', () => {
+describe.todo('directives', () => {
it('should work', async () => {
const count = ref(0)
const define = makeRender()
-describe('directives', () => {
+describe.todo('directives', () => {
it('should work', async () => {
const count = ref(0)
})
}
-describe('directive: v-model', () => {
+describe.todo('directive: v-model', () => {
test('should work with text input', async () => {
const spy = vi.fn()
on(n1 as HTMLElement, 'click', () => handleClick)
return n0
})
-describe('directive: v-show', () => {
+describe.todo('directive: v-show', () => {
test('basic', async () => {
const { host } = createDemo(true).render()
const btn = host.querySelector('button')
expect(host.innerHTML).toBe('<!--for-->')
})
- test('should work with directive hooks', async () => {
+ test.fails('should work with directive hooks', async () => {
const calls: string[] = []
const list = ref([0])
const update = ref(0)
expect(host.innerHTML).toBe('<!--if-->')
})
- test('should work with directive hooks', async () => {
+ test.todo('should work with directive hooks', async () => {
const calls: string[] = []
const show1 = ref(true)
const show2 = ref(true)
import {
+ type EffectScope,
type ShallowRef,
- getCurrentScope,
+ effectScope,
isReactive,
proxyRefs,
shallowRef,
- traverse,
triggerRef,
} from '@vue/reactivity'
import { isArray, isObject, isString } from '@vue/shared'
import { warn } from './warning'
import { currentInstance } from './component'
import { componentKey } from './component'
-import { BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
-import {
- createChildFragmentDirectives,
- invokeWithMount,
- invokeWithUnmount,
- invokeWithUpdate,
-} from './directivesChildFragment'
import type { DynamicSlot } from './componentSlots'
+import { renderEffect } from './renderEffect'
interface ForBlock extends Fragment {
- scope: BlockEffectScope
+ scope: EffectScope
state: [
item: ShallowRef<any>,
key: ShallowRef<any>,
let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null
- const update = getMemo ? updateWithMemo : updateWithoutMemo
- const parentScope = getCurrentScope()!
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
const ref: Fragment = {
nodes: oldBlocks,
}
const instance = currentInstance!
- if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) {
+ if (__DEV__ && !instance) {
warn('createFor() can only be used inside setup()')
}
- createChildFragmentDirectives(
- parentAnchor,
- () => oldBlocks.map(b => b.scope),
- // source getter
- () => traverse(src(), 1),
- // init cb
- getValue => doFor(getValue()),
- // effect cb
- getValue => doFor(getValue()),
- once,
- )
+ const update = getMemo ? updateWithMemo : updateWithoutMemo
+ once ? renderList() : renderEffect(renderList)
return ref
- function doFor(source: any) {
+ function renderList() {
+ const source = src()
const newLength = getLength(source)
const oldLength = oldBlocks.length
newBlocks = new Array(newLength)
idx: number,
anchor: Node = parentAnchor,
): ForBlock {
- const scope = new BlockEffectScope(instance, parentScope)
+ const scope = effectScope()
const [item, key, index] = getItem(source, idx)
const state = [
})
block.nodes = scope.run(() => renderItem(proxyRefs(state)))!
- invokeWithMount(scope, () => {
- // TODO v-memo
- // if (getMemo) block.update()
- if (parent) insert(block.nodes, parent, anchor)
- })
+ // TODO v-memo
+ // if (getMemo) block.update()
+ if (parent) insert(block.nodes, parent, anchor)
return block
}
}
if (needsUpdate) setState(block, newItem, newKey, newIndex)
- invokeWithUpdate(block.scope)
}
function updateWithoutMemo(
(!isReactive(newItem) && isObject(newItem))
if (needsUpdate) setState(block, newItem, newKey, newIndex)
- invokeWithUpdate(block.scope)
}
function unmount({ nodes, scope }: ForBlock) {
- invokeWithUnmount(scope, () => {
- removeBlock(nodes, parent!)
- })
+ removeBlock(nodes, parent!)
+ scope.stop()
}
}
+import { renderEffect } from './renderEffect'
import { type Block, type Fragment, fragmentKey } from './apiRender'
-import { getCurrentScope } from '@vue/reactivity'
+import { type EffectScope, effectScope } from '@vue/reactivity'
import { createComment, createTextNode, insert, remove } from './dom/element'
-import { currentInstance } from './component'
-import { warn } from './warning'
-import { BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
-import {
- createChildFragmentDirectives,
- invokeWithMount,
- invokeWithUnmount,
- invokeWithUpdate,
-} from './directivesChildFragment'
type BlockFn = () => Block
let branch: BlockFn | undefined
let parent: ParentNode | undefined | null
let block: Block | undefined
- let scope: BlockEffectScope | undefined
- const parentScope = getCurrentScope()!
+ let scope: EffectScope | undefined
const anchor = __DEV__ ? createComment('if') : createTextNode()
const fragment: Fragment = {
nodes: [],
[fragmentKey]: true,
}
- const instance = currentInstance!
- if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) {
- warn('createIf() can only be used inside setup()')
- }
-
// TODO: SSR
// if (isHydrating) {
// parent = hydrationNode!.parentNode
// setCurrentHydrationNode(hydrationNode!)
// }
- createChildFragmentDirectives(
- anchor,
- () => (scope ? [scope] : []),
- // source getter
- condition,
- // init cb
- getValue => {
- newValue = !!getValue()
- doIf()
- },
- // effect cb
- getValue => {
- if ((newValue = !!getValue()) !== oldValue) {
- doIf()
- } else if (scope) {
- invokeWithUpdate(scope)
- }
- },
- once,
- )
+ if (once) {
+ doIf()
+ } else {
+ renderEffect(() => doIf())
+ }
// TODO: SSR
// if (isHydrating) {
return fragment
function doIf() {
- parent ||= anchor.parentNode
- if (block) {
- invokeWithUnmount(scope!, () => remove(block!, parent!))
- }
- if ((branch = (oldValue = newValue) ? b1 : b2)) {
- scope = new BlockEffectScope(instance, parentScope)
- fragment.nodes = block = scope.run(branch)!
- invokeWithMount(scope, () => parent && insert(block!, parent, anchor))
- } else {
- scope = block = undefined
- fragment.nodes = []
+ if ((newValue = !!condition()) !== oldValue) {
+ parent ||= anchor.parentNode
+ if (block) {
+ scope!.stop()
+ remove(block, parent!)
+ }
+ if ((branch = (oldValue = newValue) ? b1 : b2)) {
+ scope = effectScope()
+ fragment.nodes = block = scope.run(branch)!
+ parent && insert(block, parent, anchor)
+ } else {
+ scope = block = undefined
+ fragment.nodes = []
+ }
}
}
}
+++ /dev/null
-import { EffectScope } from '@vue/reactivity'
-import type { ComponentInternalInstance } from './component'
-import type { DirectiveBindingsMap } from './directives'
-
-export class BlockEffectScope extends EffectScope {
- /**
- * instance
- * @internal
- */
- it: ComponentInternalInstance
- /**
- * isMounted
- * @internal
- */
- im: boolean
- /**
- * directives
- * @internal
- */
- dirs?: DirectiveBindingsMap
-
- constructor(
- instance: ComponentInternalInstance,
- parentScope: EffectScope | null,
- ) {
- super(false, parentScope || undefined)
- this.im = false
- this.it = instance
- }
-}
-
-export function isRenderEffectScope(
- scope: EffectScope | undefined,
-): scope is BlockEffectScope {
- return scope instanceof BlockEffectScope
-}
-import { isRef } from '@vue/reactivity'
+import { EffectScope, isRef } from '@vue/reactivity'
import {
EMPTY_OBJ,
hasOwn,
createAppContext,
} from './apiCreateVaporApp'
import type { Data } from '@vue/runtime-shared'
-import { BlockEffectScope } from './blockEffectScope'
export type Component = FunctionalComponent | ObjectComponent
root: ComponentInternalInstance
provides: Data
- scope: BlockEffectScope
+ scope: EffectScope
comps: Set<ComponentInternalInstance>
rawProps: NormalizedRawProps
parent,
root: null!, // set later
- scope: null!,
+ scope: new EffectScope(true /* detached */)!,
provides: parent ? parent.provides : Object.create(_appContext.provides),
type: component,
comps: new Set(),
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
}
instance.root = parent ? parent.root : instance
- instance.scope = new BlockEffectScope(instance, parent && parent.scope)
initProps(instance, rawProps, !isFunction(component), once)
initSlots(instance, slots)
instance.emit = emit.bind(null, instance)
import type { VaporLifecycleHooks } from './enums'
import { type ComponentInternalInstance, setCurrentInstance } from './component'
import { queuePostFlushCb } from './scheduler'
-import { type DirectiveHookName, invokeDirectiveHook } from './directives'
+import type { DirectiveHookName } from './directives'
export function invokeLifecycle(
instance: ComponentInternalInstance,
}
post ? queuePostFlushCb(fn) : fn()
}
-
- invokeDirectiveHook(instance, directive, instance.scope)
}
function invokeSub() {
-import { invokeArrayFns, isBuiltInDirective, isFunction } from '@vue/shared'
-import {
- type ComponentInternalInstance,
- currentInstance,
- isVaporComponent,
- setCurrentInstance,
-} from './component'
-import {
- EffectFlags,
- ReactiveEffect,
- getCurrentScope,
- pauseTracking,
- resetTracking,
- traverse,
-} from '@vue/reactivity'
-import {
- VaporErrorCodes,
- callWithAsyncErrorHandling,
- callWithErrorHandling,
-} from './errorHandling'
-import { type SchedulerJob, queueJob, queuePostFlushCb } from './scheduler'
+import { isBuiltInDirective } from '@vue/shared'
+import { type ComponentInternalInstance, currentInstance } from './component'
import { warn } from './warning'
-import { type BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
-import { normalizeBlock } from './dom/element'
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
return nodeOrComponent
}
- let node: Node
- if (isVaporComponent(nodeOrComponent)) {
- const root = getComponentNode(nodeOrComponent)
- if (!root) return nodeOrComponent
- node = root
- } else {
- node = nodeOrComponent
- }
-
- let bindings: DirectiveBinding[]
- const instance = currentInstance!
- const parentScope = getCurrentScope() as BlockEffectScope
-
- if (__DEV__ && !isRenderEffectScope(parentScope)) {
- warn(`Directives should be used inside of RenderEffectScope.`)
- }
-
- const directivesMap = (parentScope.dirs ||= new Map())
- if (!(bindings = directivesMap.get(node))) {
- directivesMap.set(node, (bindings = []))
- }
-
- for (const directive of directives) {
- let [dir, source, arg, modifiers] = directive
- if (!dir) continue
- if (isFunction(dir)) {
- dir = {
- mounted: dir,
- updated: dir,
- } satisfies ObjectDirective
- }
-
- const binding: DirectiveBinding = {
- dir,
- instance,
- value: null, // set later
- oldValue: undefined,
- arg,
- modifiers,
- }
-
- if (source) {
- if (dir.deep) {
- const deep = dir.deep === true ? undefined : dir.deep
- const baseSource = source
- source = () => traverse(baseSource(), deep)
- }
-
- const effect = new ReactiveEffect(() =>
- callWithErrorHandling(
- source!,
- instance,
- VaporErrorCodes.RENDER_FUNCTION,
- ),
- )
- const triggerRenderingUpdate = createRenderingUpdateTrigger(
- instance,
- effect,
- )
- effect.scheduler = () => queueJob(triggerRenderingUpdate)
-
- binding.source = effect.run.bind(effect)
- }
-
- bindings.push(binding)
-
- callDirectiveHook(node, binding, instance, 'created')
- }
+ // NOOP
return nodeOrComponent
}
-
-function getComponentNode(component: ComponentInternalInstance) {
- if (!component.block) return
-
- const nodes = normalizeBlock(component.block)
- if (nodes.length !== 1) {
- warn(
- `Runtime directive used on component with non-element root node. ` +
- `The directives will not function as intended.`,
- )
- return
- }
-
- return nodes[0]
-}
-
-export function invokeDirectiveHook(
- instance: ComponentInternalInstance | null,
- name: DirectiveHookName,
- scope: BlockEffectScope,
-): void {
- const { dirs } = scope
- if (name === 'mounted') scope.im = true
- if (!dirs) return
- const iterator = dirs.entries()
- for (const [node, bindings] of iterator) {
- for (const binding of bindings) {
- callDirectiveHook(node, binding, instance, name)
- }
- }
-}
-
-function callDirectiveHook(
- node: Node,
- binding: DirectiveBinding,
- instance: ComponentInternalInstance | null,
- name: DirectiveHookName,
-) {
- if (name === 'beforeUpdate') binding.oldValue = binding.value
- const { dir } = binding
- const hook = dir[name]
- if (!hook) return
-
- const newValue = binding.source ? binding.source() : undefined
- binding.value = newValue
- // disable tracking inside all lifecycle hooks
- // since they can potentially be called inside effects.
- pauseTracking()
- callWithAsyncErrorHandling(hook, instance, VaporErrorCodes.DIRECTIVE_HOOK, [
- node,
- binding,
- ])
- resetTracking()
-}
-
-export function createRenderingUpdateTrigger(
- instance: ComponentInternalInstance,
- effect: ReactiveEffect,
-): SchedulerJob {
- job.id = instance.uid
- return job
- function job() {
- if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) {
- return
- }
-
- if (instance.isMounted && !instance.isUpdating) {
- instance.isUpdating = true
- const reset = setCurrentInstance(instance)
-
- const { bu, u, scope } = instance
- const { dirs } = scope
- // beforeUpdate hook
- if (bu) {
- invokeArrayFns(bu)
- }
- invokeDirectiveHook(instance, 'beforeUpdate', scope)
-
- queuePostFlushCb(() => {
- instance.isUpdating = false
- const reset = setCurrentInstance(instance)
- if (dirs) {
- invokeDirectiveHook(instance, 'updated', scope)
- }
- // updated hook
- if (u) {
- queuePostFlushCb(u)
- }
- reset()
- })
- reset()
- }
- }
-}
+++ /dev/null
-import { ReactiveEffect, getCurrentScope } from '@vue/reactivity'
-import {
- type Directive,
- type DirectiveHookName,
- createRenderingUpdateTrigger,
- invokeDirectiveHook,
-} from './directives'
-import { warn } from './warning'
-import { type BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
-import { currentInstance } from './component'
-import { VaporErrorCodes, callWithErrorHandling } from './errorHandling'
-import { queueJob, queuePostFlushCb } from './scheduler'
-
-/**
- * used in createIf and createFor
- * manage directives of child fragments in components.
- */
-export function createChildFragmentDirectives(
- anchor: Node,
- getScopes: () => BlockEffectScope[],
- source: () => any,
- initCallback: (getValue: () => any) => void,
- effectCallback: (getValue: () => any) => void,
- once?: boolean,
-): void {
- let isTriggered = false
- const instance = currentInstance!
- const parentScope = getCurrentScope() as BlockEffectScope
- if (__DEV__) {
- if (!isRenderEffectScope(parentScope)) {
- warn('child directives can only be added to a render effect scope')
- }
- if (!instance) {
- warn('child directives can only be added in a component')
- }
- }
-
- const callSourceWithErrorHandling = () =>
- callWithErrorHandling(source, instance, VaporErrorCodes.RENDER_FUNCTION)
-
- if (once) {
- initCallback(callSourceWithErrorHandling)
- return
- }
-
- const directiveBindingsMap = (parentScope.dirs ||= new Map())
- const dir: Directive = {
- beforeUpdate: onDirectiveBeforeUpdate,
- beforeMount: () => invokeChildrenDirectives('beforeMount'),
- mounted: () => invokeChildrenDirectives('mounted'),
- beforeUnmount: () => invokeChildrenDirectives('beforeUnmount'),
- unmounted: () => invokeChildrenDirectives('unmounted'),
- }
- directiveBindingsMap.set(anchor, [
- {
- dir,
- instance,
- value: null,
- oldValue: undefined,
- },
- ])
-
- const effect = new ReactiveEffect(callSourceWithErrorHandling)
- const triggerRenderingUpdate = createRenderingUpdateTrigger(instance, effect)
- effect.scheduler = () => {
- isTriggered = true
- queueJob(triggerRenderingUpdate)
- }
-
- const getValue = () => effect.run()
-
- initCallback(getValue)
-
- function onDirectiveBeforeUpdate() {
- if (isTriggered) {
- isTriggered = false
- effectCallback(getValue)
- } else {
- const scopes = getScopes()
- for (const scope of scopes) {
- invokeWithUpdate(scope)
- }
- return
- }
- }
-
- function invokeChildrenDirectives(name: DirectiveHookName) {
- const scopes = getScopes()
- for (const scope of scopes) {
- invokeDirectiveHook(instance, name, scope)
- }
- }
-}
-
-export function invokeWithMount(
- scope: BlockEffectScope,
- handler?: () => any,
-): any {
- if (isRenderEffectScope(scope.parent) && !scope.parent.im) {
- return handler && handler()
- }
- return invokeWithDirsHooks(scope, 'mount', handler)
-}
-
-export function invokeWithUnmount(
- scope: BlockEffectScope,
- handler?: () => void,
-): any {
- try {
- return invokeWithDirsHooks(scope, 'unmount', handler)
- } finally {
- scope.stop()
- }
-}
-
-export function invokeWithUpdate(
- scope: BlockEffectScope,
- handler?: () => void,
-): any {
- return invokeWithDirsHooks(scope, 'update', handler)
-}
-
-const lifecycleMap = {
- mount: ['beforeMount', 'mounted'],
- update: ['beforeUpdate', 'updated'],
- unmount: ['beforeUnmount', 'unmounted'],
-} as const
-
-function invokeWithDirsHooks(
- scope: BlockEffectScope,
- name: keyof typeof lifecycleMap,
- handler?: () => any,
-) {
- const { dirs, it: instance } = scope
- const [before, after] = lifecycleMap[name]
-
- if (!dirs) {
- const res = handler && handler()
- if (name === 'mount') {
- queuePostFlushCb(() => (scope.im = true))
- }
- return res
- }
-
- invokeDirectiveHook(instance, before, scope)
- try {
- if (handler) {
- return handler()
- }
- } finally {
- queuePostFlushCb(() => {
- invokeDirectiveHook(instance, after, scope)
- })
- }
-}
queuePostFlushCb,
} from './scheduler'
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
-import { invokeDirectiveHook } from './directives'
export function renderEffect(cb: () => void): void {
const instance = getCurrentInstance()
if (instance && instance.isMounted && !instance.isUpdating) {
instance.isUpdating = true
- const { bu, u, scope } = instance
- const { dirs } = scope
+ const { bu, u } = instance
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
- if (dirs) {
- invokeDirectiveHook(instance, 'beforeUpdate', scope)
- }
effect.run()
queuePostFlushCb(() => {
instance.isUpdating = false
const reset = setCurrentInstance(instance)
- if (dirs) {
- invokeDirectiveHook(instance, 'updated', scope)
- }
// updated hook
if (u) {
queuePostFlushCb(u)