mirror of https://github.com/vuejs/core.git
test(vapor): apiWatch
This commit is contained in:
parent
12ef12105b
commit
4366a7e213
|
@ -494,7 +494,7 @@ export {
|
||||||
validateProps,
|
validateProps,
|
||||||
} from './componentProps'
|
} from './componentProps'
|
||||||
export { baseEmit, isEmitListener } from './componentEmits'
|
export { baseEmit, isEmitListener } from './componentEmits'
|
||||||
export { type SchedulerJob, queueJob } from './scheduler'
|
export { type SchedulerJob, queueJob, flushOnAppMount } from './scheduler'
|
||||||
export {
|
export {
|
||||||
type ComponentInternalOptions,
|
type ComponentInternalOptions,
|
||||||
type GenericComponentInstance,
|
type GenericComponentInstance,
|
||||||
|
|
|
@ -45,7 +45,7 @@ import {
|
||||||
type SchedulerJob,
|
type SchedulerJob,
|
||||||
SchedulerJobFlags,
|
SchedulerJobFlags,
|
||||||
type SchedulerJobs,
|
type SchedulerJobs,
|
||||||
flushPostFlushCbs,
|
flushOnAppMount,
|
||||||
flushPreFlushCbs,
|
flushPreFlushCbs,
|
||||||
queueJob,
|
queueJob,
|
||||||
queuePostFlushCb,
|
queuePostFlushCb,
|
||||||
|
@ -2357,7 +2357,6 @@ function baseCreateRenderer(
|
||||||
return teleportEnd ? hostNextSibling(teleportEnd) : el
|
return teleportEnd ? hostNextSibling(teleportEnd) : el
|
||||||
}
|
}
|
||||||
|
|
||||||
let isFlushing = false
|
|
||||||
const render: RootRenderFunction = (vnode, container, namespace) => {
|
const render: RootRenderFunction = (vnode, container, namespace) => {
|
||||||
if (vnode == null) {
|
if (vnode == null) {
|
||||||
if (container._vnode) {
|
if (container._vnode) {
|
||||||
|
@ -2375,12 +2374,7 @@ function baseCreateRenderer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
container._vnode = vnode
|
container._vnode = vnode
|
||||||
if (!isFlushing) {
|
flushOnAppMount()
|
||||||
isFlushing = true
|
|
||||||
flushPreFlushCbs()
|
|
||||||
flushPostFlushCbs()
|
|
||||||
isFlushing = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const internals: RendererInternals = {
|
const internals: RendererInternals = {
|
||||||
|
@ -2449,7 +2443,7 @@ function baseCreateRenderer(
|
||||||
createApp: createAppAPI(
|
createApp: createAppAPI(
|
||||||
mountApp,
|
mountApp,
|
||||||
unmountApp,
|
unmountApp,
|
||||||
getComponentPublicInstance,
|
getComponentPublicInstance as any,
|
||||||
render,
|
render,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,6 +217,19 @@ export function flushPostFlushCbs(seen?: CountMap): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isFlushing = false
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function flushOnAppMount(): void {
|
||||||
|
if (!isFlushing) {
|
||||||
|
isFlushing = true
|
||||||
|
flushPreFlushCbs()
|
||||||
|
flushPostFlushCbs()
|
||||||
|
isFlushing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getId = (job: SchedulerJob): number =>
|
const getId = (job: SchedulerJob): number =>
|
||||||
job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
|
job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
|
||||||
|
|
||||||
|
|
|
@ -1,80 +1,330 @@
|
||||||
import type { Ref } from '@vue/reactivity'
|
|
||||||
import {
|
import {
|
||||||
EffectScope,
|
currentInstance,
|
||||||
|
effectScope,
|
||||||
nextTick,
|
nextTick,
|
||||||
onWatcherCleanup,
|
onMounted,
|
||||||
|
onUpdated,
|
||||||
ref,
|
ref,
|
||||||
|
watch,
|
||||||
watchEffect,
|
watchEffect,
|
||||||
watchSyncEffect,
|
} from '@vue/runtime-dom'
|
||||||
} from '../src'
|
import { createComponent, defineVaporComponent, renderEffect } from '../src'
|
||||||
|
import { makeRender } from './_utils'
|
||||||
|
import type { VaporComponentInstance } from '../src/component'
|
||||||
|
|
||||||
describe.todo('watchEffect and onWatcherCleanup', () => {
|
const define = makeRender()
|
||||||
test('basic', async () => {
|
|
||||||
let dummy = 0
|
|
||||||
let source: Ref<number>
|
|
||||||
const scope = new EffectScope()
|
|
||||||
|
|
||||||
scope.run(() => {
|
// only need to port test cases related to in-component usage
|
||||||
source = ref(0)
|
describe('apiWatch', () => {
|
||||||
watchEffect(onCleanup => {
|
// #7030
|
||||||
source.value
|
it.todo(
|
||||||
|
// need if support
|
||||||
onCleanup(() => (dummy += 2))
|
'should not fire on child component unmount w/ flush: pre',
|
||||||
onWatcherCleanup(() => (dummy += 3))
|
async () => {
|
||||||
onWatcherCleanup(() => (dummy += 5))
|
const visible = ref(true)
|
||||||
|
const cb = vi.fn()
|
||||||
|
const Parent = defineVaporComponent({
|
||||||
|
props: ['visible'],
|
||||||
|
setup() {
|
||||||
|
// @ts-expect-error
|
||||||
|
return visible.value ? h(Comp) : null
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
watch(visible, cb, { flush: 'pre' })
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
define(Parent).render({
|
||||||
|
visible: () => visible.value,
|
||||||
})
|
})
|
||||||
|
expect(cb).not.toHaveBeenCalled()
|
||||||
|
visible.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toBe(0)
|
expect(cb).not.toHaveBeenCalled()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
scope.run(() => {
|
// #7030
|
||||||
source.value++
|
it('flush: pre watcher in child component should not fire before parent update', async () => {
|
||||||
|
const b = ref(0)
|
||||||
|
const calls: string[] = []
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
watch(
|
||||||
|
() => b.value,
|
||||||
|
val => {
|
||||||
|
calls.push('watcher child')
|
||||||
|
},
|
||||||
|
{ flush: 'pre' },
|
||||||
|
)
|
||||||
|
renderEffect(() => {
|
||||||
|
b.value
|
||||||
|
calls.push('render child')
|
||||||
})
|
})
|
||||||
await nextTick()
|
return []
|
||||||
expect(dummy).toBe(10)
|
},
|
||||||
|
}
|
||||||
|
|
||||||
scope.run(() => {
|
const Parent = {
|
||||||
source.value++
|
props: ['a'],
|
||||||
})
|
setup() {
|
||||||
await nextTick()
|
watch(
|
||||||
expect(dummy).toBe(20)
|
() => b.value,
|
||||||
|
val => {
|
||||||
scope.stop()
|
calls.push('watcher parent')
|
||||||
await nextTick()
|
},
|
||||||
expect(dummy).toBe(30)
|
{ flush: 'pre' },
|
||||||
|
)
|
||||||
|
renderEffect(() => {
|
||||||
|
b.value
|
||||||
|
calls.push('render parent')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('nested call to watchEffect', async () => {
|
return createComponent(Comp)
|
||||||
let dummy = 0
|
},
|
||||||
let source: Ref<number>
|
}
|
||||||
let double: Ref<number>
|
|
||||||
const scope = new EffectScope()
|
|
||||||
|
|
||||||
scope.run(() => {
|
define(Parent).render({
|
||||||
source = ref(0)
|
a: () => b.value,
|
||||||
double = ref(0)
|
})
|
||||||
|
|
||||||
|
expect(calls).toEqual(['render parent', 'render child'])
|
||||||
|
|
||||||
|
b.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([
|
||||||
|
'render parent',
|
||||||
|
'render child',
|
||||||
|
'watcher parent',
|
||||||
|
'render parent',
|
||||||
|
'watcher child',
|
||||||
|
'render child',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// #1763
|
||||||
|
it('flush: pre watcher watching props should fire before child update', async () => {
|
||||||
|
const a = ref(0)
|
||||||
|
const b = ref(0)
|
||||||
|
const c = ref(0)
|
||||||
|
const calls: string[] = []
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
props: ['a', 'b'],
|
||||||
|
setup(props: any) {
|
||||||
|
watch(
|
||||||
|
() => props.a + props.b,
|
||||||
|
() => {
|
||||||
|
calls.push('watcher 1')
|
||||||
|
c.value++
|
||||||
|
},
|
||||||
|
{ flush: 'pre' },
|
||||||
|
)
|
||||||
|
|
||||||
|
// #1777 chained pre-watcher
|
||||||
|
watch(
|
||||||
|
c,
|
||||||
|
() => {
|
||||||
|
calls.push('watcher 2')
|
||||||
|
},
|
||||||
|
{ flush: 'pre' },
|
||||||
|
)
|
||||||
|
renderEffect(() => {
|
||||||
|
c.value
|
||||||
|
calls.push('render')
|
||||||
|
})
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
define(Comp).render({
|
||||||
|
a: () => a.value,
|
||||||
|
b: () => b.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls).toEqual(['render'])
|
||||||
|
|
||||||
|
// both props are updated
|
||||||
|
// should trigger pre-flush watcher first and only once
|
||||||
|
// then trigger child render
|
||||||
|
a.value++
|
||||||
|
b.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
|
||||||
|
})
|
||||||
|
|
||||||
|
// #5721
|
||||||
|
it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const calls: string[] = []
|
||||||
|
const App = {
|
||||||
|
setup() {
|
||||||
|
watch(
|
||||||
|
count,
|
||||||
|
() => {
|
||||||
|
calls.push('watch ' + count.value)
|
||||||
|
},
|
||||||
|
{ flush: 'pre' },
|
||||||
|
)
|
||||||
|
onMounted(() => {
|
||||||
|
calls.push('mounted')
|
||||||
|
})
|
||||||
|
// mutate multiple times
|
||||||
|
count.value++
|
||||||
|
count.value++
|
||||||
|
count.value++
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
define(App).render()
|
||||||
|
expect(calls).toMatchObject(['watch 3', 'mounted'])
|
||||||
|
})
|
||||||
|
|
||||||
|
// #1852
|
||||||
|
it.todo(
|
||||||
|
// need if + templateRef
|
||||||
|
'flush: post watcher should fire after template refs updated',
|
||||||
|
async () => {
|
||||||
|
const toggle = ref(false)
|
||||||
|
let dom: Element | null = null
|
||||||
|
|
||||||
|
const App = {
|
||||||
|
setup() {
|
||||||
|
const domRef = ref<Element | null>(null)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
toggle,
|
||||||
|
() => {
|
||||||
|
dom = domRef.value
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// @ts-expect-error
|
||||||
|
return toggle.value ? h('p', { ref: domRef }) : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
render(h(App), nodeOps.createElement('div'))
|
||||||
|
expect(dom).toBe(null)
|
||||||
|
|
||||||
|
toggle.value = true
|
||||||
|
await nextTick()
|
||||||
|
expect(dom!.tagName).toBe('P')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
test('should not leak `this.proxy` to setup()', () => {
|
||||||
|
const source = vi.fn()
|
||||||
|
|
||||||
|
const Comp = defineVaporComponent({
|
||||||
|
setup() {
|
||||||
|
watch(source, () => {})
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
define(Comp).render()
|
||||||
|
|
||||||
|
// should not have any arguments
|
||||||
|
expect(source.mock.calls[0]).toMatchObject([])
|
||||||
|
})
|
||||||
|
|
||||||
|
// #2728
|
||||||
|
test('pre watcher callbacks should not track dependencies', async () => {
|
||||||
|
const a = ref(0)
|
||||||
|
const b = ref(0)
|
||||||
|
const updated = vi.fn()
|
||||||
|
|
||||||
|
const Comp = defineVaporComponent({
|
||||||
|
props: ['a'],
|
||||||
|
setup(props) {
|
||||||
|
onUpdated(updated)
|
||||||
|
watch(
|
||||||
|
() => props.a,
|
||||||
|
() => {
|
||||||
|
b.value
|
||||||
|
},
|
||||||
|
)
|
||||||
|
renderEffect(() => {
|
||||||
|
props.a
|
||||||
|
})
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
define(Comp).render({
|
||||||
|
a: () => a.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
a.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(updated).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
b.value++
|
||||||
|
await nextTick()
|
||||||
|
// should not track b as dependency of Child
|
||||||
|
expect(updated).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// #4158
|
||||||
|
test('watch should not register in owner component if created inside detached scope', () => {
|
||||||
|
let instance: VaporComponentInstance
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
instance = currentInstance as VaporComponentInstance
|
||||||
|
effectScope(true).run(() => {
|
||||||
|
watch(
|
||||||
|
() => 1,
|
||||||
|
() => {},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
define(Comp).render()
|
||||||
|
// should not record watcher in detached scope
|
||||||
|
expect(instance!.scope.effects.length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('watchEffect should keep running if created in a detached scope', async () => {
|
||||||
|
const trigger = ref(0)
|
||||||
|
let countWE = 0
|
||||||
|
let countW = 0
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
effectScope(true).run(() => {
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
double.value = source.value * 2
|
trigger.value
|
||||||
onWatcherCleanup(() => (dummy += 2))
|
countWE++
|
||||||
})
|
|
||||||
watchSyncEffect(() => {
|
|
||||||
double.value
|
|
||||||
onWatcherCleanup(() => (dummy += 3))
|
|
||||||
})
|
})
|
||||||
|
watch(trigger, () => countW++)
|
||||||
})
|
})
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const { app } = define(Comp).render()
|
||||||
|
// only watchEffect as ran so far
|
||||||
|
expect(countWE).toBe(1)
|
||||||
|
expect(countW).toBe(0)
|
||||||
|
trigger.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toBe(0)
|
// both watchers run while component is mounted
|
||||||
|
expect(countWE).toBe(2)
|
||||||
|
expect(countW).toBe(1)
|
||||||
|
|
||||||
scope.run(() => source.value++)
|
app.unmount()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toBe(5)
|
trigger.value++
|
||||||
|
|
||||||
scope.run(() => source.value++)
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toBe(10)
|
// both watchers run again event though component has been unmounted
|
||||||
|
expect(countWE).toBe(3)
|
||||||
scope.stop()
|
expect(countW).toBe(2)
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toBe(15)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,7 +5,7 @@ const node2 = document.createTextNode('node2')
|
||||||
const node3 = document.createTextNode('node3')
|
const node3 = document.createTextNode('node3')
|
||||||
const anchor = document.createTextNode('anchor')
|
const anchor = document.createTextNode('anchor')
|
||||||
|
|
||||||
describe('node ops', () => {
|
describe('block + node ops', () => {
|
||||||
test('normalizeBlock', () => {
|
test('normalizeBlock', () => {
|
||||||
expect(normalizeBlock([node1, node2, node3])).toEqual([node1, node2, node3])
|
expect(normalizeBlock([node1, node2, node3])).toEqual([node1, node2, node3])
|
||||||
expect(normalizeBlock([node1, [node2, [node3]]])).toEqual([
|
expect(normalizeBlock([node1, [node2, [node3]]])).toEqual([
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
type AppUnmountFn,
|
type AppUnmountFn,
|
||||||
type CreateAppFunction,
|
type CreateAppFunction,
|
||||||
createAppAPI,
|
createAppAPI,
|
||||||
|
flushOnAppMount,
|
||||||
normalizeContainer,
|
normalizeContainer,
|
||||||
warn,
|
warn,
|
||||||
} from '@vue/runtime-dom'
|
} from '@vue/runtime-dom'
|
||||||
|
@ -23,6 +24,7 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
|
||||||
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
||||||
container.textContent = ''
|
container.textContent = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = createComponent(
|
const instance = createComponent(
|
||||||
app._component,
|
app._component,
|
||||||
app._props as RawProps,
|
app._props as RawProps,
|
||||||
|
@ -30,7 +32,10 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
|
||||||
false,
|
false,
|
||||||
app._context,
|
app._context,
|
||||||
)
|
)
|
||||||
|
|
||||||
mountComponent(instance, container)
|
mountComponent(instance, container)
|
||||||
|
flushOnAppMount()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
nextUid,
|
nextUid,
|
||||||
popWarningContext,
|
popWarningContext,
|
||||||
pushWarningContext,
|
pushWarningContext,
|
||||||
|
queuePostFlushCb,
|
||||||
registerHMR,
|
registerHMR,
|
||||||
simpleSetCurrentInstance,
|
simpleSetCurrentInstance,
|
||||||
startMeasure,
|
startMeasure,
|
||||||
|
@ -453,10 +454,8 @@ export function mountComponent(
|
||||||
if (!instance.isMounted) {
|
if (!instance.isMounted) {
|
||||||
if (instance.bm) invokeArrayFns(instance.bm)
|
if (instance.bm) invokeArrayFns(instance.bm)
|
||||||
insert(instance.block, parent, anchor)
|
insert(instance.block, parent, anchor)
|
||||||
// TODO queuePostFlushCb(() => {
|
if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
|
||||||
if (instance.m) invokeArrayFns(instance.m)
|
|
||||||
instance.isMounted = true
|
instance.isMounted = true
|
||||||
// })
|
|
||||||
} else {
|
} else {
|
||||||
insert(instance.block, parent, anchor)
|
insert(instance.block, parent, anchor)
|
||||||
}
|
}
|
||||||
|
@ -479,10 +478,10 @@ export function unmountComponent(
|
||||||
unmountComponent(c)
|
unmountComponent(c)
|
||||||
}
|
}
|
||||||
if (parent) remove(instance.block, parent)
|
if (parent) remove(instance.block, parent)
|
||||||
// TODO queuePostFlushCb(() => {
|
if (instance.um) {
|
||||||
if (instance.um) invokeArrayFns(instance.um)
|
queuePostFlushCb(() => invokeArrayFns(instance.um!))
|
||||||
|
}
|
||||||
instance.isUnmounted = true
|
instance.isUnmounted = true
|
||||||
// })
|
|
||||||
} else if (parent) {
|
} else if (parent) {
|
||||||
remove(instance.block, parent)
|
remove(instance.block, parent)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue