feat: directive lifecycle hooks in `v-for`, `v-if` and component (#123)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
969f53f2e7
commit
b5ecb72864
|
@ -34,13 +34,13 @@ export class EffectScope {
|
||||||
*/
|
*/
|
||||||
private index: number | undefined
|
private index: number | undefined
|
||||||
|
|
||||||
constructor(public detached = false) {
|
constructor(
|
||||||
this.parent = activeEffectScope
|
public detached = false,
|
||||||
if (!detached && activeEffectScope) {
|
parent = activeEffectScope,
|
||||||
this.index =
|
) {
|
||||||
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
|
this.parent = parent
|
||||||
this,
|
if (!detached && parent) {
|
||||||
) - 1
|
this.index = (parent.scopes || (parent.scopes = [])).push(this) - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ describe('directive: v-show', () => {
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<button>toggle</button><div>child</div>')
|
expect(host.innerHTML).toBe('<button>toggle</button><div>child</div>')
|
||||||
expect(instance.dirs.get(n0)![0].dir).toBe(vShow)
|
expect(instance.scope.dirs!.get(n0)![0].dir).toBe(vShow)
|
||||||
|
|
||||||
const btn = host.querySelector('button')
|
const btn = host.querySelector('button')
|
||||||
btn?.click()
|
btn?.click()
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
import { createFor, nextTick, ref, renderEffect } from '../src'
|
import { NOOP } from '@vue/shared'
|
||||||
|
import {
|
||||||
|
type Directive,
|
||||||
|
children,
|
||||||
|
createFor,
|
||||||
|
nextTick,
|
||||||
|
ref,
|
||||||
|
renderEffect,
|
||||||
|
template,
|
||||||
|
withDirectives,
|
||||||
|
} from '../src'
|
||||||
import { makeRender } from './_utils'
|
import { makeRender } from './_utils'
|
||||||
|
import { unmountComponent } from '../src/apiRender'
|
||||||
|
|
||||||
const define = makeRender()
|
const define = makeRender()
|
||||||
|
|
||||||
|
@ -184,4 +195,92 @@ describe('createFor', () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(host.innerHTML).toBe('<!--for-->')
|
expect(host.innerHTML).toBe('<!--for-->')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should work with directive hooks', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const list = ref([0])
|
||||||
|
const update = ref(0)
|
||||||
|
const add = () => list.value.push(list.value.length)
|
||||||
|
const spySrcFn = vi.fn(() => list.value)
|
||||||
|
|
||||||
|
const vDirective: Directive = {
|
||||||
|
created: (el, { value }) => calls.push(`${value} created`),
|
||||||
|
beforeMount: (el, { value }) => calls.push(`${value} beforeMount`),
|
||||||
|
mounted: (el, { value }) => calls.push(`${value} mounted`),
|
||||||
|
beforeUpdate: (el, { value }) => calls.push(`${value} beforeUpdate`),
|
||||||
|
updated: (el, { value }) => calls.push(`${value} updated`),
|
||||||
|
beforeUnmount: (el, { value }) => calls.push(`${value} beforeUnmount`),
|
||||||
|
unmounted: (el, { value }) => calls.push(`${value} unmounted`),
|
||||||
|
}
|
||||||
|
|
||||||
|
const t0 = template('<p></p>')
|
||||||
|
const { instance } = define(() => {
|
||||||
|
const n1 = createFor(spySrcFn, block => {
|
||||||
|
const n2 = t0()
|
||||||
|
const n3 = children(n2, 0)
|
||||||
|
withDirectives(n3, [[vDirective, () => block.s[0]]])
|
||||||
|
return [n2, NOOP]
|
||||||
|
})
|
||||||
|
renderEffect(() => update.value)
|
||||||
|
return [n1]
|
||||||
|
}).render()
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
// `${item index} ${hook name}`
|
||||||
|
expect(calls).toEqual(['0 created', '0 beforeMount', '0 mounted'])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spySrcFn).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
add()
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'0 beforeUpdate',
|
||||||
|
'1 created',
|
||||||
|
'1 beforeMount',
|
||||||
|
'0 updated',
|
||||||
|
'1 mounted',
|
||||||
|
])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spySrcFn).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
list.value.reverse()
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'1 beforeUpdate',
|
||||||
|
'0 beforeUpdate',
|
||||||
|
'1 updated',
|
||||||
|
'0 updated',
|
||||||
|
])
|
||||||
|
expect(spySrcFn).toHaveBeenCalledTimes(3)
|
||||||
|
list.value.reverse()
|
||||||
|
await nextTick()
|
||||||
|
calls.length = 0
|
||||||
|
expect(spySrcFn).toHaveBeenCalledTimes(4)
|
||||||
|
|
||||||
|
update.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'0 beforeUpdate',
|
||||||
|
'1 beforeUpdate',
|
||||||
|
'0 updated',
|
||||||
|
'1 updated',
|
||||||
|
])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spySrcFn).toHaveBeenCalledTimes(4)
|
||||||
|
|
||||||
|
list.value.pop()
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'0 beforeUpdate',
|
||||||
|
'1 beforeUnmount',
|
||||||
|
'0 updated',
|
||||||
|
'1 unmounted',
|
||||||
|
])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spySrcFn).toHaveBeenCalledTimes(5)
|
||||||
|
|
||||||
|
unmountComponent(instance)
|
||||||
|
expect(calls).toEqual(['0 beforeUnmount', '0 unmounted'])
|
||||||
|
expect(spySrcFn).toHaveBeenCalledTimes(5)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
children,
|
||||||
createIf,
|
createIf,
|
||||||
insert,
|
insert,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
@ -6,9 +7,11 @@ import {
|
||||||
renderEffect,
|
renderEffect,
|
||||||
setText,
|
setText,
|
||||||
template,
|
template,
|
||||||
|
withDirectives,
|
||||||
} from '../src'
|
} from '../src'
|
||||||
import type { Mock } from 'vitest'
|
import type { Mock } from 'vitest'
|
||||||
import { makeRender } from './_utils'
|
import { makeRender } from './_utils'
|
||||||
|
import { unmountComponent } from '../src/apiRender'
|
||||||
|
|
||||||
const define = makeRender()
|
const define = makeRender()
|
||||||
|
|
||||||
|
@ -24,6 +27,8 @@ describe('createIf', () => {
|
||||||
let spyElseFn: Mock<any, any>
|
let spyElseFn: Mock<any, any>
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
|
|
||||||
|
const spyConditionFn = vi.fn(() => count.value)
|
||||||
|
|
||||||
// templates can be reused through caching.
|
// templates can be reused through caching.
|
||||||
const t0 = template('<div></div>')
|
const t0 = template('<div></div>')
|
||||||
const t1 = template('<p></p>')
|
const t1 = template('<p></p>')
|
||||||
|
@ -34,7 +39,7 @@ describe('createIf', () => {
|
||||||
|
|
||||||
insert(
|
insert(
|
||||||
createIf(
|
createIf(
|
||||||
() => count.value,
|
spyConditionFn,
|
||||||
// v-if
|
// v-if
|
||||||
(spyIfFn ||= vi.fn(() => {
|
(spyIfFn ||= vi.fn(() => {
|
||||||
const n2 = t1()
|
const n2 = t1()
|
||||||
|
@ -55,24 +60,28 @@ describe('createIf', () => {
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
|
expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
|
||||||
|
expect(spyConditionFn).toHaveBeenCalledTimes(1)
|
||||||
expect(spyIfFn!).toHaveBeenCalledTimes(0)
|
expect(spyIfFn!).toHaveBeenCalledTimes(0)
|
||||||
expect(spyElseFn!).toHaveBeenCalledTimes(1)
|
expect(spyElseFn!).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(host.innerHTML).toBe('<div><p>1</p><!--if--></div>')
|
expect(host.innerHTML).toBe('<div><p>1</p><!--if--></div>')
|
||||||
|
expect(spyConditionFn).toHaveBeenCalledTimes(2)
|
||||||
expect(spyIfFn!).toHaveBeenCalledTimes(1)
|
expect(spyIfFn!).toHaveBeenCalledTimes(1)
|
||||||
expect(spyElseFn!).toHaveBeenCalledTimes(1)
|
expect(spyElseFn!).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(host.innerHTML).toBe('<div><p>2</p><!--if--></div>')
|
expect(host.innerHTML).toBe('<div><p>2</p><!--if--></div>')
|
||||||
|
expect(spyConditionFn).toHaveBeenCalledTimes(3)
|
||||||
expect(spyIfFn!).toHaveBeenCalledTimes(1)
|
expect(spyIfFn!).toHaveBeenCalledTimes(1)
|
||||||
expect(spyElseFn!).toHaveBeenCalledTimes(1)
|
expect(spyElseFn!).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
count.value = 0
|
count.value = 0
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
|
expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
|
||||||
|
expect(spyConditionFn).toHaveBeenCalledTimes(4)
|
||||||
expect(spyIfFn!).toHaveBeenCalledTimes(1)
|
expect(spyIfFn!).toHaveBeenCalledTimes(1)
|
||||||
expect(spyElseFn!).toHaveBeenCalledTimes(2)
|
expect(spyElseFn!).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
@ -124,4 +133,113 @@ describe('createIf', () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(host.innerHTML).toBe('<!--if-->')
|
expect(host.innerHTML).toBe('<!--if-->')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should work with directive hooks', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const show1 = ref(true)
|
||||||
|
const show2 = ref(true)
|
||||||
|
const update = ref(0)
|
||||||
|
|
||||||
|
const spyConditionFn1 = vi.fn(() => show1.value)
|
||||||
|
const spyConditionFn2 = vi.fn(() => show2.value)
|
||||||
|
|
||||||
|
const vDirective: any = {
|
||||||
|
created: (el: any, { value }: any) => calls.push(`${value} created`),
|
||||||
|
beforeMount: (el: any, { value }: any) =>
|
||||||
|
calls.push(`${value} beforeMount`),
|
||||||
|
mounted: (el: any, { value }: any) => calls.push(`${value} mounted`),
|
||||||
|
beforeUpdate: (el: any, { value }: any) =>
|
||||||
|
calls.push(`${value} beforeUpdate`),
|
||||||
|
updated: (el: any, { value }: any) => calls.push(`${value} updated`),
|
||||||
|
beforeUnmount: (el: any, { value }: any) =>
|
||||||
|
calls.push(`${value} beforeUnmount`),
|
||||||
|
unmounted: (el: any, { value }: any) => calls.push(`${value} unmounted`),
|
||||||
|
}
|
||||||
|
|
||||||
|
const t0 = template('<p></p>')
|
||||||
|
const { instance } = define(() => {
|
||||||
|
const n1 = createIf(
|
||||||
|
spyConditionFn1,
|
||||||
|
() => {
|
||||||
|
const n2 = t0()
|
||||||
|
withDirectives(children(n2, 0), [
|
||||||
|
[vDirective, () => (update.value, '1')],
|
||||||
|
])
|
||||||
|
return n2
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
createIf(
|
||||||
|
spyConditionFn2,
|
||||||
|
() => {
|
||||||
|
const n2 = t0()
|
||||||
|
withDirectives(children(n2, 0), [[vDirective, () => '2']])
|
||||||
|
return n2
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const n2 = t0()
|
||||||
|
withDirectives(children(n2, 0), [[vDirective, () => '3']])
|
||||||
|
return n2
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return [n1]
|
||||||
|
}).render()
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['1 created', '1 beforeMount', '1 mounted'])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spyConditionFn1).toHaveBeenCalledTimes(1)
|
||||||
|
expect(spyConditionFn2).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
|
show1.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'1 beforeUnmount',
|
||||||
|
'2 created',
|
||||||
|
'2 beforeMount',
|
||||||
|
'1 unmounted',
|
||||||
|
'2 mounted',
|
||||||
|
])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spyConditionFn1).toHaveBeenCalledTimes(2)
|
||||||
|
expect(spyConditionFn2).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
show2.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'2 beforeUnmount',
|
||||||
|
'3 created',
|
||||||
|
'3 beforeMount',
|
||||||
|
'2 unmounted',
|
||||||
|
'3 mounted',
|
||||||
|
])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spyConditionFn1).toHaveBeenCalledTimes(2)
|
||||||
|
expect(spyConditionFn2).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
show1.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'3 beforeUnmount',
|
||||||
|
'1 created',
|
||||||
|
'1 beforeMount',
|
||||||
|
'3 unmounted',
|
||||||
|
'1 mounted',
|
||||||
|
])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spyConditionFn1).toHaveBeenCalledTimes(3)
|
||||||
|
expect(spyConditionFn2).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
update.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['1 beforeUpdate', '1 updated'])
|
||||||
|
calls.length = 0
|
||||||
|
expect(spyConditionFn1).toHaveBeenCalledTimes(3)
|
||||||
|
expect(spyConditionFn2).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
unmountComponent(instance)
|
||||||
|
expect(calls).toEqual(['1 beforeUnmount', '1 unmounted'])
|
||||||
|
expect(spyConditionFn1).toHaveBeenCalledTimes(3)
|
||||||
|
expect(spyConditionFn2).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,26 @@
|
||||||
import { type EffectScope, effectScope, isReactive } from '@vue/reactivity'
|
import { getCurrentScope, isReactive, traverse } from '@vue/reactivity'
|
||||||
import { isArray, isObject, isString } from '@vue/shared'
|
import { isArray, isObject, isString } from '@vue/shared'
|
||||||
import { createComment, createTextNode, insert, remove } from './dom/element'
|
import {
|
||||||
import { renderEffect } from './renderEffect'
|
createComment,
|
||||||
|
createTextNode,
|
||||||
|
insert,
|
||||||
|
remove as removeBlock,
|
||||||
|
} from './dom/element'
|
||||||
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
|
import { currentInstance } from './component'
|
||||||
import { componentKey } from './component'
|
import { componentKey } from './component'
|
||||||
|
import { BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
|
||||||
|
import {
|
||||||
|
createChildFragmentDirectives,
|
||||||
|
invokeWithMount,
|
||||||
|
invokeWithUnmount,
|
||||||
|
invokeWithUpdate,
|
||||||
|
} from './directivesChildFragment'
|
||||||
import type { DynamicSlot } from './componentSlots'
|
import type { DynamicSlot } from './componentSlots'
|
||||||
|
|
||||||
interface ForBlock extends Fragment {
|
interface ForBlock extends Fragment {
|
||||||
scope: EffectScope
|
scope: BlockEffectScope
|
||||||
/** state, use short key since it's used a lot in generated code */
|
/** state, use short key since it's used a lot in generated code */
|
||||||
s: [item: any, key: any, index?: number]
|
s: [item: any, key: any, index?: number]
|
||||||
update: () => void
|
update: () => void
|
||||||
|
@ -16,9 +28,11 @@ interface ForBlock extends Fragment {
|
||||||
memo: any[] | undefined
|
memo: any[] | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Source = any[] | Record<any, any> | number | Set<any> | Map<any, any>
|
||||||
|
|
||||||
/*! #__NO_SIDE_EFFECTS__ */
|
/*! #__NO_SIDE_EFFECTS__ */
|
||||||
export const createFor = (
|
export const createFor = (
|
||||||
src: () => any[] | Record<any, any> | number | Set<any> | Map<any, any>,
|
src: () => Source,
|
||||||
renderItem: (block: ForBlock) => [Block, () => void],
|
renderItem: (block: ForBlock) => [Block, () => void],
|
||||||
getKey?: (item: any, key: any, index?: number) => any,
|
getKey?: (item: any, key: any, index?: number) => any,
|
||||||
getMemo?: (item: any, key: any, index?: number) => any[],
|
getMemo?: (item: any, key: any, index?: number) => any[],
|
||||||
|
@ -29,18 +43,34 @@ export const createFor = (
|
||||||
let oldBlocks: ForBlock[] = []
|
let oldBlocks: ForBlock[] = []
|
||||||
let newBlocks: ForBlock[]
|
let newBlocks: ForBlock[]
|
||||||
let parent: ParentNode | undefined | null
|
let parent: ParentNode | undefined | null
|
||||||
|
const update = getMemo ? updateWithMemo : updateWithoutMemo
|
||||||
|
const parentScope = getCurrentScope()!
|
||||||
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
|
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
|
||||||
const ref: Fragment = {
|
const ref: Fragment = {
|
||||||
nodes: oldBlocks,
|
nodes: oldBlocks,
|
||||||
[fragmentKey]: true,
|
[fragmentKey]: true,
|
||||||
}
|
}
|
||||||
const update = getMemo ? updateWithMemo : updateWithoutMemo
|
|
||||||
once ? renderList() : renderEffect(renderList)
|
const instance = currentInstance!
|
||||||
|
if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
return ref
|
return ref
|
||||||
|
|
||||||
function renderList() {
|
function doFor(source: any) {
|
||||||
const source = src()
|
|
||||||
const newLength = getLength(source)
|
const newLength = getLength(source)
|
||||||
const oldLength = oldBlocks.length
|
const oldLength = oldBlocks.length
|
||||||
newBlocks = new Array(newLength)
|
newBlocks = new Array(newLength)
|
||||||
|
@ -225,7 +255,8 @@ export const createFor = (
|
||||||
idx: number,
|
idx: number,
|
||||||
anchor: Node = parentAnchor,
|
anchor: Node = parentAnchor,
|
||||||
): ForBlock {
|
): ForBlock {
|
||||||
const scope = effectScope()
|
const scope = new BlockEffectScope(instance, parentScope)
|
||||||
|
|
||||||
const [item, key, index] = getItem(source, idx)
|
const [item, key, index] = getItem(source, idx)
|
||||||
const block: ForBlock = (newBlocks[idx] = {
|
const block: ForBlock = (newBlocks[idx] = {
|
||||||
nodes: null!, // set later
|
nodes: null!, // set later
|
||||||
|
@ -239,8 +270,12 @@ export const createFor = (
|
||||||
const res = scope.run(() => renderItem(block))!
|
const res = scope.run(() => renderItem(block))!
|
||||||
block.nodes = res[0]
|
block.nodes = res[0]
|
||||||
block.update = res[1]
|
block.update = res[1]
|
||||||
if (getMemo) block.update()
|
|
||||||
if (parent) insert(block.nodes, parent, anchor)
|
invokeWithMount(scope, () => {
|
||||||
|
if (getMemo) block.update()
|
||||||
|
if (parent) insert(block.nodes, parent, anchor)
|
||||||
|
})
|
||||||
|
|
||||||
return block
|
return block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,10 +310,13 @@ export const createFor = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (needsUpdate) {
|
|
||||||
block.s = [newItem, newKey, newIndex]
|
block.s = [newItem, newKey, newIndex]
|
||||||
block.update()
|
invokeWithUpdate(block.scope, () => {
|
||||||
}
|
if (needsUpdate) {
|
||||||
|
block.update()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWithoutMemo(
|
function updateWithoutMemo(
|
||||||
|
@ -287,20 +325,24 @@ export const createFor = (
|
||||||
newKey = block.s[1],
|
newKey = block.s[1],
|
||||||
newIndex = block.s[2],
|
newIndex = block.s[2],
|
||||||
) {
|
) {
|
||||||
if (
|
let needsUpdate =
|
||||||
newItem !== block.s[0] ||
|
newItem !== block.s[0] ||
|
||||||
newKey !== block.s[1] ||
|
newKey !== block.s[1] ||
|
||||||
newIndex !== block.s[2] ||
|
newIndex !== block.s[2] ||
|
||||||
!isReactive(newItem)
|
!isReactive(newItem)
|
||||||
) {
|
|
||||||
block.s = [newItem, newKey, newIndex]
|
block.s = [newItem, newKey, newIndex]
|
||||||
block.update()
|
invokeWithUpdate(block.scope, () => {
|
||||||
}
|
if (needsUpdate) {
|
||||||
|
block.update()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function unmount({ nodes, scope }: ForBlock) {
|
function unmount({ nodes, scope }: ForBlock) {
|
||||||
remove(nodes, parent!)
|
invokeWithUnmount(scope, () => {
|
||||||
scope.stop()
|
removeBlock(nodes, parent!)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import { renderEffect } from './renderEffect'
|
|
||||||
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
||||||
import { type EffectScope, effectScope } from '@vue/reactivity'
|
import { getCurrentScope } from '@vue/reactivity'
|
||||||
import { createComment, createTextNode, insert, remove } from './dom/element'
|
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
|
type BlockFn = () => Block
|
||||||
|
|
||||||
|
@ -18,7 +26,8 @@ export const createIf = (
|
||||||
let branch: BlockFn | undefined
|
let branch: BlockFn | undefined
|
||||||
let parent: ParentNode | undefined | null
|
let parent: ParentNode | undefined | null
|
||||||
let block: Block | undefined
|
let block: Block | undefined
|
||||||
let scope: EffectScope | undefined
|
let scope: BlockEffectScope | undefined
|
||||||
|
const parentScope = getCurrentScope()!
|
||||||
const anchor = __DEV__ ? createComment('if') : createTextNode()
|
const anchor = __DEV__ ? createComment('if') : createTextNode()
|
||||||
const fragment: Fragment = {
|
const fragment: Fragment = {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
@ -26,35 +35,37 @@ export const createIf = (
|
||||||
[fragmentKey]: true,
|
[fragmentKey]: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const instance = currentInstance!
|
||||||
|
if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) {
|
||||||
|
warn('createIf() can only be used inside setup()')
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: SSR
|
// TODO: SSR
|
||||||
// if (isHydrating) {
|
// if (isHydrating) {
|
||||||
// parent = hydrationNode!.parentNode
|
// parent = hydrationNode!.parentNode
|
||||||
// setCurrentHydrationNode(hydrationNode!)
|
// setCurrentHydrationNode(hydrationNode!)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (once) {
|
createChildFragmentDirectives(
|
||||||
doIf()
|
anchor,
|
||||||
} else {
|
() => (scope ? [scope] : []),
|
||||||
renderEffect(() => doIf())
|
// source getter
|
||||||
}
|
condition,
|
||||||
|
// init cb
|
||||||
function doIf() {
|
getValue => {
|
||||||
if ((newValue = !!condition()) !== oldValue) {
|
newValue = !!getValue()
|
||||||
parent ||= anchor.parentNode
|
doIf()
|
||||||
if (block) {
|
},
|
||||||
scope!.stop()
|
// effect cb
|
||||||
remove(block, parent!)
|
getValue => {
|
||||||
|
if ((newValue = !!getValue()) !== oldValue) {
|
||||||
|
doIf()
|
||||||
|
} else if (scope) {
|
||||||
|
invokeWithUpdate(scope)
|
||||||
}
|
}
|
||||||
if ((branch = (oldValue = newValue) ? b1 : b2)) {
|
},
|
||||||
scope = effectScope()
|
once,
|
||||||
fragment.nodes = block = scope.run(branch)!
|
)
|
||||||
parent && insert(block, parent, anchor)
|
|
||||||
} else {
|
|
||||||
scope = block = undefined
|
|
||||||
fragment.nodes = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: SSR
|
// TODO: SSR
|
||||||
// if (isHydrating) {
|
// if (isHydrating) {
|
||||||
|
@ -62,4 +73,19 @@ export const createIf = (
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return fragment
|
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 = []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { EffectScope, isRef } from '@vue/reactivity'
|
import { isRef } from '@vue/reactivity'
|
||||||
import { EMPTY_OBJ, hasOwn, isArray, isFunction } from '@vue/shared'
|
import { EMPTY_OBJ, hasOwn, isArray, isFunction } from '@vue/shared'
|
||||||
import type { Block } from './apiRender'
|
import type { Block } from './apiRender'
|
||||||
import type { DirectiveBinding } from './directives'
|
|
||||||
import {
|
import {
|
||||||
type ComponentPropsOptions,
|
type ComponentPropsOptions,
|
||||||
type NormalizedPropsOptions,
|
type NormalizedPropsOptions,
|
||||||
|
@ -27,6 +26,7 @@ import { VaporLifecycleHooks } from './apiLifecycle'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { type AppContext, createAppContext } from './apiCreateVaporApp'
|
import { type AppContext, createAppContext } from './apiCreateVaporApp'
|
||||||
import type { Data } from '@vue/runtime-shared'
|
import type { Data } from '@vue/runtime-shared'
|
||||||
|
import { BlockEffectScope } from './blockEffectScope'
|
||||||
|
|
||||||
export type Component = FunctionalComponent | ObjectComponent
|
export type Component = FunctionalComponent | ObjectComponent
|
||||||
|
|
||||||
|
@ -154,10 +154,9 @@ export interface ComponentInternalInstance {
|
||||||
parent: ComponentInternalInstance | null
|
parent: ComponentInternalInstance | null
|
||||||
|
|
||||||
provides: Data
|
provides: Data
|
||||||
scope: EffectScope
|
scope: BlockEffectScope
|
||||||
component: Component
|
component: Component
|
||||||
comps: Set<ComponentInternalInstance>
|
comps: Set<ComponentInternalInstance>
|
||||||
dirs: Map<Node, DirectiveBinding[]>
|
|
||||||
|
|
||||||
rawProps: NormalizedRawProps
|
rawProps: NormalizedRawProps
|
||||||
propsOptions: NormalizedPropsOptions
|
propsOptions: NormalizedPropsOptions
|
||||||
|
@ -280,11 +279,10 @@ export function createComponentInstance(
|
||||||
|
|
||||||
parent,
|
parent,
|
||||||
|
|
||||||
scope: new EffectScope(true /* detached */)!,
|
scope: null!,
|
||||||
provides: parent ? parent.provides : Object.create(_appContext.provides),
|
provides: parent ? parent.provides : Object.create(_appContext.provides),
|
||||||
component,
|
component,
|
||||||
comps: new Set(),
|
comps: new Set(),
|
||||||
dirs: new Map(),
|
|
||||||
|
|
||||||
// resolved props and emits options
|
// resolved props and emits options
|
||||||
rawProps: null!, // set later
|
rawProps: null!, // set later
|
||||||
|
@ -355,6 +353,7 @@ export function createComponentInstance(
|
||||||
*/
|
*/
|
||||||
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
|
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
|
||||||
}
|
}
|
||||||
|
instance.scope = new BlockEffectScope(instance, parent && parent.scope)
|
||||||
initProps(instance, rawProps, !isFunction(component), once)
|
initProps(instance, rawProps, !isFunction(component), once)
|
||||||
initSlots(instance, slots, dynamicSlots)
|
initSlots(instance, slots, dynamicSlots)
|
||||||
instance.emit = emit.bind(null, instance)
|
instance.emit = emit.bind(null, instance)
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function invokeLifecycle(
|
||||||
post ? queuePostFlushCb(fn) : fn()
|
post ? queuePostFlushCb(fn) : fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
invokeDirectiveHook(instance, directive)
|
invokeDirectiveHook(instance, directive, instance.scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
function invokeSub() {
|
function invokeSub() {
|
||||||
|
|
|
@ -1,32 +1,48 @@
|
||||||
import { isFunction } from '@vue/shared'
|
import { invokeArrayFns, isFunction } from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
type ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
currentInstance,
|
currentInstance,
|
||||||
isVaporComponent,
|
isVaporComponent,
|
||||||
|
setCurrentInstance,
|
||||||
} from './component'
|
} from './component'
|
||||||
import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
|
import {
|
||||||
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
|
EffectFlags,
|
||||||
import { renderEffect } from './renderEffect'
|
ReactiveEffect,
|
||||||
|
type SchedulerJob,
|
||||||
|
getCurrentScope,
|
||||||
|
pauseTracking,
|
||||||
|
resetTracking,
|
||||||
|
traverse,
|
||||||
|
} from '@vue/reactivity'
|
||||||
|
import {
|
||||||
|
VaporErrorCodes,
|
||||||
|
callWithAsyncErrorHandling,
|
||||||
|
callWithErrorHandling,
|
||||||
|
} from './errorHandling'
|
||||||
|
import { queueJob, queuePostFlushCb } from './scheduler'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
|
import { type BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
|
||||||
import { normalizeBlock } from './dom/element'
|
import { normalizeBlock } from './dom/element'
|
||||||
|
|
||||||
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
|
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
|
||||||
|
|
||||||
export interface DirectiveBinding<V = any, M extends string = string> {
|
export interface DirectiveBinding<T = any, V = any, M extends string = string> {
|
||||||
instance: ComponentInternalInstance
|
instance: ComponentInternalInstance
|
||||||
source?: () => V
|
source?: () => V
|
||||||
value: V
|
value: V
|
||||||
oldValue: V | null
|
oldValue: V | null
|
||||||
arg?: string
|
arg?: string
|
||||||
modifiers?: DirectiveModifiers<M>
|
modifiers?: DirectiveModifiers<M>
|
||||||
dir: ObjectDirective<any, V>
|
dir: ObjectDirective<T, V, M>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DirectiveBindingsMap = Map<Node, DirectiveBinding[]>
|
||||||
|
|
||||||
export type DirectiveHook<
|
export type DirectiveHook<
|
||||||
T = any | null,
|
T = any | null,
|
||||||
V = any,
|
V = any,
|
||||||
M extends string = string,
|
M extends string = string,
|
||||||
> = (node: T, binding: DirectiveBinding<V, M>) => void
|
> = (node: T, binding: DirectiveBinding<T, V, M>) => void
|
||||||
|
|
||||||
// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
|
// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
|
||||||
// effect update -> `beforeUpdate` -> node updated -> `updated`
|
// effect update -> `beforeUpdate` -> node updated -> `updated`
|
||||||
|
@ -43,7 +59,7 @@ export type ObjectDirective<T = any, V = any, M extends string = string> = {
|
||||||
[K in DirectiveHookName]?: DirectiveHook<T, V, M> | undefined
|
[K in DirectiveHookName]?: DirectiveHook<T, V, M> | undefined
|
||||||
} & {
|
} & {
|
||||||
/** Watch value deeply */
|
/** Watch value deeply */
|
||||||
deep?: boolean
|
deep?: boolean | number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FunctionDirective<
|
export type FunctionDirective<
|
||||||
|
@ -86,9 +102,18 @@ export function withDirectives<T extends ComponentInternalInstance | Node>(
|
||||||
node = nodeOrComponent
|
node = nodeOrComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = currentInstance
|
let bindings: DirectiveBinding[]
|
||||||
if (!instance.dirs.has(node)) instance.dirs.set(node, [])
|
const instance = currentInstance!
|
||||||
const bindings = instance.dirs.get(node)!
|
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) {
|
for (const directive of directives) {
|
||||||
let [dir, source, arg, modifiers] = directive
|
let [dir, source, arg, modifiers] = directive
|
||||||
|
@ -103,25 +128,38 @@ export function withDirectives<T extends ComponentInternalInstance | Node>(
|
||||||
const binding: DirectiveBinding = {
|
const binding: DirectiveBinding = {
|
||||||
dir,
|
dir,
|
||||||
instance,
|
instance,
|
||||||
source,
|
|
||||||
value: null, // set later
|
value: null, // set later
|
||||||
oldValue: undefined,
|
oldValue: undefined,
|
||||||
arg,
|
arg,
|
||||||
modifiers,
|
modifiers,
|
||||||
}
|
}
|
||||||
bindings.push(binding)
|
|
||||||
|
|
||||||
callDirectiveHook(node, binding, instance, 'created')
|
|
||||||
|
|
||||||
// register source
|
|
||||||
if (source) {
|
if (source) {
|
||||||
if (dir.deep) {
|
if (dir.deep) {
|
||||||
const deep = dir.deep === true ? undefined : dir.deep
|
const deep = dir.deep === true ? undefined : dir.deep
|
||||||
const baseSource = source
|
const baseSource = source
|
||||||
source = () => traverse(baseSource(), deep)
|
source = () => traverse(baseSource(), deep)
|
||||||
}
|
}
|
||||||
renderEffect(source)
|
|
||||||
|
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodeOrComponent
|
return nodeOrComponent
|
||||||
|
@ -145,13 +183,14 @@ function getComponentNode(component: ComponentInternalInstance) {
|
||||||
export function invokeDirectiveHook(
|
export function invokeDirectiveHook(
|
||||||
instance: ComponentInternalInstance | null,
|
instance: ComponentInternalInstance | null,
|
||||||
name: DirectiveHookName,
|
name: DirectiveHookName,
|
||||||
nodes?: IterableIterator<Node>,
|
scope: BlockEffectScope,
|
||||||
) {
|
) {
|
||||||
if (!instance) return
|
const { dirs } = scope
|
||||||
nodes = nodes || instance.dirs.keys()
|
if (name === 'mounted') scope.im = true
|
||||||
for (const node of nodes) {
|
if (!dirs) return
|
||||||
const directives = instance.dirs.get(node) || []
|
const iterator = dirs.entries()
|
||||||
for (const binding of directives) {
|
for (const [node, bindings] of iterator) {
|
||||||
|
for (const binding of bindings) {
|
||||||
callDirectiveHook(node, binding, instance, name)
|
callDirectiveHook(node, binding, instance, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,3 +218,43 @@ function callDirectiveHook(
|
||||||
])
|
])
|
||||||
resetTracking()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
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) {
|
||||||
|
if (isRenderEffectScope(scope.parent) && !scope.parent.im) {
|
||||||
|
return handler && handler()
|
||||||
|
}
|
||||||
|
return invokeWithDirsHooks(scope, 'mount', handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invokeWithUnmount(
|
||||||
|
scope: BlockEffectScope,
|
||||||
|
handler?: () => void,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return invokeWithDirsHooks(scope, 'unmount', handler)
|
||||||
|
} finally {
|
||||||
|
scope.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invokeWithUpdate(
|
||||||
|
scope: BlockEffectScope,
|
||||||
|
handler?: () => void,
|
||||||
|
) {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,41 +18,6 @@ import { invokeDirectiveHook } from './directives'
|
||||||
export function renderEffect(cb: () => void) {
|
export function renderEffect(cb: () => void) {
|
||||||
const instance = getCurrentInstance()
|
const instance = getCurrentInstance()
|
||||||
const scope = getCurrentScope()
|
const scope = getCurrentScope()
|
||||||
let effect: ReactiveEffect
|
|
||||||
|
|
||||||
const job: SchedulerJob = () => {
|
|
||||||
if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance && instance.isMounted && !instance.isUpdating) {
|
|
||||||
instance.isUpdating = true
|
|
||||||
|
|
||||||
const { bu, u, dirs } = instance
|
|
||||||
// beforeUpdate hook
|
|
||||||
if (bu) {
|
|
||||||
invokeArrayFns(bu)
|
|
||||||
}
|
|
||||||
if (dirs) {
|
|
||||||
invokeDirectiveHook(instance, 'beforeUpdate')
|
|
||||||
}
|
|
||||||
|
|
||||||
effect.run()
|
|
||||||
|
|
||||||
queuePostFlushCb(() => {
|
|
||||||
instance.isUpdating = false
|
|
||||||
if (dirs) {
|
|
||||||
invokeDirectiveHook(instance, 'updated')
|
|
||||||
}
|
|
||||||
// updated hook
|
|
||||||
if (u) {
|
|
||||||
queuePostFlushCb(u)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
effect.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scope) {
|
if (scope) {
|
||||||
const baseCb = cb
|
const baseCb = cb
|
||||||
|
@ -66,16 +31,14 @@ export function renderEffect(cb: () => void) {
|
||||||
baseCb()
|
baseCb()
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
|
job.id = instance.uid
|
||||||
}
|
}
|
||||||
|
|
||||||
effect = new ReactiveEffect(() =>
|
const effect = new ReactiveEffect(() =>
|
||||||
callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
|
callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
|
||||||
)
|
)
|
||||||
|
|
||||||
effect.scheduler = () => {
|
effect.scheduler = () => queueJob(job)
|
||||||
if (instance) job.id = instance.uid
|
|
||||||
queueJob(job)
|
|
||||||
}
|
|
||||||
if (__DEV__ && instance) {
|
if (__DEV__ && instance) {
|
||||||
effect.onTrack = instance.rtc
|
effect.onTrack = instance.rtc
|
||||||
? e => invokeArrayFns(instance.rtc!, e)
|
? e => invokeArrayFns(instance.rtc!, e)
|
||||||
|
@ -85,6 +48,47 @@ export function renderEffect(cb: () => void) {
|
||||||
: void 0
|
: void 0
|
||||||
}
|
}
|
||||||
effect.run()
|
effect.run()
|
||||||
|
|
||||||
|
function job() {
|
||||||
|
if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = instance && setCurrentInstance(instance)
|
||||||
|
|
||||||
|
if (instance && instance.isMounted && !instance.isUpdating) {
|
||||||
|
instance.isUpdating = true
|
||||||
|
|
||||||
|
const { bu, u, scope } = instance
|
||||||
|
const { dirs } = scope
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
reset()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
effect.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
reset && reset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function firstEffect(
|
export function firstEffect(
|
||||||
|
|
Loading…
Reference in New Issue