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,
|
||||
} from './componentProps'
|
||||
export { baseEmit, isEmitListener } from './componentEmits'
|
||||
export { type SchedulerJob, queueJob } from './scheduler'
|
||||
export { type SchedulerJob, queueJob, flushOnAppMount } from './scheduler'
|
||||
export {
|
||||
type ComponentInternalOptions,
|
||||
type GenericComponentInstance,
|
||||
|
|
|
@ -45,7 +45,7 @@ import {
|
|||
type SchedulerJob,
|
||||
SchedulerJobFlags,
|
||||
type SchedulerJobs,
|
||||
flushPostFlushCbs,
|
||||
flushOnAppMount,
|
||||
flushPreFlushCbs,
|
||||
queueJob,
|
||||
queuePostFlushCb,
|
||||
|
@ -2357,7 +2357,6 @@ function baseCreateRenderer(
|
|||
return teleportEnd ? hostNextSibling(teleportEnd) : el
|
||||
}
|
||||
|
||||
let isFlushing = false
|
||||
const render: RootRenderFunction = (vnode, container, namespace) => {
|
||||
if (vnode == null) {
|
||||
if (container._vnode) {
|
||||
|
@ -2375,12 +2374,7 @@ function baseCreateRenderer(
|
|||
)
|
||||
}
|
||||
container._vnode = vnode
|
||||
if (!isFlushing) {
|
||||
isFlushing = true
|
||||
flushPreFlushCbs()
|
||||
flushPostFlushCbs()
|
||||
isFlushing = false
|
||||
}
|
||||
flushOnAppMount()
|
||||
}
|
||||
|
||||
const internals: RendererInternals = {
|
||||
|
@ -2449,7 +2443,7 @@ function baseCreateRenderer(
|
|||
createApp: createAppAPI(
|
||||
mountApp,
|
||||
unmountApp,
|
||||
getComponentPublicInstance,
|
||||
getComponentPublicInstance as any,
|
||||
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 =>
|
||||
job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
|
||||
|
||||
|
|
|
@ -1,80 +1,330 @@
|
|||
import type { Ref } from '@vue/reactivity'
|
||||
import {
|
||||
EffectScope,
|
||||
currentInstance,
|
||||
effectScope,
|
||||
nextTick,
|
||||
onWatcherCleanup,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
ref,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchSyncEffect,
|
||||
} from '../src'
|
||||
} from '@vue/runtime-dom'
|
||||
import { createComponent, defineVaporComponent, renderEffect } from '../src'
|
||||
import { makeRender } from './_utils'
|
||||
import type { VaporComponentInstance } from '../src/component'
|
||||
|
||||
describe.todo('watchEffect and onWatcherCleanup', () => {
|
||||
test('basic', async () => {
|
||||
let dummy = 0
|
||||
let source: Ref<number>
|
||||
const scope = new EffectScope()
|
||||
const define = makeRender()
|
||||
|
||||
scope.run(() => {
|
||||
source = ref(0)
|
||||
watchEffect(onCleanup => {
|
||||
source.value
|
||||
|
||||
onCleanup(() => (dummy += 2))
|
||||
onWatcherCleanup(() => (dummy += 3))
|
||||
onWatcherCleanup(() => (dummy += 5))
|
||||
// only need to port test cases related to in-component usage
|
||||
describe('apiWatch', () => {
|
||||
// #7030
|
||||
it.todo(
|
||||
// need if support
|
||||
'should not fire on child component unmount w/ flush: pre',
|
||||
async () => {
|
||||
const visible = ref(true)
|
||||
const cb = vi.fn()
|
||||
const Parent = defineVaporComponent({
|
||||
props: ['visible'],
|
||||
setup() {
|
||||
// @ts-expect-error
|
||||
return visible.value ? h(Comp) : null
|
||||
},
|
||||
})
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toBe(0)
|
||||
const Comp = {
|
||||
setup() {
|
||||
watch(visible, cb, { flush: 'pre' })
|
||||
return []
|
||||
},
|
||||
}
|
||||
define(Parent).render({
|
||||
visible: () => visible.value,
|
||||
})
|
||||
expect(cb).not.toHaveBeenCalled()
|
||||
visible.value = false
|
||||
await nextTick()
|
||||
expect(cb).not.toHaveBeenCalled()
|
||||
},
|
||||
)
|
||||
|
||||
scope.run(() => {
|
||||
source.value++
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toBe(10)
|
||||
// #7030
|
||||
it('flush: pre watcher in child component should not fire before parent update', async () => {
|
||||
const b = ref(0)
|
||||
const calls: string[] = []
|
||||
|
||||
scope.run(() => {
|
||||
source.value++
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toBe(20)
|
||||
const Comp = {
|
||||
setup() {
|
||||
watch(
|
||||
() => b.value,
|
||||
val => {
|
||||
calls.push('watcher child')
|
||||
},
|
||||
{ flush: 'pre' },
|
||||
)
|
||||
renderEffect(() => {
|
||||
b.value
|
||||
calls.push('render child')
|
||||
})
|
||||
return []
|
||||
},
|
||||
}
|
||||
|
||||
scope.stop()
|
||||
const Parent = {
|
||||
props: ['a'],
|
||||
setup() {
|
||||
watch(
|
||||
() => b.value,
|
||||
val => {
|
||||
calls.push('watcher parent')
|
||||
},
|
||||
{ flush: 'pre' },
|
||||
)
|
||||
renderEffect(() => {
|
||||
b.value
|
||||
calls.push('render parent')
|
||||
})
|
||||
|
||||
return createComponent(Comp)
|
||||
},
|
||||
}
|
||||
|
||||
define(Parent).render({
|
||||
a: () => b.value,
|
||||
})
|
||||
|
||||
expect(calls).toEqual(['render parent', 'render child'])
|
||||
|
||||
b.value++
|
||||
await nextTick()
|
||||
expect(dummy).toBe(30)
|
||||
expect(calls).toEqual([
|
||||
'render parent',
|
||||
'render child',
|
||||
'watcher parent',
|
||||
'render parent',
|
||||
'watcher child',
|
||||
'render child',
|
||||
])
|
||||
})
|
||||
|
||||
test('nested call to watchEffect', async () => {
|
||||
let dummy = 0
|
||||
let source: Ref<number>
|
||||
let double: Ref<number>
|
||||
const scope = new EffectScope()
|
||||
// #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[] = []
|
||||
|
||||
scope.run(() => {
|
||||
source = ref(0)
|
||||
double = ref(0)
|
||||
watchEffect(() => {
|
||||
double.value = source.value * 2
|
||||
onWatcherCleanup(() => (dummy += 2))
|
||||
})
|
||||
watchSyncEffect(() => {
|
||||
double.value
|
||||
onWatcherCleanup(() => (dummy += 3))
|
||||
})
|
||||
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,
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toBe(0)
|
||||
|
||||
scope.run(() => source.value++)
|
||||
await nextTick()
|
||||
expect(dummy).toBe(5)
|
||||
expect(calls).toEqual(['render'])
|
||||
|
||||
scope.run(() => source.value++)
|
||||
// both props are updated
|
||||
// should trigger pre-flush watcher first and only once
|
||||
// then trigger child render
|
||||
a.value++
|
||||
b.value++
|
||||
await nextTick()
|
||||
expect(dummy).toBe(10)
|
||||
expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
|
||||
})
|
||||
|
||||
scope.stop()
|
||||
// #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(dummy).toBe(15)
|
||||
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(() => {
|
||||
trigger.value
|
||||
countWE++
|
||||
})
|
||||
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()
|
||||
// both watchers run while component is mounted
|
||||
expect(countWE).toBe(2)
|
||||
expect(countW).toBe(1)
|
||||
|
||||
app.unmount()
|
||||
await nextTick()
|
||||
trigger.value++
|
||||
await nextTick()
|
||||
// both watchers run again event though component has been unmounted
|
||||
expect(countWE).toBe(3)
|
||||
expect(countW).toBe(2)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,7 +5,7 @@ const node2 = document.createTextNode('node2')
|
|||
const node3 = document.createTextNode('node3')
|
||||
const anchor = document.createTextNode('anchor')
|
||||
|
||||
describe('node ops', () => {
|
||||
describe('block + node ops', () => {
|
||||
test('normalizeBlock', () => {
|
||||
expect(normalizeBlock([node1, node2, node3])).toEqual([node1, node2, node3])
|
||||
expect(normalizeBlock([node1, [node2, [node3]]])).toEqual([
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
type AppUnmountFn,
|
||||
type CreateAppFunction,
|
||||
createAppAPI,
|
||||
flushOnAppMount,
|
||||
normalizeContainer,
|
||||
warn,
|
||||
} from '@vue/runtime-dom'
|
||||
|
@ -23,6 +24,7 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
|
|||
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
||||
container.textContent = ''
|
||||
}
|
||||
|
||||
const instance = createComponent(
|
||||
app._component,
|
||||
app._props as RawProps,
|
||||
|
@ -30,7 +32,10 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
|
|||
false,
|
||||
app._context,
|
||||
)
|
||||
|
||||
mountComponent(instance, container)
|
||||
flushOnAppMount()
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
nextUid,
|
||||
popWarningContext,
|
||||
pushWarningContext,
|
||||
queuePostFlushCb,
|
||||
registerHMR,
|
||||
simpleSetCurrentInstance,
|
||||
startMeasure,
|
||||
|
@ -453,10 +454,8 @@ export function mountComponent(
|
|||
if (!instance.isMounted) {
|
||||
if (instance.bm) invokeArrayFns(instance.bm)
|
||||
insert(instance.block, parent, anchor)
|
||||
// TODO queuePostFlushCb(() => {
|
||||
if (instance.m) invokeArrayFns(instance.m)
|
||||
if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
|
||||
instance.isMounted = true
|
||||
// })
|
||||
} else {
|
||||
insert(instance.block, parent, anchor)
|
||||
}
|
||||
|
@ -479,10 +478,10 @@ export function unmountComponent(
|
|||
unmountComponent(c)
|
||||
}
|
||||
if (parent) remove(instance.block, parent)
|
||||
// TODO queuePostFlushCb(() => {
|
||||
if (instance.um) invokeArrayFns(instance.um)
|
||||
if (instance.um) {
|
||||
queuePostFlushCb(() => invokeArrayFns(instance.um!))
|
||||
}
|
||||
instance.isUnmounted = true
|
||||
// })
|
||||
} else if (parent) {
|
||||
remove(instance.block, parent)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue