Merge remote-tracking branch 'github/master' into changing_unwrap_ref
# Conflicts: # packages/reactivity/src/ref.ts # packages/runtime-core/__tests__/apiTemplateRef.spec.ts # packages/runtime-core/src/apiWatch.ts
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renderer: portal should update children 1`] = `"<div>teleported</div>"`;
|
||||
|
||||
exports[`renderer: portal should update children 2`] = `""`;
|
||||
|
||||
exports[`renderer: portal should update children 3`] = `"teleported"`;
|
||||
|
||||
exports[`renderer: portal should update target 1`] = `"<!----><!--[object Object]--><div>root</div><!---->"`;
|
||||
|
||||
exports[`renderer: portal should update target 2`] = `"<div>teleported</div>"`;
|
||||
|
||||
exports[`renderer: portal should update target 3`] = `""`;
|
||||
|
||||
exports[`renderer: portal should update target 4`] = `"<!----><!--[object Object]--><div>root</div><!---->"`;
|
||||
|
||||
exports[`renderer: portal should update target 5`] = `""`;
|
||||
|
||||
exports[`renderer: portal should update target 6`] = `"<div>teleported</div>"`;
|
||||
|
||||
exports[`renderer: portal should work 1`] = `"<!----><!--[object Object]--><div>root</div><!---->"`;
|
||||
|
||||
exports[`renderer: portal should work 2`] = `"<div>teleported</div>"`;
|
||||
610
packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
Normal file
610
packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
Normal file
@@ -0,0 +1,610 @@
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
h,
|
||||
Component,
|
||||
ref,
|
||||
nextTick,
|
||||
Suspense
|
||||
} from '../src'
|
||||
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
|
||||
|
||||
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
|
||||
|
||||
describe('api: defineAsyncComponent', () => {
|
||||
test('simple usage', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent(
|
||||
() =>
|
||||
new Promise(r => {
|
||||
resolve = r as any
|
||||
})
|
||||
)
|
||||
|
||||
const toggle = ref(true)
|
||||
const root = nodeOps.createElement('div')
|
||||
createApp({
|
||||
render: () => (toggle.value ? h(Foo) : null)
|
||||
}).mount(root)
|
||||
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
resolve!(() => 'resolved')
|
||||
// first time resolve, wait for macro task since there are multiple
|
||||
// microtasks / .then() calls
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// already resolved component should update on nextTick
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('with loading component', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(r => {
|
||||
resolve = r as any
|
||||
}),
|
||||
loadingComponent: () => 'loading',
|
||||
delay: 1 // defaults to 200
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const root = nodeOps.createElement('div')
|
||||
createApp({
|
||||
render: () => (toggle.value ? h(Foo) : null)
|
||||
}).mount(root)
|
||||
|
||||
// due to the delay, initial mount should be empty
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// loading show up after delay
|
||||
await timeout(1)
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// already resolved component should update on nextTick without loading
|
||||
// state
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('with loading component + explicit delay (0)', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(r => {
|
||||
resolve = r as any
|
||||
}),
|
||||
loadingComponent: () => 'loading',
|
||||
delay: 0
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const root = nodeOps.createElement('div')
|
||||
createApp({
|
||||
render: () => (toggle.value ? h(Foo) : null)
|
||||
}).mount(root)
|
||||
|
||||
// with delay: 0, should show loading immediately
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// already resolved component should update on nextTick without loading
|
||||
// state
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('error without error component', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
let reject: (e: Error) => void
|
||||
const Foo = defineAsyncComponent(
|
||||
() =>
|
||||
new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
})
|
||||
)
|
||||
|
||||
const toggle = ref(true)
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => (toggle.value ? h(Foo) : null)
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
const err = new Error('foo')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0]).toBe(err)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// errored out on previous load, toggle and mock success this time
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('error with error component', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
let reject: (e: Error) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
}),
|
||||
errorComponent: (props: { error: Error }) => props.error.message
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => (toggle.value ? h(Foo) : null)
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
const err = new Error('errored out')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(serializeInner(root)).toBe('errored out')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// errored out on previous load, toggle and mock success this time
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('error with error + loading components', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
let reject: (e: Error) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
}),
|
||||
errorComponent: (props: { error: Error }) => props.error.message,
|
||||
loadingComponent: () => 'loading',
|
||||
delay: 1
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => (toggle.value ? h(Foo) : null)
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
|
||||
app.mount(root)
|
||||
|
||||
// due to the delay, initial mount should be empty
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// loading show up after delay
|
||||
await timeout(1)
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
const err = new Error('errored out')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(serializeInner(root)).toBe('errored out')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// errored out on previous load, toggle and mock success this time
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// loading show up after delay
|
||||
await timeout(1)
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('timeout without error component', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
timeout: 1
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => h(Foo)
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
await timeout(1)
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0].message).toMatch(
|
||||
`Async component timed out after 1ms.`
|
||||
)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// if it resolved after timeout, should still work
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('timeout with error component', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
timeout: 1,
|
||||
errorComponent: () => 'timed out'
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => h(Foo)
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
await timeout(1)
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(serializeInner(root)).toBe('timed out')
|
||||
|
||||
// if it resolved after timeout, should still work
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('timeout with error + loading components', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
delay: 1,
|
||||
timeout: 16,
|
||||
errorComponent: () => 'timed out',
|
||||
loadingComponent: () => 'loading'
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => h(Foo)
|
||||
})
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
await timeout(1)
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
await timeout(16)
|
||||
expect(serializeInner(root)).toBe('timed out')
|
||||
expect(handler).toHaveBeenCalled()
|
||||
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('timeout without error component, but with loading component', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
delay: 1,
|
||||
timeout: 16,
|
||||
loadingComponent: () => 'loading'
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => h(Foo)
|
||||
})
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
await timeout(1)
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
await timeout(16)
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0].message).toMatch(
|
||||
`Async component timed out after 16ms.`
|
||||
)
|
||||
// should still display loading
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('with suspense', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent(
|
||||
() =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
})
|
||||
)
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () =>
|
||||
h(Suspense, null, {
|
||||
default: () => [h(Foo), ' & ', h(Foo)],
|
||||
fallback: () => 'loading'
|
||||
})
|
||||
})
|
||||
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved & resolved')
|
||||
})
|
||||
|
||||
test('suspensible: false', async () => {
|
||||
let resolve: (comp: Component) => void
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
suspensible: false
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () =>
|
||||
h(Suspense, null, {
|
||||
default: () => [h(Foo), ' & ', h(Foo)],
|
||||
fallback: () => 'loading'
|
||||
})
|
||||
})
|
||||
|
||||
app.mount(root)
|
||||
// should not show suspense fallback
|
||||
expect(serializeInner(root)).toBe('<!----> & <!---->')
|
||||
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(serializeInner(root)).toBe('resolved & resolved')
|
||||
})
|
||||
|
||||
test('suspense with error handling', async () => {
|
||||
let reject: (e: Error) => void
|
||||
const Foo = defineAsyncComponent(
|
||||
() =>
|
||||
new Promise((_resolve, _reject) => {
|
||||
reject = _reject
|
||||
})
|
||||
)
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () =>
|
||||
h(Suspense, null, {
|
||||
default: () => [h(Foo), ' & ', h(Foo)],
|
||||
fallback: () => 'loading'
|
||||
})
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('loading')
|
||||
|
||||
reject!(new Error('no'))
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(serializeInner(root)).toBe('<!----> & <!---->')
|
||||
})
|
||||
|
||||
test('retry (success)', async () => {
|
||||
let loaderCallCount = 0
|
||||
let resolve: (comp: Component) => void
|
||||
let reject: (e: Error) => void
|
||||
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () => {
|
||||
loaderCallCount++
|
||||
return new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
})
|
||||
},
|
||||
onError(error, retry, fail) {
|
||||
if (error.message.match(/foo/)) {
|
||||
retry()
|
||||
} else {
|
||||
fail()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => h(Foo)
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
expect(loaderCallCount).toBe(1)
|
||||
|
||||
const err = new Error('foo')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
expect(loaderCallCount).toBe(2)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => 'resolved')
|
||||
await timeout()
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
expect(serializeInner(root)).toBe('resolved')
|
||||
})
|
||||
|
||||
test('retry (skipped)', async () => {
|
||||
let loaderCallCount = 0
|
||||
let reject: (e: Error) => void
|
||||
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () => {
|
||||
loaderCallCount++
|
||||
return new Promise((_resolve, _reject) => {
|
||||
reject = _reject
|
||||
})
|
||||
},
|
||||
onError(error, retry, fail) {
|
||||
if (error.message.match(/bar/)) {
|
||||
retry()
|
||||
} else {
|
||||
fail()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => h(Foo)
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
expect(loaderCallCount).toBe(1)
|
||||
|
||||
const err = new Error('foo')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
// should fail because retryWhen returns false
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0]).toBe(err)
|
||||
expect(loaderCallCount).toBe(1)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
})
|
||||
|
||||
test('retry (fail w/ max retry attempts)', async () => {
|
||||
let loaderCallCount = 0
|
||||
let reject: (e: Error) => void
|
||||
|
||||
const Foo = defineAsyncComponent({
|
||||
loader: () => {
|
||||
loaderCallCount++
|
||||
return new Promise((_resolve, _reject) => {
|
||||
reject = _reject
|
||||
})
|
||||
},
|
||||
onError(error, retry, fail, attempts) {
|
||||
if (error.message.match(/foo/) && attempts <= 1) {
|
||||
retry()
|
||||
} else {
|
||||
fail()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp({
|
||||
render: () => h(Foo)
|
||||
})
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn())
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
expect(loaderCallCount).toBe(1)
|
||||
|
||||
// first retry
|
||||
const err = new Error('foo')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
expect(loaderCallCount).toBe(2)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
|
||||
// 2nd retry, should fail due to reaching maxRetries
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0]).toBe(err)
|
||||
expect(loaderCallCount).toBe(2)
|
||||
expect(serializeInner(root)).toBe('<!---->')
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
h,
|
||||
nodeOps,
|
||||
serializeInner,
|
||||
mockWarn,
|
||||
provide,
|
||||
inject,
|
||||
resolveComponent,
|
||||
@@ -11,46 +10,64 @@ import {
|
||||
withDirectives,
|
||||
Plugin,
|
||||
ref,
|
||||
getCurrentInstance
|
||||
getCurrentInstance,
|
||||
defineComponent
|
||||
} from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('api: createApp', () => {
|
||||
mockWarn()
|
||||
|
||||
test('mount', () => {
|
||||
const Comp = {
|
||||
const Comp = defineComponent({
|
||||
props: {
|
||||
count: {
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
setup(props: { count: number }) {
|
||||
setup(props) {
|
||||
return () => props.count
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const root1 = nodeOps.createElement('div')
|
||||
createApp().mount(Comp, root1)
|
||||
createApp(Comp).mount(root1)
|
||||
expect(serializeInner(root1)).toBe(`0`)
|
||||
|
||||
// mount with props
|
||||
const root2 = nodeOps.createElement('div')
|
||||
const app2 = createApp()
|
||||
app2.mount(Comp, root2, { count: 1 })
|
||||
const app2 = createApp(Comp, { count: 1 })
|
||||
app2.mount(root2)
|
||||
expect(serializeInner(root2)).toBe(`1`)
|
||||
|
||||
// remount warning
|
||||
const root3 = nodeOps.createElement('div')
|
||||
app2.mount(Comp, root3)
|
||||
app2.mount(root3)
|
||||
expect(serializeInner(root3)).toBe(``)
|
||||
expect(`already been mounted`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('provide', () => {
|
||||
const app = createApp()
|
||||
app.provide('foo', 1)
|
||||
app.provide('bar', 2)
|
||||
test('unmount', () => {
|
||||
const Comp = defineComponent({
|
||||
props: {
|
||||
count: {
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return () => props.count
|
||||
}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
const app = createApp(Comp)
|
||||
app.mount(root)
|
||||
|
||||
app.unmount(root)
|
||||
expect(serializeInner(root)).toBe(``)
|
||||
})
|
||||
|
||||
test('provide', () => {
|
||||
const Root = {
|
||||
setup() {
|
||||
// test override
|
||||
@@ -63,29 +80,24 @@ describe('api: createApp', () => {
|
||||
setup() {
|
||||
const foo = inject('foo')
|
||||
const bar = inject('bar')
|
||||
try {
|
||||
inject('__proto__')
|
||||
} catch (e) {}
|
||||
return () => `${foo},${bar}`
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Root)
|
||||
app.provide('foo', 1)
|
||||
app.provide('bar', 2)
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(Root, root)
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe(`3,2`)
|
||||
expect('[Vue warn]: injection "__proto__" not found.').toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('component', () => {
|
||||
const app = createApp()
|
||||
|
||||
const FooBar = () => 'foobar!'
|
||||
app.component('FooBar', FooBar)
|
||||
expect(app.component('FooBar')).toBe(FooBar)
|
||||
|
||||
app.component('BarBaz', () => 'barbaz!')
|
||||
|
||||
app.component('BarBaz', () => 'barbaz!')
|
||||
expect(
|
||||
'Component "BarBaz" has already been registered in target app.'
|
||||
).toHaveBeenWarnedTimes(1)
|
||||
|
||||
const Root = {
|
||||
// local override
|
||||
components: {
|
||||
@@ -102,33 +114,29 @@ describe('api: createApp', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Root)
|
||||
|
||||
const FooBar = () => 'foobar!'
|
||||
app.component('FooBar', FooBar)
|
||||
expect(app.component('FooBar')).toBe(FooBar)
|
||||
|
||||
app.component('BarBaz', () => 'barbaz!')
|
||||
|
||||
app.component('BarBaz', () => 'barbaz!')
|
||||
expect(
|
||||
'Component "BarBaz" has already been registered in target app.'
|
||||
).toHaveBeenWarnedTimes(1)
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(Root, root)
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe(`<div>foobar!barbaz-local!</div>`)
|
||||
})
|
||||
|
||||
test('directive', () => {
|
||||
const app = createApp()
|
||||
|
||||
const spy1 = jest.fn()
|
||||
const spy2 = jest.fn()
|
||||
const spy3 = jest.fn()
|
||||
|
||||
const FooBar = { mounted: spy1 }
|
||||
app.directive('FooBar', FooBar)
|
||||
expect(app.directive('FooBar')).toBe(FooBar)
|
||||
|
||||
app.directive('BarBaz', {
|
||||
mounted: spy2
|
||||
})
|
||||
|
||||
app.directive('BarBaz', {
|
||||
mounted: spy2
|
||||
})
|
||||
expect(
|
||||
'Directive "BarBaz" has already been registered in target app.'
|
||||
).toHaveBeenWarnedTimes(1)
|
||||
|
||||
const Root = {
|
||||
// local override
|
||||
directives: {
|
||||
@@ -145,8 +153,25 @@ describe('api: createApp', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Root)
|
||||
|
||||
const FooBar = { mounted: spy1 }
|
||||
app.directive('FooBar', FooBar)
|
||||
expect(app.directive('FooBar')).toBe(FooBar)
|
||||
|
||||
app.directive('BarBaz', {
|
||||
mounted: spy2
|
||||
})
|
||||
|
||||
app.directive('BarBaz', {
|
||||
mounted: spy2
|
||||
})
|
||||
expect(
|
||||
'Directive "BarBaz" has already been registered in target app.'
|
||||
).toHaveBeenWarnedTimes(1)
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(Root, root)
|
||||
app.mount(root)
|
||||
expect(spy1).toHaveBeenCalled()
|
||||
expect(spy2).not.toHaveBeenCalled()
|
||||
expect(spy3).toHaveBeenCalled()
|
||||
@@ -212,7 +237,7 @@ describe('api: createApp', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp()
|
||||
const app = createApp(Comp)
|
||||
app.mixin(mixinA)
|
||||
app.mixin(mixinB)
|
||||
|
||||
@@ -226,7 +251,7 @@ describe('api: createApp', () => {
|
||||
).toHaveBeenWarnedTimes(1)
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(Comp, root)
|
||||
app.mount(root)
|
||||
|
||||
expect(serializeInner(root)).toBe(`123`)
|
||||
expect(calls).toEqual([
|
||||
@@ -242,13 +267,15 @@ describe('api: createApp', () => {
|
||||
test('use', () => {
|
||||
const PluginA: Plugin = app => app.provide('foo', 1)
|
||||
const PluginB: Plugin = {
|
||||
install: app => app.provide('bar', 2)
|
||||
install: (app, arg1, arg2) => app.provide('bar', arg1 + arg2)
|
||||
}
|
||||
const PluginC: any = undefined
|
||||
|
||||
const app = createApp()
|
||||
app.use(PluginA)
|
||||
app.use(PluginB)
|
||||
class PluginC {
|
||||
someProperty = {}
|
||||
static install() {
|
||||
app.provide('baz', 2)
|
||||
}
|
||||
}
|
||||
const PluginD: any = undefined
|
||||
|
||||
const Root = {
|
||||
setup() {
|
||||
@@ -257,8 +284,14 @@ describe('api: createApp', () => {
|
||||
return () => `${foo},${bar}`
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Root)
|
||||
app.use(PluginA)
|
||||
app.use(PluginB, 1, 1)
|
||||
app.use(PluginC)
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(Root, root)
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe(`1,2`)
|
||||
|
||||
app.use(PluginA)
|
||||
@@ -266,7 +299,7 @@ describe('api: createApp', () => {
|
||||
`Plugin has already been applied to target app`
|
||||
).toHaveBeenWarnedTimes(1)
|
||||
|
||||
app.use(PluginC)
|
||||
app.use(PluginD)
|
||||
expect(
|
||||
`A plugin must either be a function or an object with an "install" ` +
|
||||
`function.`
|
||||
@@ -274,18 +307,14 @@ describe('api: createApp', () => {
|
||||
})
|
||||
|
||||
test('config.errorHandler', () => {
|
||||
const app = createApp()
|
||||
|
||||
const error = new Error()
|
||||
const count = ref(0)
|
||||
|
||||
const handler = (app.config.errorHandler = jest.fn(
|
||||
(err, instance, info) => {
|
||||
expect(err).toBe(error)
|
||||
expect((instance as any).count).toBe(count.value)
|
||||
expect(info).toBe(`render function`)
|
||||
}
|
||||
))
|
||||
const handler = jest.fn((err, instance, info) => {
|
||||
expect(err).toBe(error)
|
||||
expect((instance as any).count).toBe(count.value)
|
||||
expect(info).toBe(`render function`)
|
||||
})
|
||||
|
||||
const Root = {
|
||||
setup() {
|
||||
@@ -299,21 +328,19 @@ describe('api: createApp', () => {
|
||||
}
|
||||
}
|
||||
|
||||
app.mount(Root, nodeOps.createElement('div'))
|
||||
const app = createApp(Root)
|
||||
app.config.errorHandler = handler
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
expect(handler).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('config.warnHandler', () => {
|
||||
const app = createApp()
|
||||
let ctx: any
|
||||
|
||||
const handler = (app.config.warnHandler = jest.fn(
|
||||
(msg, instance, trace) => {
|
||||
expect(msg).toMatch(`Component is missing template or render function`)
|
||||
expect(instance).toBe(ctx.proxy)
|
||||
expect(trace).toMatch(`Hello`)
|
||||
}
|
||||
))
|
||||
const handler = jest.fn((msg, instance, trace) => {
|
||||
expect(msg).toMatch(`Component is missing template or render function`)
|
||||
expect(instance).toBe(ctx.proxy)
|
||||
expect(trace).toMatch(`Hello`)
|
||||
})
|
||||
|
||||
const Root = {
|
||||
name: 'Hello',
|
||||
@@ -322,7 +349,9 @@ describe('api: createApp', () => {
|
||||
}
|
||||
}
|
||||
|
||||
app.mount(Root, nodeOps.createElement('div'))
|
||||
const app = createApp(Root)
|
||||
app.config.warnHandler = handler
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -330,107 +359,119 @@ describe('api: createApp', () => {
|
||||
const isNativeTag = jest.fn(tag => tag === 'div')
|
||||
|
||||
test('Component.name', () => {
|
||||
const app = createApp()
|
||||
Object.defineProperty(app.config, 'isNativeTag', {
|
||||
value: isNativeTag,
|
||||
writable: false
|
||||
})
|
||||
|
||||
const Root = {
|
||||
name: 'div',
|
||||
setup() {
|
||||
return {
|
||||
count: ref(0)
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
app.mount(Root, nodeOps.createElement('div'))
|
||||
const app = createApp(Root)
|
||||
|
||||
Object.defineProperty(app.config, 'isNativeTag', {
|
||||
value: isNativeTag,
|
||||
writable: false
|
||||
})
|
||||
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
expect(
|
||||
`Do not use built-in or reserved HTML elements as component id: div`
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('Component.components', () => {
|
||||
const app = createApp()
|
||||
Object.defineProperty(app.config, 'isNativeTag', {
|
||||
value: isNativeTag,
|
||||
writable: false
|
||||
})
|
||||
|
||||
const Root = {
|
||||
components: {
|
||||
div: () => 'div'
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
count: ref(0)
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
app.mount(Root, nodeOps.createElement('div'))
|
||||
const app = createApp(Root)
|
||||
Object.defineProperty(app.config, 'isNativeTag', {
|
||||
value: isNativeTag,
|
||||
writable: false
|
||||
})
|
||||
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
expect(
|
||||
`Do not use built-in or reserved HTML elements as component id: div`
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('Component.directives', () => {
|
||||
const app = createApp()
|
||||
Object.defineProperty(app.config, 'isNativeTag', {
|
||||
value: isNativeTag,
|
||||
writable: false
|
||||
})
|
||||
|
||||
const Root = {
|
||||
directives: {
|
||||
bind: () => {}
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
count: ref(0)
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
app.mount(Root, nodeOps.createElement('div'))
|
||||
const app = createApp(Root)
|
||||
Object.defineProperty(app.config, 'isNativeTag', {
|
||||
value: isNativeTag,
|
||||
writable: false
|
||||
})
|
||||
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
expect(
|
||||
`Do not use built-in directive ids as custom directive id: bind`
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('register using app.component', () => {
|
||||
const app = createApp()
|
||||
const app = createApp({
|
||||
render() {}
|
||||
})
|
||||
|
||||
Object.defineProperty(app.config, 'isNativeTag', {
|
||||
value: isNativeTag,
|
||||
writable: false
|
||||
})
|
||||
|
||||
const Root = {
|
||||
setup() {
|
||||
return {
|
||||
count: ref(0)
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
app.component('div', () => 'div')
|
||||
app.mount(Root, nodeOps.createElement('div'))
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
expect(
|
||||
`Do not use built-in or reserved HTML elements as component id: div`
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
||||
test('config.optionMergeStrategies', () => {
|
||||
let merged: string
|
||||
const App = defineComponent({
|
||||
render() {},
|
||||
mixins: [{ foo: 'mixin' }],
|
||||
extends: { foo: 'extends' },
|
||||
foo: 'local',
|
||||
beforeCreate() {
|
||||
merged = this.$options.foo
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.mixin({
|
||||
foo: 'global'
|
||||
})
|
||||
app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
|
||||
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
expect(merged!).toBe('global,extends,mixin,local')
|
||||
})
|
||||
|
||||
test('config.globalProperties', () => {
|
||||
const app = createApp({
|
||||
render() {
|
||||
return this.foo
|
||||
}
|
||||
})
|
||||
app.config.globalProperties.foo = 'hello'
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('hello')
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
readonly,
|
||||
reactive
|
||||
} from '../src/index'
|
||||
import { render, nodeOps, serialize, mockWarn } from '@vue/runtime-test'
|
||||
import { render, nodeOps, serialize } from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
// reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
|
||||
|
||||
@@ -283,4 +284,27 @@ describe('api: provide/inject', () => {
|
||||
expect(serialize(root)).toBe(`<div><!----></div>`)
|
||||
expect(`injection "foo" not found.`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('should not warn when default value is undefined', () => {
|
||||
const Provider = {
|
||||
setup() {
|
||||
return () => h(Middle)
|
||||
}
|
||||
}
|
||||
|
||||
const Middle = {
|
||||
render: () => h(Consumer)
|
||||
}
|
||||
|
||||
const Consumer = {
|
||||
setup() {
|
||||
const foo = inject('foo', undefined)
|
||||
return () => foo
|
||||
}
|
||||
}
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Provider), root)
|
||||
expect(`injection "foo" not found.`).not.toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
nextTick,
|
||||
renderToString,
|
||||
ref,
|
||||
createComponent,
|
||||
mockWarn
|
||||
defineComponent
|
||||
} from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('api: options', () => {
|
||||
test('data', async () => {
|
||||
const Comp = createComponent({
|
||||
const Comp = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
foo: 1
|
||||
@@ -42,7 +42,7 @@ describe('api: options', () => {
|
||||
})
|
||||
|
||||
test('computed', async () => {
|
||||
const Comp = createComponent({
|
||||
const Comp = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
foo: 1
|
||||
@@ -52,9 +52,7 @@ describe('api: options', () => {
|
||||
bar(): number {
|
||||
return this.foo + 1
|
||||
},
|
||||
baz(): number {
|
||||
return this.bar + 1
|
||||
}
|
||||
baz: (vm): number => vm.bar + 1
|
||||
},
|
||||
render() {
|
||||
return h(
|
||||
@@ -78,7 +76,7 @@ describe('api: options', () => {
|
||||
})
|
||||
|
||||
test('methods', async () => {
|
||||
const Comp = createComponent({
|
||||
const Comp = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
foo: 1
|
||||
@@ -149,30 +147,24 @@ describe('api: options', () => {
|
||||
|
||||
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
|
||||
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
|
||||
expect(spy).toHaveReturnedWith(ctx)
|
||||
}
|
||||
|
||||
assertCall(spyA, 0, [1, undefined])
|
||||
assertCall(spyB, 0, [2, undefined])
|
||||
assertCall(spyC, 0, [{ qux: 3 }, undefined])
|
||||
expect(spyA).toHaveReturnedWith(ctx)
|
||||
expect(spyB).toHaveReturnedWith(ctx)
|
||||
expect(spyC).toHaveReturnedWith(ctx)
|
||||
|
||||
ctx.foo++
|
||||
await nextTick()
|
||||
expect(spyA).toHaveBeenCalledTimes(2)
|
||||
assertCall(spyA, 1, [2, 1])
|
||||
expect(spyA).toHaveBeenCalledTimes(1)
|
||||
assertCall(spyA, 0, [2, 1])
|
||||
|
||||
ctx.bar++
|
||||
await nextTick()
|
||||
expect(spyB).toHaveBeenCalledTimes(2)
|
||||
assertCall(spyB, 1, [3, 2])
|
||||
expect(spyB).toHaveBeenCalledTimes(1)
|
||||
assertCall(spyB, 0, [3, 2])
|
||||
|
||||
ctx.baz.qux++
|
||||
await nextTick()
|
||||
expect(spyC).toHaveBeenCalledTimes(2)
|
||||
expect(spyC).toHaveBeenCalledTimes(1)
|
||||
// new and old objects have same identity
|
||||
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
|
||||
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
|
||||
})
|
||||
|
||||
test('watch array', async () => {
|
||||
@@ -218,30 +210,24 @@ describe('api: options', () => {
|
||||
|
||||
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
|
||||
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
|
||||
expect(spy).toHaveReturnedWith(ctx)
|
||||
}
|
||||
|
||||
assertCall(spyA, 0, [1, undefined])
|
||||
assertCall(spyB, 0, [2, undefined])
|
||||
assertCall(spyC, 0, [{ qux: 3 }, undefined])
|
||||
expect(spyA).toHaveReturnedWith(ctx)
|
||||
expect(spyB).toHaveReturnedWith(ctx)
|
||||
expect(spyC).toHaveReturnedWith(ctx)
|
||||
|
||||
ctx.foo++
|
||||
await nextTick()
|
||||
expect(spyA).toHaveBeenCalledTimes(2)
|
||||
assertCall(spyA, 1, [2, 1])
|
||||
expect(spyA).toHaveBeenCalledTimes(1)
|
||||
assertCall(spyA, 0, [2, 1])
|
||||
|
||||
ctx.bar++
|
||||
await nextTick()
|
||||
expect(spyB).toHaveBeenCalledTimes(2)
|
||||
assertCall(spyB, 1, [3, 2])
|
||||
expect(spyB).toHaveBeenCalledTimes(1)
|
||||
assertCall(spyB, 0, [3, 2])
|
||||
|
||||
ctx.baz.qux++
|
||||
await nextTick()
|
||||
expect(spyC).toHaveBeenCalledTimes(2)
|
||||
expect(spyC).toHaveBeenCalledTimes(1)
|
||||
// new and old objects have same identity
|
||||
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
|
||||
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
|
||||
})
|
||||
|
||||
test('provide/inject', () => {
|
||||
@@ -295,7 +281,7 @@ describe('api: options', () => {
|
||||
}
|
||||
} as any
|
||||
|
||||
expect(renderToString(h(Root))).toBe(`<!---->1112<!---->`)
|
||||
expect(renderToString(h(Root))).toBe(`1112`)
|
||||
})
|
||||
|
||||
test('lifecycle', async () => {
|
||||
@@ -536,7 +522,7 @@ describe('api: options', () => {
|
||||
})
|
||||
|
||||
test('accessing setup() state from options', async () => {
|
||||
const Comp = createComponent({
|
||||
const Comp = defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
count: ref(0)
|
||||
@@ -648,9 +634,9 @@ describe('api: options', () => {
|
||||
test('data property is already declared in props', () => {
|
||||
const Comp = {
|
||||
props: { foo: Number },
|
||||
data: {
|
||||
data: () => ({
|
||||
foo: 1
|
||||
},
|
||||
}),
|
||||
render() {}
|
||||
}
|
||||
|
||||
@@ -663,9 +649,9 @@ describe('api: options', () => {
|
||||
|
||||
test('computed property is already declared in data', () => {
|
||||
const Comp = {
|
||||
data: {
|
||||
data: () => ({
|
||||
foo: 1
|
||||
},
|
||||
}),
|
||||
computed: {
|
||||
foo() {}
|
||||
},
|
||||
@@ -713,9 +699,9 @@ describe('api: options', () => {
|
||||
|
||||
test('methods property is already declared in data', () => {
|
||||
const Comp = {
|
||||
data: {
|
||||
data: () => ({
|
||||
foo: 2
|
||||
},
|
||||
}),
|
||||
methods: {
|
||||
foo() {}
|
||||
},
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
render,
|
||||
serializeInner,
|
||||
nextTick,
|
||||
watch,
|
||||
createComponent,
|
||||
watchEffect,
|
||||
defineComponent,
|
||||
triggerEvent,
|
||||
TestElement
|
||||
} from '@vue/runtime-test'
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
describe('api: setup context', () => {
|
||||
it('should expose return values to template render context', () => {
|
||||
const Comp = createComponent({
|
||||
const Comp = defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
// ref should auto-unwrap
|
||||
@@ -53,9 +53,10 @@ describe('api: setup context', () => {
|
||||
render: () => h(Child, { count: count.value })
|
||||
}
|
||||
|
||||
const Child = createComponent({
|
||||
setup(props: { count: number }) {
|
||||
watch(() => {
|
||||
const Child = defineComponent({
|
||||
props: { count: Number },
|
||||
setup(props) {
|
||||
watchEffect(() => {
|
||||
dummy = props.count
|
||||
})
|
||||
return () => h('div', props.count)
|
||||
@@ -82,13 +83,13 @@ describe('api: setup context', () => {
|
||||
render: () => h(Child, { count: count.value })
|
||||
}
|
||||
|
||||
const Child = createComponent({
|
||||
const Child = defineComponent({
|
||||
props: {
|
||||
count: Number
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
watch(() => {
|
||||
watchEffect(() => {
|
||||
dummy = props.count
|
||||
})
|
||||
return () => h('div', props.count)
|
||||
@@ -119,7 +120,6 @@ describe('api: setup context', () => {
|
||||
// puts everything received in attrs
|
||||
// disable implicit fallthrough
|
||||
inheritAttrs: false,
|
||||
props: {},
|
||||
setup(props: any, { attrs }: any) {
|
||||
return () => h('div', attrs)
|
||||
}
|
||||
@@ -177,7 +177,7 @@ describe('api: setup context', () => {
|
||||
})
|
||||
}
|
||||
|
||||
const Child = createComponent({
|
||||
const Child = defineComponent({
|
||||
props: {
|
||||
count: {
|
||||
type: Number,
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
h,
|
||||
render,
|
||||
nextTick,
|
||||
createComponent
|
||||
Ref,
|
||||
defineComponent,
|
||||
reactive
|
||||
} from '@vue/runtime-test'
|
||||
|
||||
// reference: https://vue-composition-api-rfc.netlify.com/api.html#template-refs
|
||||
@@ -82,7 +84,7 @@ describe('api: template refs', () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
const fn = jest.fn()
|
||||
|
||||
const Comp = createComponent(() => () => h('div', { ref: fn }))
|
||||
const Comp = defineComponent(() => () => h('div', { ref: fn }))
|
||||
render(h(Comp), root)
|
||||
expect(fn.mock.calls[0][0]).toBe(root.children[0])
|
||||
})
|
||||
@@ -93,7 +95,7 @@ describe('api: template refs', () => {
|
||||
const fn2 = jest.fn()
|
||||
const fn = ref(fn1)
|
||||
|
||||
const Comp = createComponent(() => () => h('div', { ref: fn.value }))
|
||||
const Comp = defineComponent(() => () => h('div', { ref: fn.value }))
|
||||
|
||||
render(h(Comp), root)
|
||||
expect(fn1.mock.calls).toHaveLength(1)
|
||||
@@ -112,7 +114,7 @@ describe('api: template refs', () => {
|
||||
const fn = jest.fn()
|
||||
const toggle = ref(true)
|
||||
|
||||
const Comp = createComponent(() => () =>
|
||||
const Comp = defineComponent(() => () =>
|
||||
toggle.value ? h('div', { ref: fn }) : null
|
||||
)
|
||||
render(h(Comp), root)
|
||||
@@ -175,4 +177,46 @@ describe('api: template refs', () => {
|
||||
await nextTick()
|
||||
expect(el.value).toBe(null)
|
||||
})
|
||||
|
||||
test('string ref inside slots', async () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
const spy = jest.fn()
|
||||
const Child = {
|
||||
render(this: any) {
|
||||
return this.$slots.default()
|
||||
}
|
||||
}
|
||||
|
||||
const Comp = {
|
||||
render() {
|
||||
return h(Child, () => {
|
||||
return h('div', { ref: 'foo' })
|
||||
})
|
||||
},
|
||||
mounted(this: any) {
|
||||
spy(this.$refs.foo.tag)
|
||||
}
|
||||
}
|
||||
render(h(Comp), root)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('div')
|
||||
})
|
||||
|
||||
it('should work with direct reactive property', () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
const state = reactive({
|
||||
refKey: null
|
||||
})
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
return state
|
||||
},
|
||||
render() {
|
||||
return h('div', { ref: 'refKey' })
|
||||
}
|
||||
}
|
||||
render(h(Comp), root)
|
||||
expect(state.refKey).toBe(root.children[0])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { watch, reactive, computed, nextTick, ref, h } from '../src/index'
|
||||
import {
|
||||
watch,
|
||||
watchEffect,
|
||||
reactive,
|
||||
computed,
|
||||
nextTick,
|
||||
ref,
|
||||
h
|
||||
} from '../src/index'
|
||||
import { render, nodeOps, serializeInner } from '@vue/runtime-test'
|
||||
import {
|
||||
ITERATE_KEY,
|
||||
@@ -6,17 +14,19 @@ import {
|
||||
TrackOpTypes,
|
||||
TriggerOpTypes
|
||||
} from '@vue/reactivity'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
// reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
|
||||
|
||||
describe('api: watch', () => {
|
||||
it('basic usage', async () => {
|
||||
mockWarn()
|
||||
|
||||
it('effect', async () => {
|
||||
const state = reactive({ count: 0 })
|
||||
let dummy
|
||||
watch(() => {
|
||||
watchEffect(() => {
|
||||
dummy = state.count
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toBe(0)
|
||||
|
||||
state.count++
|
||||
@@ -33,12 +43,11 @@ describe('api: watch', () => {
|
||||
dummy = [count, prevCount]
|
||||
// assert types
|
||||
count + 1
|
||||
prevCount + 1
|
||||
if (prevCount) {
|
||||
prevCount + 1
|
||||
}
|
||||
}
|
||||
)
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([0, undefined])
|
||||
|
||||
state.count++
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([1, 0])
|
||||
@@ -51,11 +60,10 @@ describe('api: watch', () => {
|
||||
dummy = [count, prevCount]
|
||||
// assert types
|
||||
count + 1
|
||||
prevCount + 1
|
||||
if (prevCount) {
|
||||
prevCount + 1
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([0, undefined])
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([1, 0])
|
||||
@@ -69,11 +77,10 @@ describe('api: watch', () => {
|
||||
dummy = [count, prevCount]
|
||||
// assert types
|
||||
count + 1
|
||||
prevCount + 1
|
||||
if (prevCount) {
|
||||
prevCount + 1
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([1, undefined])
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([2, 1])
|
||||
@@ -91,8 +98,6 @@ describe('api: watch', () => {
|
||||
vals.concat(1)
|
||||
oldVals.concat(1)
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([[1, 1, 2], []])
|
||||
|
||||
state.count++
|
||||
count.value++
|
||||
@@ -107,28 +112,25 @@ describe('api: watch', () => {
|
||||
let dummy
|
||||
watch([() => state.count, status] as const, (vals, oldVals) => {
|
||||
dummy = [vals, oldVals]
|
||||
let [count] = vals
|
||||
let [, oldStatus] = oldVals
|
||||
const [count] = vals
|
||||
const [, oldStatus] = oldVals
|
||||
// assert types
|
||||
count + 1
|
||||
oldStatus === true
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([[1, false], []])
|
||||
|
||||
state.count++
|
||||
status.value = false
|
||||
status.value = true
|
||||
await nextTick()
|
||||
expect(dummy).toMatchObject([[2, false], [1, false]])
|
||||
expect(dummy).toMatchObject([[2, true], [1, false]])
|
||||
})
|
||||
|
||||
it('stopping the watcher', async () => {
|
||||
it('stopping the watcher (effect)', async () => {
|
||||
const state = reactive({ count: 0 })
|
||||
let dummy
|
||||
const stop = watch(() => {
|
||||
const stop = watchEffect(() => {
|
||||
dummy = state.count
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toBe(0)
|
||||
|
||||
stop()
|
||||
@@ -138,15 +140,35 @@ describe('api: watch', () => {
|
||||
expect(dummy).toBe(0)
|
||||
})
|
||||
|
||||
it('cleanup registration (basic)', async () => {
|
||||
it('stopping the watcher (with source)', async () => {
|
||||
const state = reactive({ count: 0 })
|
||||
let dummy
|
||||
const stop = watch(
|
||||
() => state.count,
|
||||
count => {
|
||||
dummy = count
|
||||
}
|
||||
)
|
||||
|
||||
state.count++
|
||||
await nextTick()
|
||||
expect(dummy).toBe(1)
|
||||
|
||||
stop()
|
||||
state.count++
|
||||
await nextTick()
|
||||
// should not update
|
||||
expect(dummy).toBe(1)
|
||||
})
|
||||
|
||||
it('cleanup registration (effect)', async () => {
|
||||
const state = reactive({ count: 0 })
|
||||
const cleanup = jest.fn()
|
||||
let dummy
|
||||
const stop = watch(onCleanup => {
|
||||
const stop = watchEffect(onCleanup => {
|
||||
onCleanup(cleanup)
|
||||
dummy = state.count
|
||||
})
|
||||
await nextTick()
|
||||
expect(dummy).toBe(0)
|
||||
|
||||
state.count++
|
||||
@@ -166,27 +188,35 @@ describe('api: watch', () => {
|
||||
onCleanup(cleanup)
|
||||
dummy = count
|
||||
})
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(dummy).toBe(0)
|
||||
expect(cleanup).toHaveBeenCalledTimes(0)
|
||||
expect(dummy).toBe(1)
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(cleanup).toHaveBeenCalledTimes(1)
|
||||
expect(dummy).toBe(1)
|
||||
expect(dummy).toBe(2)
|
||||
|
||||
stop()
|
||||
expect(cleanup).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('flush timing: post', async () => {
|
||||
it('flush timing: post (default)', async () => {
|
||||
const count = ref(0)
|
||||
let callCount = 0
|
||||
const assertion = jest.fn(count => {
|
||||
expect(serializeInner(root)).toBe(`${count}`)
|
||||
callCount++
|
||||
// on mount, the watcher callback should be called before DOM render
|
||||
// on update, should be called after the count is updated
|
||||
const expectedDOM = callCount === 1 ? `` : `${count}`
|
||||
expect(serializeInner(root)).toBe(expectedDOM)
|
||||
})
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
watch(() => {
|
||||
watchEffect(() => {
|
||||
assertion(count.value)
|
||||
})
|
||||
return () => count.value
|
||||
@@ -194,7 +224,6 @@ describe('api: watch', () => {
|
||||
}
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
await nextTick()
|
||||
expect(assertion).toHaveBeenCalledTimes(1)
|
||||
|
||||
count.value++
|
||||
@@ -221,7 +250,7 @@ describe('api: watch', () => {
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
watch(
|
||||
watchEffect(
|
||||
() => {
|
||||
assertion(count.value, count2.value)
|
||||
},
|
||||
@@ -234,7 +263,6 @@ describe('api: watch', () => {
|
||||
}
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
await nextTick()
|
||||
expect(assertion).toHaveBeenCalledTimes(1)
|
||||
|
||||
count.value++
|
||||
@@ -264,7 +292,7 @@ describe('api: watch', () => {
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
watch(
|
||||
watchEffect(
|
||||
() => {
|
||||
assertion(count.value)
|
||||
},
|
||||
@@ -277,7 +305,6 @@ describe('api: watch', () => {
|
||||
}
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
await nextTick()
|
||||
expect(assertion).toHaveBeenCalledTimes(1)
|
||||
|
||||
count.value++
|
||||
@@ -310,9 +337,6 @@ describe('api: watch', () => {
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
expect(dummy).toEqual([0, 1, 1, true])
|
||||
|
||||
state.nested.count++
|
||||
await nextTick()
|
||||
expect(dummy).toEqual([1, 1, 1, true])
|
||||
@@ -333,15 +357,75 @@ describe('api: watch', () => {
|
||||
expect(dummy).toEqual([1, 2, 2, false])
|
||||
})
|
||||
|
||||
it('lazy', async () => {
|
||||
it('immediate', async () => {
|
||||
const count = ref(0)
|
||||
const cb = jest.fn()
|
||||
watch(count, cb, { lazy: true })
|
||||
await nextTick()
|
||||
expect(cb).not.toHaveBeenCalled()
|
||||
watch(count, cb, { immediate: true })
|
||||
expect(cb).toHaveBeenCalledTimes(1)
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(cb).toHaveBeenCalled()
|
||||
expect(cb).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('immediate: triggers when initial value is null', async () => {
|
||||
const state = ref(null)
|
||||
const spy = jest.fn()
|
||||
watch(() => state.value, spy, { immediate: true })
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('immediate: triggers when initial value is undefined', async () => {
|
||||
const state = ref()
|
||||
const spy = jest.fn()
|
||||
watch(() => state.value, spy, { immediate: true })
|
||||
expect(spy).toHaveBeenCalled()
|
||||
state.value = 3
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
// testing if undefined can trigger the watcher
|
||||
state.value = undefined
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(3)
|
||||
// it shouldn't trigger if the same value is set
|
||||
state.value = undefined
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('warn immediate option when using effect', async () => {
|
||||
const count = ref(0)
|
||||
let dummy
|
||||
watchEffect(
|
||||
() => {
|
||||
dummy = count.value
|
||||
},
|
||||
// @ts-ignore
|
||||
{ immediate: false }
|
||||
)
|
||||
expect(dummy).toBe(0)
|
||||
expect(`"immediate" option is only respected`).toHaveBeenWarned()
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(dummy).toBe(1)
|
||||
})
|
||||
|
||||
it('warn and not respect deep option when using effect', async () => {
|
||||
const arr = ref([1, [2]])
|
||||
const spy = jest.fn()
|
||||
watchEffect(
|
||||
() => {
|
||||
spy()
|
||||
return arr
|
||||
},
|
||||
// @ts-ignore
|
||||
{ deep: true }
|
||||
)
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
;(arr.value[1] as Array<number>)[0] = 3
|
||||
await nextTick()
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(`"deep" option is only respected`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
it('onTrack', async () => {
|
||||
@@ -351,7 +435,7 @@ describe('api: watch', () => {
|
||||
events.push(e)
|
||||
})
|
||||
const obj = reactive({ foo: 1, bar: 2 })
|
||||
watch(
|
||||
watchEffect(
|
||||
() => {
|
||||
dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
|
||||
},
|
||||
@@ -386,7 +470,7 @@ describe('api: watch', () => {
|
||||
events.push(e)
|
||||
})
|
||||
const obj = reactive({ foo: 1 })
|
||||
watch(
|
||||
watchEffect(
|
||||
() => {
|
||||
dummy = obj.foo
|
||||
},
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { h, ref, render, nodeOps, nextTick } from '@vue/runtime-test'
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
render,
|
||||
nodeOps,
|
||||
nextTick,
|
||||
defineComponent
|
||||
} from '@vue/runtime-test'
|
||||
|
||||
describe('renderer: component', () => {
|
||||
test.todo('should work')
|
||||
@@ -7,7 +14,34 @@ describe('renderer: component', () => {
|
||||
|
||||
test.todo('componentProxy')
|
||||
|
||||
test.todo('componentProps')
|
||||
describe('componentProps', () => {
|
||||
test.todo('should work')
|
||||
|
||||
test('should convert empty booleans to true', () => {
|
||||
let b1: any, b2: any, b3: any
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: {
|
||||
b1: Boolean,
|
||||
b2: [Boolean, String],
|
||||
b3: [String, Boolean]
|
||||
},
|
||||
setup(props) {
|
||||
;({ b1, b2, b3 } = props)
|
||||
return () => ''
|
||||
}
|
||||
})
|
||||
|
||||
render(
|
||||
h(Comp, <any>{ b1: '', b2: '', b3: '' }),
|
||||
nodeOps.createElement('div')
|
||||
)
|
||||
|
||||
expect(b1).toBe(true)
|
||||
expect(b2).toBe(true)
|
||||
expect(b3).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('slots', () => {
|
||||
test('should respect $stable flag', async () => {
|
||||
@@ -52,4 +86,61 @@ describe('renderer: component', () => {
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
test('emit', async () => {
|
||||
let noMatchEmitResult: any
|
||||
let singleEmitResult: any
|
||||
let multiEmitResult: any
|
||||
|
||||
const Child = defineComponent({
|
||||
setup(_, { emit }) {
|
||||
noMatchEmitResult = emit('foo')
|
||||
singleEmitResult = emit('bar')
|
||||
multiEmitResult = emit('baz')
|
||||
return () => h('div')
|
||||
}
|
||||
})
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
return () =>
|
||||
h(Child, {
|
||||
// emit triggering single handler
|
||||
onBar: () => 1,
|
||||
// emit triggering multiple handlers
|
||||
onBaz: [() => Promise.resolve(2), () => Promise.resolve(3)]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render(h(App), nodeOps.createElement('div'))
|
||||
|
||||
// assert return values from emit
|
||||
expect(noMatchEmitResult).toMatchObject([])
|
||||
expect(singleEmitResult).toMatchObject([1])
|
||||
expect(await Promise.all(multiEmitResult)).toMatchObject([2, 3])
|
||||
})
|
||||
|
||||
// for v-model:foo-bar usage in DOM templates
|
||||
test('emit update:xxx events should trigger kebab-case equivalent', () => {
|
||||
const Child = defineComponent({
|
||||
setup(_, { emit }) {
|
||||
emit('update:fooBar', 1)
|
||||
return () => h('div')
|
||||
}
|
||||
})
|
||||
|
||||
const handler = jest.fn()
|
||||
const App = {
|
||||
setup() {
|
||||
return () =>
|
||||
h(Child, {
|
||||
'onUpdate:foo-bar': handler
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render(h(App), nodeOps.createElement('div'))
|
||||
expect(handler).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
106
packages/runtime-core/__tests__/componentEmits.spec.ts
Normal file
106
packages/runtime-core/__tests__/componentEmits.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// Note: emits and listener fallthrough is tested in
|
||||
// ./rendererAttrsFallthrough.spec.ts.
|
||||
|
||||
import { mockWarn } from '@vue/shared'
|
||||
import { render, defineComponent, h, nodeOps } from '@vue/runtime-test'
|
||||
import { isEmitListener } from '../src/componentEmits'
|
||||
|
||||
describe('emits option', () => {
|
||||
mockWarn()
|
||||
|
||||
test('trigger both raw event and capitalize handlers', () => {
|
||||
const Foo = defineComponent({
|
||||
render() {},
|
||||
created() {
|
||||
// the `emit` function is bound on component instances
|
||||
this.$emit('foo')
|
||||
this.$emit('bar')
|
||||
}
|
||||
})
|
||||
|
||||
const onfoo = jest.fn()
|
||||
const onBar = jest.fn()
|
||||
const Comp = () => h(Foo, { onfoo, onBar })
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
|
||||
expect(onfoo).toHaveBeenCalled()
|
||||
expect(onBar).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('trigger hyphendated events for update:xxx events', () => {
|
||||
const Foo = defineComponent({
|
||||
render() {},
|
||||
created() {
|
||||
this.$emit('update:fooProp')
|
||||
this.$emit('update:barProp')
|
||||
}
|
||||
})
|
||||
|
||||
const fooSpy = jest.fn()
|
||||
const barSpy = jest.fn()
|
||||
const Comp = () =>
|
||||
h(Foo, {
|
||||
'onUpdate:fooProp': fooSpy,
|
||||
'onUpdate:bar-prop': barSpy
|
||||
})
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
|
||||
expect(fooSpy).toHaveBeenCalled()
|
||||
expect(barSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('warning for undeclared event (array)', () => {
|
||||
const Foo = defineComponent({
|
||||
emits: ['foo'],
|
||||
render() {},
|
||||
created() {
|
||||
// @ts-ignore
|
||||
this.$emit('bar')
|
||||
}
|
||||
})
|
||||
render(h(Foo), nodeOps.createElement('div'))
|
||||
expect(
|
||||
`Component emitted event "bar" but it is not declared`
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('warning for undeclared event (object)', () => {
|
||||
const Foo = defineComponent({
|
||||
emits: {
|
||||
foo: null
|
||||
},
|
||||
render() {},
|
||||
created() {
|
||||
// @ts-ignore
|
||||
this.$emit('bar')
|
||||
}
|
||||
})
|
||||
render(h(Foo), nodeOps.createElement('div'))
|
||||
expect(
|
||||
`Component emitted event "bar" but it is not declared`
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('validator warning', () => {
|
||||
const Foo = defineComponent({
|
||||
emits: {
|
||||
foo: (arg: number) => arg > 0
|
||||
},
|
||||
render() {},
|
||||
created() {
|
||||
this.$emit('foo', -1)
|
||||
}
|
||||
})
|
||||
render(h(Foo), nodeOps.createElement('div'))
|
||||
expect(`event validation failed for event "foo"`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('isEmitListener', () => {
|
||||
expect(isEmitListener(['click'], 'onClick')).toBe(true)
|
||||
expect(isEmitListener(['click'], 'onclick')).toBe(true)
|
||||
expect(isEmitListener({ click: null }, 'onClick')).toBe(true)
|
||||
expect(isEmitListener({ click: null }, 'onclick')).toBe(true)
|
||||
expect(isEmitListener(['click'], 'onBlick')).toBe(false)
|
||||
expect(isEmitListener({ click: null }, 'onBlick')).toBe(false)
|
||||
})
|
||||
})
|
||||
232
packages/runtime-core/__tests__/componentProps.spec.ts
Normal file
232
packages/runtime-core/__tests__/componentProps.spec.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
getCurrentInstance,
|
||||
render,
|
||||
h,
|
||||
nodeOps,
|
||||
FunctionalComponent,
|
||||
defineComponent,
|
||||
ref
|
||||
} from '@vue/runtime-test'
|
||||
import { render as domRender, nextTick } from 'vue'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('component props', () => {
|
||||
mockWarn()
|
||||
|
||||
test('stateful', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
let proxy: any
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: ['foo'],
|
||||
render() {
|
||||
props = this.$props
|
||||
attrs = this.$attrs
|
||||
proxy = this
|
||||
}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp, { foo: 1, bar: 2 }), root)
|
||||
expect(proxy.foo).toBe(1)
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
|
||||
expect(proxy.foo).toBe(2)
|
||||
expect(props).toEqual({ foo: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render(h(Comp, { qux: 5 }), root)
|
||||
expect(proxy.foo).toBeUndefined()
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
test('stateful with setup', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: ['foo'],
|
||||
setup(_props, { attrs: _attrs }) {
|
||||
return () => {
|
||||
props = _props
|
||||
attrs = _attrs
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp, { foo: 1, bar: 2 }), root)
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
|
||||
expect(props).toEqual({ foo: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render(h(Comp, { qux: 5 }), root)
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
test('functional with declaration', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
|
||||
const Comp: FunctionalComponent = (_props, { attrs: _attrs }) => {
|
||||
props = _props
|
||||
attrs = _attrs
|
||||
}
|
||||
Comp.props = ['foo']
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp, { foo: 1, bar: 2 }), root)
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
|
||||
expect(props).toEqual({ foo: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render(h(Comp, { qux: 5 }), root)
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
test('functional without declaration', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
const Comp: FunctionalComponent = (_props, { attrs: _attrs }) => {
|
||||
props = _props
|
||||
attrs = _attrs
|
||||
}
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
render(h(Comp, { foo: 1 }), root)
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ foo: 1 })
|
||||
expect(props).toBe(attrs)
|
||||
|
||||
render(h(Comp, { bar: 2 }), root)
|
||||
expect(props).toEqual({ bar: 2 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
expect(props).toBe(attrs)
|
||||
})
|
||||
|
||||
test('boolean casting', () => {
|
||||
let proxy: any
|
||||
const Comp = {
|
||||
props: {
|
||||
foo: Boolean,
|
||||
bar: Boolean,
|
||||
baz: Boolean,
|
||||
qux: Boolean
|
||||
},
|
||||
render() {
|
||||
proxy = this
|
||||
}
|
||||
}
|
||||
render(
|
||||
h(Comp, {
|
||||
// absent should cast to false
|
||||
bar: '', // empty string should cast to true
|
||||
baz: 'baz', // same string should cast to true
|
||||
qux: 'ok' // other values should be left in-tact (but raise warning)
|
||||
}),
|
||||
nodeOps.createElement('div')
|
||||
)
|
||||
|
||||
expect(proxy.foo).toBe(false)
|
||||
expect(proxy.bar).toBe(true)
|
||||
expect(proxy.baz).toBe(true)
|
||||
expect(proxy.qux).toBe('ok')
|
||||
expect('type check failed for prop "qux"').toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('default value', () => {
|
||||
let proxy: any
|
||||
const Comp = {
|
||||
props: {
|
||||
foo: {
|
||||
default: 1
|
||||
},
|
||||
bar: {
|
||||
default: () => ({ a: 1 })
|
||||
}
|
||||
},
|
||||
render() {
|
||||
proxy = this
|
||||
}
|
||||
}
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp, { foo: 2 }), root)
|
||||
expect(proxy.foo).toBe(2)
|
||||
expect(proxy.bar).toEqual({ a: 1 })
|
||||
|
||||
render(h(Comp, { foo: undefined, bar: { b: 2 } }), root)
|
||||
expect(proxy.foo).toBe(1)
|
||||
expect(proxy.bar).toEqual({ b: 2 })
|
||||
})
|
||||
|
||||
test('optimized props updates', async () => {
|
||||
const Child = defineComponent({
|
||||
props: ['foo'],
|
||||
template: `<div>{{ foo }}</div>`
|
||||
})
|
||||
|
||||
const foo = ref(1)
|
||||
const id = ref('a')
|
||||
|
||||
const Comp = defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
foo,
|
||||
id
|
||||
}
|
||||
},
|
||||
components: { Child },
|
||||
template: `<Child :foo="foo" :id="id"/>`
|
||||
})
|
||||
|
||||
// Note this one is using the main Vue render so it can compile template
|
||||
// on the fly
|
||||
const root = document.createElement('div')
|
||||
domRender(h(Comp), root)
|
||||
expect(root.innerHTML).toBe('<div id="a">1</div>')
|
||||
|
||||
foo.value++
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<div id="a">2</div>')
|
||||
|
||||
id.value = 'b'
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<div id="b">2</div>')
|
||||
})
|
||||
|
||||
test('warn props mutation', () => {
|
||||
let instance: ComponentInternalInstance
|
||||
let setupProps: any
|
||||
const Comp = {
|
||||
props: ['foo'],
|
||||
setup(props: any) {
|
||||
instance = getCurrentInstance()!
|
||||
setupProps = props
|
||||
return () => null
|
||||
}
|
||||
}
|
||||
render(h(Comp, { foo: 1 }), nodeOps.createElement('div'))
|
||||
expect(setupProps.foo).toBe(1)
|
||||
expect(instance!.props.foo).toBe(1)
|
||||
setupProps.foo = 2
|
||||
expect(`Set operation on key "foo" failed`).toHaveBeenWarned()
|
||||
expect(() => {
|
||||
;(instance!.proxy as any).foo = 2
|
||||
}).toThrow(TypeError)
|
||||
expect(`Attempting to mutate prop "foo"`).toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
@@ -1,16 +1,17 @@
|
||||
import {
|
||||
createApp,
|
||||
h,
|
||||
render,
|
||||
getCurrentInstance,
|
||||
nodeOps,
|
||||
mockWarn
|
||||
createApp
|
||||
} from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
import { ComponentInternalInstance } from '../src/component'
|
||||
|
||||
describe('component: proxy', () => {
|
||||
mockWarn()
|
||||
|
||||
test('data', () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
const Comp = {
|
||||
@@ -27,14 +28,13 @@ describe('component: proxy', () => {
|
||||
return null
|
||||
}
|
||||
}
|
||||
app.mount(Comp, nodeOps.createElement('div'))
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
expect(instanceProxy.foo).toBe(1)
|
||||
instanceProxy.foo = 2
|
||||
expect(instance!.data.foo).toBe(2)
|
||||
})
|
||||
|
||||
test('renderContext', () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
const Comp = {
|
||||
@@ -51,40 +51,27 @@ describe('component: proxy', () => {
|
||||
return null
|
||||
}
|
||||
}
|
||||
app.mount(Comp, nodeOps.createElement('div'))
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
expect(instanceProxy.foo).toBe(1)
|
||||
instanceProxy.foo = 2
|
||||
expect(instance!.renderContext.foo).toBe(2)
|
||||
})
|
||||
|
||||
test('propsProxy', () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
test('should not expose non-declared props', () => {
|
||||
let instanceProxy: any
|
||||
const Comp = {
|
||||
props: {
|
||||
foo: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return () => null
|
||||
},
|
||||
mounted() {
|
||||
instance = getCurrentInstance()!
|
||||
instanceProxy = this
|
||||
}
|
||||
}
|
||||
app.mount(Comp, nodeOps.createElement('div'))
|
||||
expect(instanceProxy.foo).toBe(1)
|
||||
expect(instance!.propsProxy!.foo).toBe(1)
|
||||
expect(() => (instanceProxy.foo = 2)).toThrow(TypeError)
|
||||
expect(`Attempting to mutate prop "foo"`).toHaveBeenWarned()
|
||||
render(h(Comp, { count: 1 }), nodeOps.createElement('div'))
|
||||
expect('count' in instanceProxy).toBe(false)
|
||||
})
|
||||
|
||||
test('public properties', () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
const Comp = {
|
||||
@@ -96,14 +83,16 @@ describe('component: proxy', () => {
|
||||
instanceProxy = this
|
||||
}
|
||||
}
|
||||
app.mount(Comp, nodeOps.createElement('div'))
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
expect(instanceProxy.$data).toBe(instance!.data)
|
||||
expect(instanceProxy.$props).toBe(instance!.propsProxy)
|
||||
expect(instanceProxy.$props).toBe(instance!.props)
|
||||
expect(instanceProxy.$attrs).toBe(instance!.attrs)
|
||||
expect(instanceProxy.$slots).toBe(instance!.slots)
|
||||
expect(instanceProxy.$refs).toBe(instance!.refs)
|
||||
expect(instanceProxy.$parent).toBe(instance!.parent)
|
||||
expect(instanceProxy.$root).toBe(instance!.root)
|
||||
expect(instanceProxy.$parent).toBe(
|
||||
instance!.parent && instance!.parent.proxy
|
||||
)
|
||||
expect(instanceProxy.$root).toBe(instance!.root.proxy)
|
||||
expect(instanceProxy.$emit).toBe(instance!.emit)
|
||||
expect(instanceProxy.$el).toBe(instance!.vnode.el)
|
||||
expect(instanceProxy.$options).toBe(instance!.type)
|
||||
@@ -112,7 +101,6 @@ describe('component: proxy', () => {
|
||||
})
|
||||
|
||||
test('sink', async () => {
|
||||
const app = createApp()
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
const Comp = {
|
||||
@@ -124,14 +112,40 @@ describe('component: proxy', () => {
|
||||
instanceProxy = this
|
||||
}
|
||||
}
|
||||
app.mount(Comp, nodeOps.createElement('div'))
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
instanceProxy.foo = 1
|
||||
expect(instanceProxy.foo).toBe(1)
|
||||
expect(instance!.sink.foo).toBe(1)
|
||||
})
|
||||
|
||||
test('globalProperties', () => {
|
||||
let instance: ComponentInternalInstance
|
||||
let instanceProxy: any
|
||||
const Comp = {
|
||||
setup() {
|
||||
return () => null
|
||||
},
|
||||
mounted() {
|
||||
instance = getCurrentInstance()!
|
||||
instanceProxy = this
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Comp)
|
||||
app.config.globalProperties.foo = 1
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
|
||||
expect(instanceProxy.foo).toBe(1)
|
||||
|
||||
// set should overwrite globalProperties with local
|
||||
instanceProxy.foo = 2
|
||||
expect(instanceProxy.foo).toBe(2)
|
||||
expect(instance!.sink.foo).toBe(2)
|
||||
// should not affect global
|
||||
expect(app.config.globalProperties.foo).toBe(1)
|
||||
})
|
||||
|
||||
test('has check', () => {
|
||||
const app = createApp()
|
||||
let instanceProxy: any
|
||||
const Comp = {
|
||||
render() {},
|
||||
@@ -152,7 +166,11 @@ describe('component: proxy', () => {
|
||||
instanceProxy = this
|
||||
}
|
||||
}
|
||||
app.mount(Comp, nodeOps.createElement('div'), { msg: 'hello' })
|
||||
|
||||
const app = createApp(Comp, { msg: 'hello' })
|
||||
app.config.globalProperties.global = 1
|
||||
|
||||
app.mount(nodeOps.createElement('div'))
|
||||
|
||||
// props
|
||||
expect('msg' in instanceProxy).toBe(true)
|
||||
@@ -162,6 +180,8 @@ describe('component: proxy', () => {
|
||||
expect('bar' in instanceProxy).toBe(true)
|
||||
// public properties
|
||||
expect('$el' in instanceProxy).toBe(true)
|
||||
// global properties
|
||||
expect('global' in instanceProxy).toBe(true)
|
||||
|
||||
// non-existent
|
||||
expect('$foobar' in instanceProxy).toBe(false)
|
||||
@@ -170,5 +190,28 @@ describe('component: proxy', () => {
|
||||
// set non-existent (goes into sink)
|
||||
instanceProxy.baz = 1
|
||||
expect('baz' in instanceProxy).toBe(true)
|
||||
|
||||
// dev mode ownKeys check for console inspection
|
||||
// should only expose own keys
|
||||
expect(Object.keys(instanceProxy)).toMatchObject([
|
||||
'msg',
|
||||
'bar',
|
||||
'foo',
|
||||
'baz'
|
||||
])
|
||||
})
|
||||
|
||||
// #864
|
||||
test('should not warn declared but absent props', () => {
|
||||
const Comp = {
|
||||
props: ['test'],
|
||||
render(this: any) {
|
||||
return this.test
|
||||
}
|
||||
}
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
expect(
|
||||
`was accessed during render but is not defined`
|
||||
).not.toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
serializeInner,
|
||||
serialize,
|
||||
VNodeProps,
|
||||
KeepAlive
|
||||
KeepAlive,
|
||||
TestElement
|
||||
} from '@vue/runtime-test'
|
||||
|
||||
function mount(
|
||||
@@ -42,13 +43,13 @@ function mockProps(extra: BaseTransitionProps = {}, withKeepAlive = false) {
|
||||
}
|
||||
}),
|
||||
onEnter: jest.fn((el, done) => {
|
||||
cbs.doneEnter[serialize(el)] = done
|
||||
cbs.doneEnter[serialize(el as TestElement)] = done
|
||||
}),
|
||||
onAfterEnter: jest.fn(),
|
||||
onEnterCancelled: jest.fn(),
|
||||
onBeforeLeave: jest.fn(),
|
||||
onLeave: jest.fn((el, done) => {
|
||||
cbs.doneLeave[serialize(el)] = done
|
||||
cbs.doneLeave[serialize(el as TestElement)] = done
|
||||
}),
|
||||
onAfterLeave: jest.fn(),
|
||||
onLeaveCancelled: jest.fn(),
|
||||
@@ -64,8 +65,10 @@ function assertCalls(
|
||||
props: BaseTransitionProps,
|
||||
calls: Record<string, number>
|
||||
) {
|
||||
Object.keys(calls).forEach((key: keyof BaseTransitionProps) => {
|
||||
expect(props[key]).toHaveBeenCalledTimes(calls[key])
|
||||
Object.keys(calls).forEach(key => {
|
||||
expect(props[key as keyof BaseTransitionProps]).toHaveBeenCalledTimes(
|
||||
calls[key]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -147,19 +150,19 @@ describe('BaseTransition', () => {
|
||||
const toggle = ref(true)
|
||||
const hooks: VNodeProps = {
|
||||
onVnodeBeforeMount(vnode) {
|
||||
vnode.transition!.beforeEnter(vnode.el)
|
||||
vnode.transition!.beforeEnter(vnode.el!)
|
||||
},
|
||||
onVnodeMounted(vnode) {
|
||||
vnode.transition!.enter(vnode.el)
|
||||
vnode.transition!.enter(vnode.el!)
|
||||
},
|
||||
onVnodeUpdated(vnode, oldVnode) {
|
||||
if (oldVnode.props!.id !== vnode.props!.id) {
|
||||
if (vnode.props!.id) {
|
||||
vnode.transition!.beforeEnter(vnode.el)
|
||||
vnode.transition!.beforeEnter(vnode.el!)
|
||||
state.show = true
|
||||
vnode.transition!.enter(vnode.el)
|
||||
vnode.transition!.enter(vnode.el!)
|
||||
} else {
|
||||
vnode.transition!.leave(vnode.el, () => {
|
||||
vnode.transition!.leave(vnode.el!, () => {
|
||||
state.show = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -531,5 +531,32 @@ describe('KeepAlive', () => {
|
||||
await nextTick()
|
||||
expect(Foo.unmounted).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should update re-activated component if props have changed', async () => {
|
||||
const Foo = (props: { n: number }) => props.n
|
||||
|
||||
const toggle = ref(true)
|
||||
const n = ref(0)
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
return () =>
|
||||
h(KeepAlive, () => (toggle.value ? h(Foo, { n: n.value }) : null))
|
||||
}
|
||||
}
|
||||
|
||||
render(h(App), root)
|
||||
expect(serializeInner(root)).toBe(`0`)
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<!---->`)
|
||||
|
||||
n.value++
|
||||
await nextTick()
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`1`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
nextTick,
|
||||
onMounted,
|
||||
watch,
|
||||
watchEffect,
|
||||
onUnmounted,
|
||||
onErrorCaptured
|
||||
} from '@vue/runtime-test'
|
||||
@@ -21,7 +22,7 @@ describe('Suspense', () => {
|
||||
})
|
||||
|
||||
// a simple async factory for testing purposes only.
|
||||
function createAsyncComponent<T extends ComponentOptions>(
|
||||
function defineAsyncComponent<T extends ComponentOptions>(
|
||||
comp: T,
|
||||
delay: number = 0
|
||||
) {
|
||||
@@ -41,7 +42,7 @@ describe('Suspense', () => {
|
||||
}
|
||||
|
||||
test('fallback content', async () => {
|
||||
const Async = createAsyncComponent({
|
||||
const Async = defineAsyncComponent({
|
||||
render() {
|
||||
return h('div', 'async')
|
||||
}
|
||||
@@ -69,7 +70,7 @@ describe('Suspense', () => {
|
||||
test('nested async deps', async () => {
|
||||
const calls: string[] = []
|
||||
|
||||
const AsyncOuter = createAsyncComponent({
|
||||
const AsyncOuter = defineAsyncComponent({
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
calls.push('outer mounted')
|
||||
@@ -78,7 +79,7 @@ describe('Suspense', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const AsyncInner = createAsyncComponent(
|
||||
const AsyncInner = defineAsyncComponent(
|
||||
{
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
@@ -117,7 +118,7 @@ describe('Suspense', () => {
|
||||
})
|
||||
|
||||
test('onResolve', async () => {
|
||||
const Async = createAsyncComponent({
|
||||
const Async = defineAsyncComponent({
|
||||
render() {
|
||||
return h('div', 'async')
|
||||
}
|
||||
@@ -163,9 +164,15 @@ describe('Suspense', () => {
|
||||
// extra tick needed for Node 12+
|
||||
deps.push(p.then(() => Promise.resolve()))
|
||||
|
||||
watch(() => {
|
||||
watchEffect(() => {
|
||||
calls.push('immediate effect')
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
watch(count, v => {
|
||||
calls.push('watch callback')
|
||||
})
|
||||
count.value++ // trigger the watcher now
|
||||
|
||||
onMounted(() => {
|
||||
calls.push('mounted')
|
||||
@@ -193,23 +200,30 @@ describe('Suspense', () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||
expect(calls).toEqual([])
|
||||
expect(calls).toEqual([`immediate effect`])
|
||||
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>async</div>`)
|
||||
expect(calls).toEqual([`watch callback`, `mounted`])
|
||||
expect(calls).toEqual([`immediate effect`, `watch callback`, `mounted`])
|
||||
|
||||
// effects inside an already resolved suspense should happen at normal timing
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<!---->`)
|
||||
expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted'])
|
||||
expect(calls).toEqual([
|
||||
`immediate effect`,
|
||||
`watch callback`,
|
||||
`mounted`,
|
||||
'unmounted'
|
||||
])
|
||||
})
|
||||
|
||||
test('content update before suspense resolve', async () => {
|
||||
const Async = createAsyncComponent({
|
||||
setup(props: { msg: string }) {
|
||||
const Async = defineAsyncComponent({
|
||||
props: { msg: String },
|
||||
setup(props: any) {
|
||||
return () => h('div', props.msg)
|
||||
}
|
||||
})
|
||||
@@ -253,9 +267,15 @@ describe('Suspense', () => {
|
||||
const p = new Promise(r => setTimeout(r, 1))
|
||||
deps.push(p)
|
||||
|
||||
watch(() => {
|
||||
watchEffect(() => {
|
||||
calls.push('immediate effect')
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
watch(count, () => {
|
||||
calls.push('watch callback')
|
||||
})
|
||||
count.value++ // trigger the watcher now
|
||||
|
||||
onMounted(() => {
|
||||
calls.push('mounted')
|
||||
@@ -283,7 +303,7 @@ describe('Suspense', () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||
expect(calls).toEqual([])
|
||||
expect(calls).toEqual(['immediate effect'])
|
||||
|
||||
// remove the async dep before it's resolved
|
||||
toggle.value = false
|
||||
@@ -294,15 +314,15 @@ describe('Suspense', () => {
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<!---->`)
|
||||
// should discard effects
|
||||
expect(calls).toEqual([])
|
||||
// should discard effects (except for immediate ones)
|
||||
expect(calls).toEqual(['immediate effect'])
|
||||
})
|
||||
|
||||
test('unmount suspense after resolve', async () => {
|
||||
const toggle = ref(true)
|
||||
const unmounted = jest.fn()
|
||||
|
||||
const Async = createAsyncComponent({
|
||||
const Async = defineAsyncComponent({
|
||||
setup() {
|
||||
onUnmounted(unmounted)
|
||||
return () => h('div', 'async')
|
||||
@@ -341,7 +361,7 @@ describe('Suspense', () => {
|
||||
const mounted = jest.fn()
|
||||
const unmounted = jest.fn()
|
||||
|
||||
const Async = createAsyncComponent({
|
||||
const Async = defineAsyncComponent({
|
||||
setup() {
|
||||
onMounted(mounted)
|
||||
onUnmounted(unmounted)
|
||||
@@ -381,7 +401,7 @@ describe('Suspense', () => {
|
||||
test('nested suspense (parent resolves first)', async () => {
|
||||
const calls: string[] = []
|
||||
|
||||
const AsyncOuter = createAsyncComponent(
|
||||
const AsyncOuter = defineAsyncComponent(
|
||||
{
|
||||
setup: () => {
|
||||
onMounted(() => {
|
||||
@@ -393,7 +413,7 @@ describe('Suspense', () => {
|
||||
1
|
||||
)
|
||||
|
||||
const AsyncInner = createAsyncComponent(
|
||||
const AsyncInner = defineAsyncComponent(
|
||||
{
|
||||
setup: () => {
|
||||
onMounted(() => {
|
||||
@@ -432,14 +452,14 @@ describe('Suspense', () => {
|
||||
await deps[0]
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>async outer</div><div>fallback inner</div><!---->`
|
||||
`<div>async outer</div><div>fallback inner</div>`
|
||||
)
|
||||
expect(calls).toEqual([`outer mounted`])
|
||||
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>async outer</div><div>async inner</div><!---->`
|
||||
`<div>async outer</div><div>async inner</div>`
|
||||
)
|
||||
expect(calls).toEqual([`outer mounted`, `inner mounted`])
|
||||
})
|
||||
@@ -447,7 +467,7 @@ describe('Suspense', () => {
|
||||
test('nested suspense (child resolves first)', async () => {
|
||||
const calls: string[] = []
|
||||
|
||||
const AsyncOuter = createAsyncComponent(
|
||||
const AsyncOuter = defineAsyncComponent(
|
||||
{
|
||||
setup: () => {
|
||||
onMounted(() => {
|
||||
@@ -459,7 +479,7 @@ describe('Suspense', () => {
|
||||
10
|
||||
)
|
||||
|
||||
const AsyncInner = createAsyncComponent(
|
||||
const AsyncInner = defineAsyncComponent(
|
||||
{
|
||||
setup: () => {
|
||||
onMounted(() => {
|
||||
@@ -503,7 +523,7 @@ describe('Suspense', () => {
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>async outer</div><div>async inner</div><!---->`
|
||||
`<div>async outer</div><div>async inner</div>`
|
||||
)
|
||||
expect(calls).toEqual([`inner mounted`, `outer mounted`])
|
||||
})
|
||||
@@ -517,15 +537,18 @@ describe('Suspense', () => {
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
const error = ref<Error | null>(null)
|
||||
onErrorCaptured(e => {
|
||||
error.value = e
|
||||
const errorMessage = ref<string | null>(null)
|
||||
onErrorCaptured(err => {
|
||||
errorMessage.value =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: `A non-Error value thrown: ${err}`
|
||||
return true
|
||||
})
|
||||
|
||||
return () =>
|
||||
error.value
|
||||
? h('div', error.value.message)
|
||||
errorMessage.value
|
||||
? h('div', errorMessage.value)
|
||||
: h(Suspense, null, {
|
||||
default: h(Async),
|
||||
fallback: h('div', 'fallback')
|
||||
@@ -546,8 +569,9 @@ describe('Suspense', () => {
|
||||
const msg = ref('nested msg')
|
||||
const calls: number[] = []
|
||||
|
||||
const AsyncChildWithSuspense = createAsyncComponent({
|
||||
setup(props: { msg: string }) {
|
||||
const AsyncChildWithSuspense = defineAsyncComponent({
|
||||
props: { msg: String },
|
||||
setup(props: any) {
|
||||
onMounted(() => {
|
||||
calls.push(0)
|
||||
})
|
||||
@@ -559,9 +583,10 @@ describe('Suspense', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const AsyncInsideNestedSuspense = createAsyncComponent(
|
||||
const AsyncInsideNestedSuspense = defineAsyncComponent(
|
||||
{
|
||||
setup(props: { msg: string }) {
|
||||
props: { msg: String },
|
||||
setup(props: any) {
|
||||
onMounted(() => {
|
||||
calls.push(2)
|
||||
})
|
||||
@@ -571,8 +596,9 @@ describe('Suspense', () => {
|
||||
20
|
||||
)
|
||||
|
||||
const AsyncChildParent = createAsyncComponent({
|
||||
setup(props: { msg: string }) {
|
||||
const AsyncChildParent = defineAsyncComponent({
|
||||
props: { msg: String },
|
||||
setup(props: any) {
|
||||
onMounted(() => {
|
||||
calls.push(1)
|
||||
})
|
||||
@@ -580,9 +606,10 @@ describe('Suspense', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const NestedAsyncChild = createAsyncComponent(
|
||||
const NestedAsyncChild = defineAsyncComponent(
|
||||
{
|
||||
setup(props: { msg: string }) {
|
||||
props: { msg: String },
|
||||
setup(props: any) {
|
||||
onMounted(() => {
|
||||
calls.push(3)
|
||||
})
|
||||
@@ -644,7 +671,7 @@ describe('Suspense', () => {
|
||||
await deps[3]
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>nested fallback</div><div>root async</div><!---->`
|
||||
`<div>nested fallback</div><div>root async</div>`
|
||||
)
|
||||
expect(calls).toEqual([0, 1, 3])
|
||||
|
||||
@@ -655,7 +682,7 @@ describe('Suspense', () => {
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>nested changed</div><div>root async</div><!---->`
|
||||
`<div>nested changed</div><div>root async</div>`
|
||||
)
|
||||
expect(calls).toEqual([0, 1, 3, 2])
|
||||
|
||||
@@ -663,20 +690,20 @@ describe('Suspense', () => {
|
||||
msg.value = 'nested changed again'
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>nested changed again</div><div>root async</div><!---->`
|
||||
`<div>nested changed again</div><div>root async</div>`
|
||||
)
|
||||
})
|
||||
|
||||
test('new async dep after resolve should cause suspense to restart', async () => {
|
||||
const toggle = ref(false)
|
||||
|
||||
const ChildA = createAsyncComponent({
|
||||
const ChildA = defineAsyncComponent({
|
||||
setup() {
|
||||
return () => h('div', 'Child A')
|
||||
}
|
||||
})
|
||||
|
||||
const ChildB = createAsyncComponent({
|
||||
const ChildB = defineAsyncComponent({
|
||||
setup() {
|
||||
return () => h('div', 'Child B')
|
||||
}
|
||||
@@ -698,7 +725,7 @@ describe('Suspense', () => {
|
||||
|
||||
await deps[0]
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<!----><div>Child A</div><!----><!---->`)
|
||||
expect(serializeInner(root)).toBe(`<div>Child A</div><!---->`)
|
||||
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
@@ -706,10 +733,8 @@ describe('Suspense', () => {
|
||||
|
||||
await deps[1]
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>Child A</div><div>Child B</div><!---->`
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>Child A</div><div>Child B</div>`)
|
||||
})
|
||||
|
||||
test.todo('portal inside suspense')
|
||||
test.todo('teleport inside suspense')
|
||||
})
|
||||
|
||||
302
packages/runtime-core/__tests__/components/Teleport.spec.ts
Normal file
302
packages/runtime-core/__tests__/components/Teleport.spec.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import {
|
||||
nodeOps,
|
||||
serializeInner,
|
||||
render,
|
||||
h,
|
||||
Teleport,
|
||||
Text,
|
||||
ref,
|
||||
nextTick
|
||||
} from '@vue/runtime-test'
|
||||
import { createVNode, Fragment } from '../../src/vnode'
|
||||
|
||||
describe('renderer: teleport', () => {
|
||||
test('should work', () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
render(
|
||||
h(() => [
|
||||
h(Teleport, { to: target }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
]),
|
||||
root
|
||||
)
|
||||
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
})
|
||||
|
||||
test('should update target', async () => {
|
||||
const targetA = nodeOps.createElement('div')
|
||||
const targetB = nodeOps.createElement('div')
|
||||
const target = ref(targetA)
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
render(
|
||||
h(() => [
|
||||
h(Teleport, { to: target.value }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
]),
|
||||
root
|
||||
)
|
||||
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(targetA)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
expect(serializeInner(targetB)).toMatchInlineSnapshot(`""`)
|
||||
|
||||
target.value = targetB
|
||||
await nextTick()
|
||||
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(targetA)).toMatchInlineSnapshot(`""`)
|
||||
expect(serializeInner(targetB)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
})
|
||||
|
||||
test('should update children', async () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
const children = ref([h('div', 'teleported')])
|
||||
|
||||
render(h(Teleport, { to: target }, children.value), root)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
|
||||
children.value = []
|
||||
await nextTick()
|
||||
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
|
||||
children.value = [createVNode(Text, null, 'teleported')]
|
||||
await nextTick()
|
||||
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
})
|
||||
|
||||
test('should remove children when unmounted', () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
render(
|
||||
h(() => [
|
||||
h(Teleport, { to: target }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
|
||||
render(null, root)
|
||||
expect(serializeInner(target)).toBe('')
|
||||
})
|
||||
|
||||
test('multiple teleport with same target', () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
render(
|
||||
h('div', [
|
||||
h(Teleport, { to: target }, h('div', 'one')),
|
||||
h(Teleport, { to: target }, 'two')
|
||||
]),
|
||||
root
|
||||
)
|
||||
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(`"<div>one</div>two"`)
|
||||
|
||||
// update existing content
|
||||
render(
|
||||
h('div', [
|
||||
h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
|
||||
h(Teleport, { to: target }, 'three')
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>one</div><div>two</div>three"`
|
||||
)
|
||||
|
||||
// toggling
|
||||
render(h('div', [null, h(Teleport, { to: target }, 'three')]), root)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<div><!----><!--teleport start--><!--teleport end--></div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(`"three"`)
|
||||
|
||||
// toggle back
|
||||
render(
|
||||
h('div', [
|
||||
h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
|
||||
h(Teleport, { to: target }, 'three')
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>"`
|
||||
)
|
||||
// should append
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"three<div>one</div><div>two</div>"`
|
||||
)
|
||||
|
||||
// toggle the other teleport
|
||||
render(
|
||||
h('div', [
|
||||
h(Teleport, { to: target }, [h('div', 'one'), h('div', 'two')]),
|
||||
null
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<div><!--teleport start--><!--teleport end--><!----></div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>one</div><div>two</div>"`
|
||||
)
|
||||
})
|
||||
|
||||
test('disabled', () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
const renderWithDisabled = (disabled: boolean) => {
|
||||
return h(Fragment, [
|
||||
h(Teleport, { to: target, disabled }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
])
|
||||
}
|
||||
|
||||
render(renderWithDisabled(false), root)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
|
||||
render(renderWithDisabled(true), root)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toBe(``)
|
||||
|
||||
// toggle back
|
||||
render(renderWithDisabled(false), root)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
})
|
||||
|
||||
test('moving teleport while enabled', () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
render(
|
||||
h(Fragment, [
|
||||
h(Teleport, { to: target }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
|
||||
render(
|
||||
h(Fragment, [
|
||||
h('div', 'root'),
|
||||
h(Teleport, { to: target }, h('div', 'teleported'))
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<div>root</div><!--teleport start--><!--teleport end-->"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
|
||||
render(
|
||||
h(Fragment, [
|
||||
h(Teleport, { to: target }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toMatchInlineSnapshot(
|
||||
`"<div>teleported</div>"`
|
||||
)
|
||||
})
|
||||
|
||||
test('moving teleport while disabled', () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
render(
|
||||
h(Fragment, [
|
||||
h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toBe('')
|
||||
|
||||
render(
|
||||
h(Fragment, [
|
||||
h('div', 'root'),
|
||||
h(Teleport, { to: target, disabled: true }, h('div', 'teleported'))
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<div>root</div><!--teleport start--><div>teleported</div><!--teleport end-->"`
|
||||
)
|
||||
expect(serializeInner(target)).toBe('')
|
||||
|
||||
render(
|
||||
h(Fragment, [
|
||||
h(Teleport, { to: target, disabled: true }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toMatchInlineSnapshot(
|
||||
`"<!--teleport start--><div>teleported</div><!--teleport end--><div>root</div>"`
|
||||
)
|
||||
expect(serializeInner(target)).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -98,6 +98,15 @@ describe('directives', () => {
|
||||
expect(prevVNode).toBe(null)
|
||||
}) as DirectiveHook)
|
||||
|
||||
const dir = {
|
||||
beforeMount,
|
||||
mounted,
|
||||
beforeUpdate,
|
||||
updated,
|
||||
beforeUnmount,
|
||||
unmounted
|
||||
}
|
||||
|
||||
let _instance: ComponentInternalInstance | null = null
|
||||
let _vnode: VNode | null = null
|
||||
let _prevVnode: VNode | null = null
|
||||
@@ -109,14 +118,7 @@ describe('directives', () => {
|
||||
_prevVnode = _vnode
|
||||
_vnode = withDirectives(h('div', count.value), [
|
||||
[
|
||||
{
|
||||
beforeMount,
|
||||
mounted,
|
||||
beforeUpdate,
|
||||
updated,
|
||||
beforeUnmount,
|
||||
unmounted
|
||||
},
|
||||
dir,
|
||||
// value
|
||||
count.value,
|
||||
// argument
|
||||
@@ -132,17 +134,17 @@ describe('directives', () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
|
||||
expect(beforeMount).toHaveBeenCalled()
|
||||
expect(mounted).toHaveBeenCalled()
|
||||
expect(beforeMount).toHaveBeenCalledTimes(1)
|
||||
expect(mounted).toHaveBeenCalledTimes(1)
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(beforeUpdate).toHaveBeenCalled()
|
||||
expect(updated).toHaveBeenCalled()
|
||||
expect(beforeUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(updated).toHaveBeenCalledTimes(1)
|
||||
|
||||
render(null, root)
|
||||
expect(beforeUnmount).toHaveBeenCalled()
|
||||
expect(unmounted).toHaveBeenCalled()
|
||||
expect(beforeUnmount).toHaveBeenCalledTimes(1)
|
||||
expect(unmounted).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should work with a function directive', async () => {
|
||||
@@ -198,4 +200,144 @@ describe('directives', () => {
|
||||
await nextTick()
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should work on component vnode', async () => {
|
||||
const count = ref(0)
|
||||
|
||||
function assertBindings(binding: DirectiveBinding) {
|
||||
expect(binding.value).toBe(count.value)
|
||||
expect(binding.arg).toBe('foo')
|
||||
expect(binding.instance).toBe(_instance && _instance.proxy)
|
||||
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
|
||||
}
|
||||
|
||||
const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||
expect(el.tag).toBe('div')
|
||||
// should not be inserted yet
|
||||
expect(el.parentNode).toBe(null)
|
||||
expect(root.children.length).toBe(0)
|
||||
|
||||
assertBindings(binding)
|
||||
|
||||
expect(vnode.type).toBe(_vnode!.type)
|
||||
expect(prevVNode).toBe(null)
|
||||
}) as DirectiveHook)
|
||||
|
||||
const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||
expect(el.tag).toBe('div')
|
||||
// should be inserted now
|
||||
expect(el.parentNode).toBe(root)
|
||||
expect(root.children[0]).toBe(el)
|
||||
|
||||
assertBindings(binding)
|
||||
|
||||
expect(vnode.type).toBe(_vnode!.type)
|
||||
expect(prevVNode).toBe(null)
|
||||
}) as DirectiveHook)
|
||||
|
||||
const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||
expect(el.tag).toBe('div')
|
||||
expect(el.parentNode).toBe(root)
|
||||
expect(root.children[0]).toBe(el)
|
||||
|
||||
// node should not have been updated yet
|
||||
// expect(el.children[0].text).toBe(`${count.value - 1}`)
|
||||
|
||||
assertBindings(binding)
|
||||
|
||||
expect(vnode.type).toBe(_vnode!.type)
|
||||
expect(prevVNode!.type).toBe(_prevVnode!.type)
|
||||
}) as DirectiveHook)
|
||||
|
||||
const updated = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||
expect(el.tag).toBe('div')
|
||||
expect(el.parentNode).toBe(root)
|
||||
expect(root.children[0]).toBe(el)
|
||||
|
||||
// node should have been updated
|
||||
expect(el.children[0].text).toBe(`${count.value}`)
|
||||
|
||||
assertBindings(binding)
|
||||
|
||||
expect(vnode.type).toBe(_vnode!.type)
|
||||
expect(prevVNode!.type).toBe(_prevVnode!.type)
|
||||
}) as DirectiveHook)
|
||||
|
||||
const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||
expect(el.tag).toBe('div')
|
||||
// should be removed now
|
||||
expect(el.parentNode).toBe(root)
|
||||
expect(root.children[0]).toBe(el)
|
||||
|
||||
assertBindings(binding)
|
||||
|
||||
expect(vnode.type).toBe(_vnode!.type)
|
||||
expect(prevVNode).toBe(null)
|
||||
}) as DirectiveHook)
|
||||
|
||||
const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
|
||||
expect(el.tag).toBe('div')
|
||||
// should have been removed
|
||||
expect(el.parentNode).toBe(null)
|
||||
expect(root.children.length).toBe(0)
|
||||
|
||||
assertBindings(binding)
|
||||
|
||||
expect(vnode.type).toBe(_vnode!.type)
|
||||
expect(prevVNode).toBe(null)
|
||||
}) as DirectiveHook)
|
||||
|
||||
const dir = {
|
||||
beforeMount,
|
||||
mounted,
|
||||
beforeUpdate,
|
||||
updated,
|
||||
beforeUnmount,
|
||||
unmounted
|
||||
}
|
||||
|
||||
let _instance: ComponentInternalInstance | null = null
|
||||
let _vnode: VNode | null = null
|
||||
let _prevVnode: VNode | null = null
|
||||
|
||||
const Child = (props: { count: number }) => {
|
||||
_prevVnode = _vnode
|
||||
_vnode = h('div', props.count)
|
||||
return _vnode
|
||||
}
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
_instance = currentInstance
|
||||
},
|
||||
render() {
|
||||
return withDirectives(h(Child, { count: count.value }), [
|
||||
[
|
||||
dir,
|
||||
// value
|
||||
count.value,
|
||||
// argument
|
||||
'foo',
|
||||
// modifiers
|
||||
{ ok: true }
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
|
||||
expect(beforeMount).toHaveBeenCalledTimes(1)
|
||||
expect(mounted).toHaveBeenCalledTimes(1)
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(beforeUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(updated).toHaveBeenCalledTimes(1)
|
||||
|
||||
render(null, root)
|
||||
expect(beforeUnmount).toHaveBeenCalledTimes(1)
|
||||
expect(unmounted).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
watch,
|
||||
ref,
|
||||
nextTick,
|
||||
mockWarn,
|
||||
createComponent
|
||||
defineComponent,
|
||||
watchEffect
|
||||
} from '@vue/runtime-test'
|
||||
import { setErrorRecovery } from '../src/errorHandling'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('error handling', () => {
|
||||
mockWarn()
|
||||
@@ -235,13 +236,13 @@ describe('error handling', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const Child = createComponent(() => () => h('div', { ref }))
|
||||
const Child = defineComponent(() => () => h('div', { ref }))
|
||||
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
expect(fn).toHaveBeenCalledWith(err, 'ref function')
|
||||
})
|
||||
|
||||
test('in watch (simple usage)', () => {
|
||||
test('in effect', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = jest.fn()
|
||||
|
||||
@@ -257,7 +258,7 @@ describe('error handling', () => {
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
watch(() => {
|
||||
watchEffect(() => {
|
||||
throw err
|
||||
})
|
||||
return () => null
|
||||
@@ -298,7 +299,7 @@ describe('error handling', () => {
|
||||
expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
|
||||
})
|
||||
|
||||
test('in watch callback', () => {
|
||||
test('in watch callback', async () => {
|
||||
const err = new Error('foo')
|
||||
const fn = jest.fn()
|
||||
|
||||
@@ -312,10 +313,11 @@ describe('error handling', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const count = ref(0)
|
||||
const Child = {
|
||||
setup() {
|
||||
watch(
|
||||
() => 1,
|
||||
() => count.value,
|
||||
() => {
|
||||
throw err
|
||||
}
|
||||
@@ -325,10 +327,13 @@ describe('error handling', () => {
|
||||
}
|
||||
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
|
||||
count.value++
|
||||
await nextTick()
|
||||
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
|
||||
})
|
||||
|
||||
test('in watch cleanup', async () => {
|
||||
test('in effect cleanup', async () => {
|
||||
const err = new Error('foo')
|
||||
const count = ref(0)
|
||||
const fn = jest.fn()
|
||||
@@ -345,7 +350,7 @@ describe('error handling', () => {
|
||||
|
||||
const Child = {
|
||||
setup() {
|
||||
watch(onCleanup => {
|
||||
watchEffect(onCleanup => {
|
||||
count.value
|
||||
onCleanup(() => {
|
||||
throw err
|
||||
@@ -362,7 +367,7 @@ describe('error handling', () => {
|
||||
expect(fn).toHaveBeenCalledWith(err, 'watcher cleanup function')
|
||||
})
|
||||
|
||||
test('in component event handler', () => {
|
||||
test('in component event handler via emit', () => {
|
||||
const err = new Error('foo')
|
||||
const fn = jest.fn()
|
||||
|
||||
@@ -392,6 +397,78 @@ describe('error handling', () => {
|
||||
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
|
||||
})
|
||||
|
||||
test('in component event handler via emit (async)', async () => {
|
||||
const err = new Error('foo')
|
||||
const fn = jest.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return true
|
||||
})
|
||||
return () =>
|
||||
h(Child, {
|
||||
async onFoo() {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let res: any
|
||||
const Child = {
|
||||
setup(props: any, { emit }: any) {
|
||||
res = emit('foo')
|
||||
return () => null
|
||||
}
|
||||
}
|
||||
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
|
||||
try {
|
||||
await Promise.all(res)
|
||||
} catch (e) {
|
||||
expect(e).toBe(err)
|
||||
}
|
||||
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
|
||||
})
|
||||
|
||||
test('in component event handler via emit (async + array)', async () => {
|
||||
const err = new Error('foo')
|
||||
const fn = jest.fn()
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
fn(err, info)
|
||||
return true
|
||||
})
|
||||
return () =>
|
||||
h(Child, {
|
||||
onFoo: [() => Promise.reject(err), () => Promise.resolve(1)]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let res: any
|
||||
const Child = {
|
||||
setup(props: any, { emit }: any) {
|
||||
res = emit('foo')
|
||||
return () => null
|
||||
}
|
||||
}
|
||||
|
||||
render(h(Comp), nodeOps.createElement('div'))
|
||||
|
||||
try {
|
||||
await Promise.all(res)
|
||||
} catch (e) {
|
||||
expect(e).toBe(err)
|
||||
}
|
||||
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
|
||||
})
|
||||
|
||||
it('should warn unhandled', () => {
|
||||
const onError = jest.spyOn(console, 'error')
|
||||
onError.mockImplementation(() => {})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
mockWarn,
|
||||
createApp,
|
||||
nodeOps,
|
||||
resolveComponent,
|
||||
@@ -7,19 +6,23 @@ import {
|
||||
Component,
|
||||
Directive,
|
||||
resolveDynamicComponent,
|
||||
getCurrentInstance
|
||||
h,
|
||||
serializeInner,
|
||||
createVNode
|
||||
} from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('resolveAssets', () => {
|
||||
mockWarn()
|
||||
|
||||
test('should work', () => {
|
||||
const app = createApp()
|
||||
const FooBar = () => null
|
||||
const BarBaz = { mounted: () => null }
|
||||
|
||||
let component1: Component
|
||||
let component2: Component
|
||||
let component3: Component
|
||||
let component4: Component
|
||||
let component1: Component | string
|
||||
let component2: Component | string
|
||||
let component3: Component | string
|
||||
let component4: Component | string
|
||||
let directive1: Directive
|
||||
let directive2: Directive
|
||||
let directive3: Directive
|
||||
@@ -49,8 +52,9 @@ describe('resolveAssets', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Root)
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(Root, root)
|
||||
app.mount(root)
|
||||
expect(component1!).toBe(FooBar)
|
||||
expect(component2!).toBe(FooBar)
|
||||
expect(component3!).toBe(FooBar)
|
||||
@@ -63,8 +67,6 @@ describe('resolveAssets', () => {
|
||||
})
|
||||
|
||||
describe('warning', () => {
|
||||
mockWarn()
|
||||
|
||||
test('used outside render() or setup()', () => {
|
||||
resolveComponent('foo')
|
||||
expect(
|
||||
@@ -78,7 +80,6 @@ describe('resolveAssets', () => {
|
||||
})
|
||||
|
||||
test('not exist', () => {
|
||||
const app = createApp()
|
||||
const Root = {
|
||||
setup() {
|
||||
resolveComponent('foo')
|
||||
@@ -87,36 +88,64 @@ describe('resolveAssets', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Root)
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(Root, root)
|
||||
app.mount(root)
|
||||
expect('Failed to resolve component: foo').toHaveBeenWarned()
|
||||
expect('Failed to resolve directive: bar').toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('resolve dynamic component', () => {
|
||||
const app = createApp()
|
||||
const dynamicComponents = {
|
||||
foo: () => 'foo',
|
||||
bar: () => 'bar',
|
||||
baz: { render: () => 'baz' }
|
||||
}
|
||||
let foo, bar, baz // dynamic components
|
||||
|
||||
const Child = {
|
||||
render(this: any) {
|
||||
return this.$slots.default()
|
||||
}
|
||||
}
|
||||
|
||||
const Root = {
|
||||
components: { foo: dynamicComponents.foo },
|
||||
setup() {
|
||||
const instance = getCurrentInstance()!
|
||||
return () => {
|
||||
foo = resolveDynamicComponent('foo', instance) // <component is="foo"/>
|
||||
bar = resolveDynamicComponent(dynamicComponents.bar, instance) // <component :is="bar"/>, function
|
||||
baz = resolveDynamicComponent(dynamicComponents.baz, instance) // <component :is="baz"/>, object
|
||||
foo = resolveDynamicComponent('foo') // <component is="foo"/>
|
||||
bar = resolveDynamicComponent(dynamicComponents.bar) // <component :is="bar"/>, function
|
||||
return h(Child, () => {
|
||||
// check inside child slots
|
||||
baz = resolveDynamicComponent(dynamicComponents.baz) // <component :is="baz"/>, object
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Root)
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(Root, root)
|
||||
app.mount(root)
|
||||
expect(foo).toBe(dynamicComponents.foo)
|
||||
expect(bar).toBe(dynamicComponents.bar)
|
||||
expect(baz).toBe(dynamicComponents.baz)
|
||||
})
|
||||
|
||||
test('resolve dynamic component should fallback to plain element without warning', () => {
|
||||
const Root = {
|
||||
setup() {
|
||||
return () => {
|
||||
return createVNode(resolveDynamicComponent('div') as string, null, {
|
||||
default: () => 'hello'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(Root)
|
||||
const root = nodeOps.createElement('div')
|
||||
app.mount(root)
|
||||
expect(serializeInner(root)).toBe('<div>hello</div>')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
68
packages/runtime-core/__tests__/helpers/scopeId.spec.ts
Normal file
68
packages/runtime-core/__tests__/helpers/scopeId.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { withScopeId } from '../../src/helpers/scopeId'
|
||||
import { h, render, nodeOps, serializeInner } from '@vue/runtime-test'
|
||||
|
||||
describe('scopeId runtime support', () => {
|
||||
const withParentId = withScopeId('parent')
|
||||
const withChildId = withScopeId('child')
|
||||
|
||||
test('should attach scopeId', () => {
|
||||
const App = {
|
||||
__scopeId: 'parent',
|
||||
render: withParentId(() => {
|
||||
return h('div', [h('div')])
|
||||
})
|
||||
}
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(App), root)
|
||||
expect(serializeInner(root)).toBe(`<div parent><div parent></div></div>`)
|
||||
})
|
||||
|
||||
test('should attach scopeId to components in parent component', () => {
|
||||
const Child = {
|
||||
__scopeId: 'child',
|
||||
render: withChildId(() => {
|
||||
return h('div')
|
||||
})
|
||||
}
|
||||
const App = {
|
||||
__scopeId: 'parent',
|
||||
render: withParentId(() => {
|
||||
return h('div', [h(Child)])
|
||||
})
|
||||
}
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(App), root)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<div parent><div parent child></div></div>`
|
||||
)
|
||||
})
|
||||
|
||||
test('should work on slots', () => {
|
||||
const Child = {
|
||||
__scopeId: 'child',
|
||||
render: withChildId(function(this: any) {
|
||||
return h('div', this.$slots.default())
|
||||
})
|
||||
}
|
||||
const App = {
|
||||
__scopeId: 'parent',
|
||||
render: withParentId(() => {
|
||||
return h(
|
||||
Child,
|
||||
withParentId(() => {
|
||||
return h('div')
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(App), root)
|
||||
// slot content should have:
|
||||
// - scopeId from parent
|
||||
// - slotted scopeId (with `-s` postfix) from child (the tree owner)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<div parent child><div parent child-s></div></div>`
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toHandlers } from '../../src/helpers/toHandlers'
|
||||
import { mockWarn } from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('toHandlers', () => {
|
||||
mockWarn()
|
||||
|
||||
148
packages/runtime-core/__tests__/hmr.spec.ts
Normal file
148
packages/runtime-core/__tests__/hmr.spec.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { HMRRuntime } from '../src/hmr'
|
||||
import '../src/hmr'
|
||||
import { ComponentOptions, RenderFunction } from '../src/component'
|
||||
import {
|
||||
render,
|
||||
nodeOps,
|
||||
h,
|
||||
serializeInner,
|
||||
triggerEvent,
|
||||
TestElement,
|
||||
nextTick
|
||||
} from '@vue/runtime-test'
|
||||
import * as runtimeTest from '@vue/runtime-test'
|
||||
import { baseCompile } from '@vue/compiler-core'
|
||||
|
||||
declare var __VUE_HMR_RUNTIME__: HMRRuntime
|
||||
const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__
|
||||
|
||||
function compileToFunction(template: string) {
|
||||
const { code } = baseCompile(template)
|
||||
const render = new Function('Vue', code)(runtimeTest) as RenderFunction
|
||||
render._rc = true // isRuntimeCompiled
|
||||
return render
|
||||
}
|
||||
|
||||
describe('hot module replacement', () => {
|
||||
test('inject global runtime', () => {
|
||||
expect(createRecord).toBeDefined()
|
||||
expect(rerender).toBeDefined()
|
||||
expect(reload).toBeDefined()
|
||||
})
|
||||
|
||||
test('createRecord', () => {
|
||||
expect(createRecord('test1', {})).toBe(true)
|
||||
// if id has already been created, should return false
|
||||
expect(createRecord('test1', {})).toBe(false)
|
||||
})
|
||||
|
||||
test('rerender', async () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
const parentId = 'test2-parent'
|
||||
const childId = 'test2-child'
|
||||
|
||||
const Child: ComponentOptions = {
|
||||
__hmrId: childId,
|
||||
render: compileToFunction(`<slot/>`)
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
|
||||
const Parent: ComponentOptions = {
|
||||
__hmrId: parentId,
|
||||
data() {
|
||||
return { count: 0 }
|
||||
},
|
||||
components: { Child },
|
||||
render: compileToFunction(
|
||||
`<div @click="count++">{{ count }}<Child>{{ count }}</Child></div>`
|
||||
)
|
||||
}
|
||||
createRecord(parentId, Parent)
|
||||
|
||||
render(h(Parent), root)
|
||||
expect(serializeInner(root)).toBe(`<div>00</div>`)
|
||||
|
||||
// Perform some state change. This change should be preserved after the
|
||||
// re-render!
|
||||
triggerEvent(root.children[0] as TestElement, 'click')
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>11</div>`)
|
||||
|
||||
// // Update text while preserving state
|
||||
// rerender(
|
||||
// parentId,
|
||||
// compileToFunction(
|
||||
// `<div @click="count++">{{ count }}!<Child>{{ count }}</Child></div>`
|
||||
// )
|
||||
// )
|
||||
// expect(serializeInner(root)).toBe(`<div>1!1</div>`)
|
||||
|
||||
// Should force child update on slot content change
|
||||
rerender(
|
||||
parentId,
|
||||
compileToFunction(
|
||||
`<div @click="count++">{{ count }}!<Child>{{ count }}!</Child></div>`
|
||||
)
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>1!1!</div>`)
|
||||
|
||||
// Should force update element children despite block optimization
|
||||
rerender(
|
||||
parentId,
|
||||
compileToFunction(
|
||||
`<div @click="count++">{{ count }}<span>{{ count }}</span>
|
||||
<Child>{{ count }}!</Child>
|
||||
</div>`
|
||||
)
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>1<span>1</span>1!</div>`)
|
||||
|
||||
// Should force update child slot elements
|
||||
rerender(
|
||||
parentId,
|
||||
compileToFunction(
|
||||
`<div @click="count++">
|
||||
<Child><span>{{ count }}</span></Child>
|
||||
</div>`
|
||||
)
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div><span>1</span></div>`)
|
||||
})
|
||||
|
||||
test('reload', async () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
const childId = 'test3-child'
|
||||
const unmoutSpy = jest.fn()
|
||||
const mountSpy = jest.fn()
|
||||
|
||||
const Child: ComponentOptions = {
|
||||
__hmrId: childId,
|
||||
data() {
|
||||
return { count: 0 }
|
||||
},
|
||||
unmounted: unmoutSpy,
|
||||
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
|
||||
}
|
||||
createRecord(childId, Child)
|
||||
|
||||
const Parent: ComponentOptions = {
|
||||
render: () => h(Child)
|
||||
}
|
||||
|
||||
render(h(Parent), root)
|
||||
expect(serializeInner(root)).toBe(`<div>0</div>`)
|
||||
|
||||
reload(childId, {
|
||||
__hmrId: childId,
|
||||
data() {
|
||||
return { count: 1 }
|
||||
},
|
||||
mounted: mountSpy,
|
||||
render: compileToFunction(`<div @click="count++">{{ count }}</div>`)
|
||||
})
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>1</div>`)
|
||||
expect(unmoutSpy).toHaveBeenCalledTimes(1)
|
||||
expect(mountSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
660
packages/runtime-core/__tests__/hydration.spec.ts
Normal file
660
packages/runtime-core/__tests__/hydration.spec.ts
Normal file
@@ -0,0 +1,660 @@
|
||||
import {
|
||||
createSSRApp,
|
||||
h,
|
||||
ref,
|
||||
nextTick,
|
||||
VNode,
|
||||
Teleport,
|
||||
createStaticVNode,
|
||||
Suspense,
|
||||
onMounted,
|
||||
defineAsyncComponent,
|
||||
defineComponent
|
||||
} from '@vue/runtime-dom'
|
||||
import { renderToString } from '@vue/server-renderer'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
import { SSRContext } from 'packages/server-renderer/src/renderToString'
|
||||
|
||||
function mountWithHydration(html: string, render: () => any) {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = html
|
||||
const app = createSSRApp({
|
||||
render
|
||||
})
|
||||
return {
|
||||
vnode: app.mount(container).$.subTree as VNode<Node, Element> & {
|
||||
el: Element
|
||||
},
|
||||
container
|
||||
}
|
||||
}
|
||||
|
||||
const triggerEvent = (type: string, el: Element) => {
|
||||
const event = new Event(type)
|
||||
el.dispatchEvent(event)
|
||||
}
|
||||
|
||||
describe('SSR hydration', () => {
|
||||
mockWarn()
|
||||
|
||||
test('text', async () => {
|
||||
const msg = ref('foo')
|
||||
const { vnode, container } = mountWithHydration('foo', () => msg.value)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(container.textContent).toBe('foo')
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(container.textContent).toBe('bar')
|
||||
})
|
||||
|
||||
test('comment', () => {
|
||||
const { vnode, container } = mountWithHydration('<!---->', () => null)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(vnode.el.nodeType).toBe(8) // comment
|
||||
})
|
||||
|
||||
test('static', () => {
|
||||
const html = '<div><span>hello</span></div>'
|
||||
const { vnode, container } = mountWithHydration(html, () =>
|
||||
createStaticVNode(html)
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(vnode.el.outerHTML).toBe(html)
|
||||
})
|
||||
|
||||
test('element with text children', async () => {
|
||||
const msg = ref('foo')
|
||||
const { vnode, container } = mountWithHydration(
|
||||
'<div class="foo">foo</div>',
|
||||
() => h('div', { class: msg.value }, msg.value)
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(container.firstChild!.textContent).toBe('foo')
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
|
||||
})
|
||||
|
||||
test('element with elements children', async () => {
|
||||
const msg = ref('foo')
|
||||
const fn = jest.fn()
|
||||
const { vnode, container } = mountWithHydration(
|
||||
'<div><span>foo</span><span class="foo"></span></div>',
|
||||
() =>
|
||||
h('div', [
|
||||
h('span', msg.value),
|
||||
h('span', { class: msg.value, onClick: fn })
|
||||
])
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect((vnode.children as VNode[])[0].el).toBe(
|
||||
container.firstChild!.childNodes[0]
|
||||
)
|
||||
expect((vnode.children as VNode[])[1].el).toBe(
|
||||
container.firstChild!.childNodes[1]
|
||||
)
|
||||
|
||||
// event handler
|
||||
triggerEvent('click', vnode.el.querySelector('.foo')!)
|
||||
expect(fn).toHaveBeenCalled()
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
|
||||
})
|
||||
|
||||
test('Fragment', async () => {
|
||||
const msg = ref('foo')
|
||||
const fn = jest.fn()
|
||||
const { vnode, container } = mountWithHydration(
|
||||
'<div><!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]--></div>',
|
||||
() =>
|
||||
h('div', [
|
||||
[h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
|
||||
])
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
|
||||
expect(vnode.el.innerHTML).toBe(
|
||||
`<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`
|
||||
)
|
||||
|
||||
// start fragment 1
|
||||
const fragment1 = (vnode.children as VNode[])[0]
|
||||
expect(fragment1.el).toBe(vnode.el.childNodes[0])
|
||||
const fragment1Children = fragment1.children as VNode[]
|
||||
|
||||
// first <span>
|
||||
expect(fragment1Children[0].el!.tagName).toBe('SPAN')
|
||||
expect(fragment1Children[0].el).toBe(vnode.el.childNodes[1])
|
||||
|
||||
// start fragment 2
|
||||
const fragment2 = fragment1Children[1]
|
||||
expect(fragment2.el).toBe(vnode.el.childNodes[2])
|
||||
const fragment2Children = fragment2.children as VNode[]
|
||||
|
||||
// second <span>
|
||||
expect(fragment2Children[0].el!.tagName).toBe('SPAN')
|
||||
expect(fragment2Children[0].el).toBe(vnode.el.childNodes[3])
|
||||
|
||||
// end fragment 2
|
||||
expect(fragment2.anchor).toBe(vnode.el.childNodes[4])
|
||||
|
||||
// end fragment 1
|
||||
expect(fragment1.anchor).toBe(vnode.el.childNodes[5])
|
||||
|
||||
// event handler
|
||||
triggerEvent('click', vnode.el.querySelector('.foo')!)
|
||||
expect(fn).toHaveBeenCalled()
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(vnode.el.innerHTML).toBe(
|
||||
`<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`
|
||||
)
|
||||
})
|
||||
|
||||
test('Teleport', async () => {
|
||||
const msg = ref('foo')
|
||||
const fn = jest.fn()
|
||||
const teleportContainer = document.createElement('div')
|
||||
teleportContainer.id = 'teleport'
|
||||
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!---->`
|
||||
document.body.appendChild(teleportContainer)
|
||||
|
||||
const { vnode, container } = mountWithHydration(
|
||||
'<!--teleport start--><!--teleport end-->',
|
||||
() =>
|
||||
h(Teleport, { to: '#teleport' }, [
|
||||
h('span', msg.value),
|
||||
h('span', { class: msg.value, onClick: fn })
|
||||
])
|
||||
)
|
||||
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(vnode.anchor).toBe(container.lastChild)
|
||||
|
||||
expect(vnode.target).toBe(teleportContainer)
|
||||
expect((vnode.children as VNode[])[0].el).toBe(
|
||||
teleportContainer.childNodes[0]
|
||||
)
|
||||
expect((vnode.children as VNode[])[1].el).toBe(
|
||||
teleportContainer.childNodes[1]
|
||||
)
|
||||
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
|
||||
|
||||
// event handler
|
||||
triggerEvent('click', teleportContainer.querySelector('.foo')!)
|
||||
expect(fn).toHaveBeenCalled()
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(teleportContainer.innerHTML).toBe(
|
||||
`<span>bar</span><span class="bar"></span><!---->`
|
||||
)
|
||||
})
|
||||
|
||||
test('Teleport (multiple + integration)', async () => {
|
||||
const msg = ref('foo')
|
||||
const fn1 = jest.fn()
|
||||
const fn2 = jest.fn()
|
||||
|
||||
const Comp = () => [
|
||||
h(Teleport, { to: '#teleport2' }, [
|
||||
h('span', msg.value),
|
||||
h('span', { class: msg.value, onClick: fn1 })
|
||||
]),
|
||||
h(Teleport, { to: '#teleport2' }, [
|
||||
h('span', msg.value + '2'),
|
||||
h('span', { class: msg.value + '2', onClick: fn2 })
|
||||
])
|
||||
]
|
||||
|
||||
const teleportContainer = document.createElement('div')
|
||||
teleportContainer.id = 'teleport2'
|
||||
const ctx: SSRContext = {}
|
||||
const mainHtml = await renderToString(h(Comp), ctx)
|
||||
expect(mainHtml).toMatchInlineSnapshot(
|
||||
`"<!--[--><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--><!--]-->"`
|
||||
)
|
||||
|
||||
const teleportHtml = ctx.teleports!['#teleport2']
|
||||
expect(teleportHtml).toMatchInlineSnapshot(
|
||||
`"<span>foo</span><span class=\\"foo\\"></span><!----><span>foo2</span><span class=\\"foo2\\"></span><!---->"`
|
||||
)
|
||||
|
||||
teleportContainer.innerHTML = teleportHtml
|
||||
document.body.appendChild(teleportContainer)
|
||||
|
||||
const { vnode, container } = mountWithHydration(mainHtml, Comp)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
const teleportVnode1 = (vnode.children as VNode[])[0]
|
||||
const teleportVnode2 = (vnode.children as VNode[])[1]
|
||||
expect(teleportVnode1.el).toBe(container.childNodes[1])
|
||||
expect(teleportVnode1.anchor).toBe(container.childNodes[2])
|
||||
expect(teleportVnode2.el).toBe(container.childNodes[3])
|
||||
expect(teleportVnode2.anchor).toBe(container.childNodes[4])
|
||||
|
||||
expect(teleportVnode1.target).toBe(teleportContainer)
|
||||
expect((teleportVnode1 as any).children[0].el).toBe(
|
||||
teleportContainer.childNodes[0]
|
||||
)
|
||||
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
|
||||
|
||||
expect(teleportVnode2.target).toBe(teleportContainer)
|
||||
expect((teleportVnode2 as any).children[0].el).toBe(
|
||||
teleportContainer.childNodes[3]
|
||||
)
|
||||
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
|
||||
|
||||
// // event handler
|
||||
triggerEvent('click', teleportContainer.querySelector('.foo')!)
|
||||
expect(fn1).toHaveBeenCalled()
|
||||
|
||||
triggerEvent('click', teleportContainer.querySelector('.foo2')!)
|
||||
expect(fn2).toHaveBeenCalled()
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
|
||||
`"<span>bar</span><span class=\\"bar\\"></span><!----><span>bar2</span><span class=\\"bar2\\"></span><!---->"`
|
||||
)
|
||||
})
|
||||
|
||||
test('Teleport (disabled)', async () => {
|
||||
const msg = ref('foo')
|
||||
const fn1 = jest.fn()
|
||||
const fn2 = jest.fn()
|
||||
|
||||
const Comp = () => [
|
||||
h('div', 'foo'),
|
||||
h(Teleport, { to: '#teleport3', disabled: true }, [
|
||||
h('span', msg.value),
|
||||
h('span', { class: msg.value, onClick: fn1 })
|
||||
]),
|
||||
h('div', { class: msg.value + '2', onClick: fn2 }, 'bar')
|
||||
]
|
||||
|
||||
const teleportContainer = document.createElement('div')
|
||||
teleportContainer.id = 'teleport3'
|
||||
const ctx: SSRContext = {}
|
||||
const mainHtml = await renderToString(h(Comp), ctx)
|
||||
expect(mainHtml).toMatchInlineSnapshot(
|
||||
`"<!--[--><div>foo</div><!--teleport start--><span>foo</span><span class=\\"foo\\"></span><!--teleport end--><div class=\\"foo2\\">bar</div><!--]-->"`
|
||||
)
|
||||
|
||||
const teleportHtml = ctx.teleports!['#teleport3']
|
||||
expect(teleportHtml).toMatchInlineSnapshot(`"<!---->"`)
|
||||
|
||||
teleportContainer.innerHTML = teleportHtml
|
||||
document.body.appendChild(teleportContainer)
|
||||
|
||||
const { vnode, container } = mountWithHydration(mainHtml, Comp)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
const children = vnode.children as VNode[]
|
||||
|
||||
expect(children[0].el).toBe(container.childNodes[1])
|
||||
|
||||
const teleportVnode = children[1]
|
||||
expect(teleportVnode.el).toBe(container.childNodes[2])
|
||||
expect((teleportVnode.children as VNode[])[0].el).toBe(
|
||||
container.childNodes[3]
|
||||
)
|
||||
expect((teleportVnode.children as VNode[])[1].el).toBe(
|
||||
container.childNodes[4]
|
||||
)
|
||||
expect(teleportVnode.anchor).toBe(container.childNodes[5])
|
||||
expect(children[2].el).toBe(container.childNodes[6])
|
||||
|
||||
expect(teleportVnode.target).toBe(teleportContainer)
|
||||
expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
|
||||
|
||||
// // event handler
|
||||
triggerEvent('click', container.querySelector('.foo')!)
|
||||
expect(fn1).toHaveBeenCalled()
|
||||
|
||||
triggerEvent('click', container.querySelector('.foo2')!)
|
||||
expect(fn2).toHaveBeenCalled()
|
||||
|
||||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||
`"<!--[--><div>foo</div><!--teleport start--><span>bar</span><span class=\\"bar\\"></span><!--teleport end--><div class=\\"bar2\\">bar</div><!--]-->"`
|
||||
)
|
||||
})
|
||||
|
||||
// compile SSR + client render fn from the same template & hydrate
|
||||
test('full compiler integration', async () => {
|
||||
const mounted: string[] = []
|
||||
const log = jest.fn()
|
||||
const toggle = ref(true)
|
||||
|
||||
const Child = {
|
||||
data() {
|
||||
return {
|
||||
count: 0,
|
||||
text: 'hello',
|
||||
style: {
|
||||
color: 'red'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mounted.push('child')
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<span class="count" :style="style">{{ count }}</span>
|
||||
<button class="inc" @click="count++">inc</button>
|
||||
<button class="change" @click="style.color = 'green'" >change color</button>
|
||||
<button class="emit" @click="$emit('foo')">emit</button>
|
||||
<span class="text">{{ text }}</span>
|
||||
<input v-model="text">
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const App = {
|
||||
setup() {
|
||||
return { toggle }
|
||||
},
|
||||
mounted() {
|
||||
mounted.push('parent')
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<span>hello</span>
|
||||
<template v-if="toggle">
|
||||
<Child @foo="log('child')"/>
|
||||
<template v-if="true">
|
||||
<button class="parent-click" @click="log('click')">click me</button>
|
||||
</template>
|
||||
</template>
|
||||
<span>hello</span>
|
||||
</div>`,
|
||||
components: {
|
||||
Child
|
||||
},
|
||||
methods: {
|
||||
log
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div')
|
||||
// server render
|
||||
container.innerHTML = await renderToString(h(App))
|
||||
// hydrate
|
||||
createSSRApp(App).mount(container)
|
||||
|
||||
// assert interactions
|
||||
// 1. parent button click
|
||||
triggerEvent('click', container.querySelector('.parent-click')!)
|
||||
expect(log).toHaveBeenCalledWith('click')
|
||||
|
||||
// 2. child inc click + text interpolation
|
||||
const count = container.querySelector('.count') as HTMLElement
|
||||
expect(count.textContent).toBe(`0`)
|
||||
triggerEvent('click', container.querySelector('.inc')!)
|
||||
await nextTick()
|
||||
expect(count.textContent).toBe(`1`)
|
||||
|
||||
// 3. child color click + style binding
|
||||
expect(count.style.color).toBe('red')
|
||||
triggerEvent('click', container.querySelector('.change')!)
|
||||
await nextTick()
|
||||
expect(count.style.color).toBe('green')
|
||||
|
||||
// 4. child event emit
|
||||
triggerEvent('click', container.querySelector('.emit')!)
|
||||
expect(log).toHaveBeenCalledWith('child')
|
||||
|
||||
// 5. child v-model
|
||||
const text = container.querySelector('.text')!
|
||||
const input = container.querySelector('input')!
|
||||
expect(text.textContent).toBe('hello')
|
||||
input.value = 'bye'
|
||||
triggerEvent('input', input)
|
||||
await nextTick()
|
||||
expect(text.textContent).toBe('bye')
|
||||
})
|
||||
|
||||
test('Suspense', async () => {
|
||||
const AsyncChild = {
|
||||
async setup() {
|
||||
const count = ref(0)
|
||||
return () =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
onClick: () => {
|
||||
count.value++
|
||||
}
|
||||
},
|
||||
count.value
|
||||
)
|
||||
}
|
||||
}
|
||||
const { vnode, container } = mountWithHydration('<span>0</span>', () =>
|
||||
h(Suspense, () => h(AsyncChild))
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
// wait for hydration to finish
|
||||
await new Promise(r => setTimeout(r))
|
||||
triggerEvent('click', container.querySelector('span')!)
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe(`<span>1</span>`)
|
||||
})
|
||||
|
||||
test('Suspense (full integration)', async () => {
|
||||
const mountedCalls: number[] = []
|
||||
const asyncDeps: Promise<any>[] = []
|
||||
|
||||
const AsyncChild = defineComponent({
|
||||
props: ['n'],
|
||||
async setup(props) {
|
||||
const count = ref(props.n)
|
||||
onMounted(() => {
|
||||
mountedCalls.push(props.n)
|
||||
})
|
||||
const p = new Promise(r => setTimeout(r, props.n * 10))
|
||||
asyncDeps.push(p)
|
||||
await p
|
||||
return () =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
onClick: () => {
|
||||
count.value++
|
||||
}
|
||||
},
|
||||
count.value
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const done = jest.fn()
|
||||
const App = {
|
||||
template: `
|
||||
<Suspense @resolve="done">
|
||||
<AsyncChild :n="1" />
|
||||
<AsyncChild :n="2" />
|
||||
</Suspense>`,
|
||||
components: {
|
||||
AsyncChild
|
||||
},
|
||||
methods: {
|
||||
done
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div')
|
||||
// server render
|
||||
container.innerHTML = await renderToString(h(App))
|
||||
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||
`"<!--[--><span>1</span><span>2</span><!--]-->"`
|
||||
)
|
||||
// reset asyncDeps from ssr
|
||||
asyncDeps.length = 0
|
||||
// hydrate
|
||||
createSSRApp(App).mount(container)
|
||||
|
||||
expect(mountedCalls.length).toBe(0)
|
||||
expect(asyncDeps.length).toBe(2)
|
||||
|
||||
// wait for hydration to complete
|
||||
await Promise.all(asyncDeps)
|
||||
await new Promise(r => setTimeout(r))
|
||||
|
||||
// should flush buffered effects
|
||||
expect(mountedCalls).toMatchObject([1, 2])
|
||||
expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`)
|
||||
|
||||
const span1 = container.querySelector('span')!
|
||||
triggerEvent('click', span1)
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toMatch(`<span>2</span><span>2</span>`)
|
||||
|
||||
const span2 = span1.nextSibling as Element
|
||||
triggerEvent('click', span2)
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`)
|
||||
})
|
||||
|
||||
test('async component', async () => {
|
||||
const spy = jest.fn()
|
||||
const Comp = () =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
onClick: spy
|
||||
},
|
||||
'hello!'
|
||||
)
|
||||
|
||||
let serverResolve: any
|
||||
let AsyncComp = defineAsyncComponent(
|
||||
() =>
|
||||
new Promise(r => {
|
||||
serverResolve = r
|
||||
})
|
||||
)
|
||||
|
||||
const App = {
|
||||
render() {
|
||||
return ['hello', h(AsyncComp), 'world']
|
||||
}
|
||||
}
|
||||
|
||||
// server render
|
||||
const htmlPromise = renderToString(h(App))
|
||||
serverResolve(Comp)
|
||||
const html = await htmlPromise
|
||||
expect(html).toMatchInlineSnapshot(
|
||||
`"<!--[-->hello<button>hello!</button>world<!--]-->"`
|
||||
)
|
||||
|
||||
// hydration
|
||||
let clientResolve: any
|
||||
AsyncComp = defineAsyncComponent(
|
||||
() =>
|
||||
new Promise(r => {
|
||||
clientResolve = r
|
||||
})
|
||||
)
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = html
|
||||
createSSRApp(App).mount(container)
|
||||
|
||||
// hydration not complete yet
|
||||
triggerEvent('click', container.querySelector('button')!)
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
// resolve
|
||||
clientResolve(Comp)
|
||||
await new Promise(r => setTimeout(r))
|
||||
|
||||
// should be hydrated now
|
||||
triggerEvent('click', container.querySelector('button')!)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('mismatch handling', () => {
|
||||
test('text node', () => {
|
||||
const { container } = mountWithHydration(`foo`, () => 'bar')
|
||||
expect(container.textContent).toBe('bar')
|
||||
expect(`Hydration text mismatch`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('element text content', () => {
|
||||
const { container } = mountWithHydration(`<div>foo</div>`, () =>
|
||||
h('div', 'bar')
|
||||
)
|
||||
expect(container.innerHTML).toBe('<div>bar</div>')
|
||||
expect(`Hydration text content mismatch in <div>`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('not enough children', () => {
|
||||
const { container } = mountWithHydration(`<div></div>`, () =>
|
||||
h('div', [h('span', 'foo'), h('span', 'bar')])
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><span>foo</span><span>bar</span></div>'
|
||||
)
|
||||
expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('too many children', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div><span>foo</span><span>bar</span></div>`,
|
||||
() => h('div', [h('span', 'foo')])
|
||||
)
|
||||
expect(container.innerHTML).toBe('<div><span>foo</span></div>')
|
||||
expect(`Hydration children mismatch in <div>`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('complete mismatch', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div><span>foo</span><span>bar</span></div>`,
|
||||
() => h('div', [h('div', 'foo'), h('p', 'bar')])
|
||||
)
|
||||
expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
|
||||
expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
|
||||
})
|
||||
|
||||
test('fragment mismatch removal', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
|
||||
() => h('div', [h('span', 'replaced')])
|
||||
)
|
||||
expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
|
||||
expect(`Hydration node mismatch`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('fragment not enough children', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
|
||||
() => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')])
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>'
|
||||
)
|
||||
expect(`Hydration node mismatch`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('fragment too many children', () => {
|
||||
const { container } = mountWithHydration(
|
||||
`<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
|
||||
() => h('div', [[h('div', 'foo')], h('div', 'baz')])
|
||||
)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--[--><div>foo</div><!--]--><div>baz</div></div>'
|
||||
)
|
||||
// fragment ends early and attempts to hydrate the extra <div>bar</div>
|
||||
// as 2nd fragment child.
|
||||
expect(`Hydration text content mismatch`).toHaveBeenWarned()
|
||||
// exccesive children removal
|
||||
expect(`Hydration children mismatch`).toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,14 +6,18 @@ import {
|
||||
mergeProps,
|
||||
ref,
|
||||
onUpdated,
|
||||
createComponent
|
||||
defineComponent,
|
||||
openBlock,
|
||||
createBlock,
|
||||
FunctionalComponent,
|
||||
createCommentVNode
|
||||
} from '@vue/runtime-dom'
|
||||
import { mockWarn } from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('attribute fallthrough', () => {
|
||||
mockWarn()
|
||||
|
||||
it('everything should be in props when component has no declared props', async () => {
|
||||
it('should allow attrs to fallthrough', async () => {
|
||||
const click = jest.fn()
|
||||
const childUpdated = jest.fn()
|
||||
|
||||
@@ -28,85 +32,18 @@ describe('attribute fallthrough', () => {
|
||||
|
||||
return () =>
|
||||
h(Child, {
|
||||
foo: 1,
|
||||
foo: count.value + 1,
|
||||
id: 'test',
|
||||
class: 'c' + count.value,
|
||||
style: { color: count.value ? 'red' : 'green' },
|
||||
onClick: inc
|
||||
onClick: inc,
|
||||
'data-id': count.value + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup(props: any) {
|
||||
onUpdated(childUpdated)
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
mergeProps(
|
||||
{
|
||||
class: 'c2',
|
||||
style: { fontWeight: 'bold' }
|
||||
},
|
||||
props
|
||||
),
|
||||
props.foo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(Hello), root)
|
||||
|
||||
const node = root.children[0] as HTMLElement
|
||||
|
||||
expect(node.getAttribute('id')).toBe('test')
|
||||
expect(node.getAttribute('foo')).toBe('1')
|
||||
expect(node.getAttribute('class')).toBe('c2 c0')
|
||||
expect(node.style.color).toBe('green')
|
||||
expect(node.style.fontWeight).toBe('bold')
|
||||
node.dispatchEvent(new CustomEvent('click'))
|
||||
expect(click).toHaveBeenCalled()
|
||||
|
||||
await nextTick()
|
||||
expect(childUpdated).toHaveBeenCalled()
|
||||
expect(node.getAttribute('id')).toBe('test')
|
||||
expect(node.getAttribute('foo')).toBe('1')
|
||||
expect(node.getAttribute('class')).toBe('c2 c1')
|
||||
expect(node.style.color).toBe('red')
|
||||
expect(node.style.fontWeight).toBe('bold')
|
||||
})
|
||||
|
||||
it('should implicitly fallthrough on single root nodes', async () => {
|
||||
const click = jest.fn()
|
||||
const childUpdated = jest.fn()
|
||||
|
||||
const Hello = {
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
|
||||
function inc() {
|
||||
count.value++
|
||||
click()
|
||||
}
|
||||
|
||||
return () =>
|
||||
h(Child, {
|
||||
foo: 1,
|
||||
id: 'test',
|
||||
class: 'c' + count.value,
|
||||
style: { color: count.value ? 'red' : 'green' },
|
||||
onClick: inc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const Child = createComponent({
|
||||
props: {
|
||||
foo: Number
|
||||
},
|
||||
setup(props) {
|
||||
onUpdated(childUpdated)
|
||||
return () =>
|
||||
h(
|
||||
@@ -118,7 +55,7 @@ describe('attribute fallthrough', () => {
|
||||
props.foo
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
@@ -126,25 +63,137 @@ describe('attribute fallthrough', () => {
|
||||
|
||||
const node = root.children[0] as HTMLElement
|
||||
|
||||
// with declared props, any parent attr that isn't a prop falls through
|
||||
expect(node.getAttribute('id')).toBe('test')
|
||||
expect(node.getAttribute('foo')).toBe('1')
|
||||
expect(node.getAttribute('class')).toBe('c2 c0')
|
||||
expect(node.style.color).toBe('green')
|
||||
expect(node.style.fontWeight).toBe('bold')
|
||||
expect(node.dataset.id).toBe('1')
|
||||
node.dispatchEvent(new CustomEvent('click'))
|
||||
expect(click).toHaveBeenCalled()
|
||||
|
||||
await nextTick()
|
||||
expect(childUpdated).toHaveBeenCalled()
|
||||
expect(node.getAttribute('id')).toBe('test')
|
||||
expect(node.getAttribute('foo')).toBe('2')
|
||||
expect(node.getAttribute('class')).toBe('c2 c1')
|
||||
expect(node.style.color).toBe('red')
|
||||
expect(node.style.fontWeight).toBe('bold')
|
||||
expect(node.dataset.id).toBe('2')
|
||||
})
|
||||
|
||||
it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
|
||||
const click = jest.fn()
|
||||
const childUpdated = jest.fn()
|
||||
|
||||
const count = ref(0)
|
||||
|
||||
function inc() {
|
||||
count.value++
|
||||
click()
|
||||
}
|
||||
|
||||
const Hello = () =>
|
||||
h(Child, {
|
||||
foo: count.value + 1,
|
||||
id: 'test',
|
||||
class: 'c' + count.value,
|
||||
style: { color: count.value ? 'red' : 'green' },
|
||||
onClick: inc
|
||||
})
|
||||
|
||||
const Child = (props: any) => {
|
||||
childUpdated()
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: 'c2',
|
||||
style: { fontWeight: 'bold' }
|
||||
},
|
||||
props.foo
|
||||
)
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(Hello), root)
|
||||
|
||||
const node = root.children[0] as HTMLElement
|
||||
|
||||
// not whitelisted
|
||||
expect(node.getAttribute('id')).toBe(null)
|
||||
expect(node.getAttribute('foo')).toBe(null)
|
||||
|
||||
// whitelisted: style, class, event listeners
|
||||
expect(node.getAttribute('class')).toBe('c2 c0')
|
||||
expect(node.style.color).toBe('green')
|
||||
expect(node.style.fontWeight).toBe('bold')
|
||||
node.dispatchEvent(new CustomEvent('click'))
|
||||
expect(click).toHaveBeenCalled()
|
||||
|
||||
// ...while declared ones remain props
|
||||
expect(node.hasAttribute('foo')).toBe(false)
|
||||
await nextTick()
|
||||
expect(childUpdated).toHaveBeenCalled()
|
||||
expect(node.getAttribute('id')).toBe(null)
|
||||
expect(node.getAttribute('foo')).toBe(null)
|
||||
expect(node.getAttribute('class')).toBe('c2 c1')
|
||||
expect(node.style.color).toBe('red')
|
||||
expect(node.style.fontWeight).toBe('bold')
|
||||
})
|
||||
|
||||
it('should allow all attrs on functional component with declared props', async () => {
|
||||
const click = jest.fn()
|
||||
const childUpdated = jest.fn()
|
||||
|
||||
const count = ref(0)
|
||||
|
||||
function inc() {
|
||||
count.value++
|
||||
click()
|
||||
}
|
||||
|
||||
const Hello = () =>
|
||||
h(Child, {
|
||||
foo: count.value + 1,
|
||||
id: 'test',
|
||||
class: 'c' + count.value,
|
||||
style: { color: count.value ? 'red' : 'green' },
|
||||
onClick: inc
|
||||
})
|
||||
|
||||
const Child = (props: { foo: number }) => {
|
||||
childUpdated()
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: 'c2',
|
||||
style: { fontWeight: 'bold' }
|
||||
},
|
||||
props.foo
|
||||
)
|
||||
}
|
||||
Child.props = ['foo']
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(Hello), root)
|
||||
|
||||
const node = root.children[0] as HTMLElement
|
||||
|
||||
expect(node.getAttribute('id')).toBe('test')
|
||||
expect(node.getAttribute('foo')).toBe(null) // declared as prop
|
||||
expect(node.getAttribute('class')).toBe('c2 c0')
|
||||
expect(node.style.color).toBe('green')
|
||||
expect(node.style.fontWeight).toBe('bold')
|
||||
node.dispatchEvent(new CustomEvent('click'))
|
||||
expect(click).toHaveBeenCalled()
|
||||
|
||||
await nextTick()
|
||||
expect(childUpdated).toHaveBeenCalled()
|
||||
expect(node.getAttribute('id')).toBe('test')
|
||||
expect(node.getAttribute('foo')).toBe(null)
|
||||
expect(node.getAttribute('class')).toBe('c2 c1')
|
||||
expect(node.style.color).toBe('red')
|
||||
expect(node.style.fontWeight).toBe('bold')
|
||||
|
||||
expect(node.hasAttribute('foo')).toBe(false)
|
||||
})
|
||||
|
||||
it('should fallthrough for nested components', async () => {
|
||||
@@ -175,12 +224,16 @@ describe('attribute fallthrough', () => {
|
||||
const Child = {
|
||||
setup(props: any) {
|
||||
onUpdated(childUpdated)
|
||||
// HOC simply passing props down.
|
||||
// this will result in merging the same attrs, but should be deduped by
|
||||
// `mergeProps`.
|
||||
return () => h(GrandChild, props)
|
||||
}
|
||||
}
|
||||
|
||||
const GrandChild = createComponent({
|
||||
const GrandChild = defineComponent({
|
||||
props: {
|
||||
id: String,
|
||||
foo: Number
|
||||
},
|
||||
setup(props) {
|
||||
@@ -189,6 +242,7 @@ describe('attribute fallthrough', () => {
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
id: props.id,
|
||||
class: 'c2',
|
||||
style: { fontWeight: 'bold' }
|
||||
},
|
||||
@@ -232,7 +286,7 @@ describe('attribute fallthrough', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const Child = createComponent({
|
||||
const Child = defineComponent({
|
||||
props: ['foo'],
|
||||
inheritAttrs: false,
|
||||
render() {
|
||||
@@ -255,7 +309,7 @@ describe('attribute fallthrough', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const Child = createComponent({
|
||||
const Child = defineComponent({
|
||||
props: ['foo'],
|
||||
inheritAttrs: false,
|
||||
render() {
|
||||
@@ -287,7 +341,7 @@ describe('attribute fallthrough', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const Child = createComponent({
|
||||
const Child = defineComponent({
|
||||
props: ['foo'],
|
||||
render() {
|
||||
return [h('div'), h('div')]
|
||||
@@ -308,7 +362,7 @@ describe('attribute fallthrough', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const Child = createComponent({
|
||||
const Child = defineComponent({
|
||||
props: ['foo'],
|
||||
render() {
|
||||
return [h('div'), h('div', this.$attrs)]
|
||||
@@ -320,8 +374,157 @@ describe('attribute fallthrough', () => {
|
||||
render(h(Parent), root)
|
||||
|
||||
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
|
||||
expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
|
||||
})
|
||||
|
||||
it('should not warn when context.attrs is used during render', () => {
|
||||
const Parent = {
|
||||
render() {
|
||||
return h(Child, { foo: 1, class: 'parent' })
|
||||
}
|
||||
}
|
||||
|
||||
const Child = defineComponent({
|
||||
props: ['foo'],
|
||||
setup(_props, { attrs }) {
|
||||
return () => [h('div'), h('div', attrs)]
|
||||
}
|
||||
})
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(Parent), root)
|
||||
|
||||
expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
|
||||
expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
|
||||
})
|
||||
|
||||
// #677
|
||||
it('should update merged dynamic attrs on optimized child root', async () => {
|
||||
const aria = ref('true')
|
||||
const cls = ref('bar')
|
||||
const Parent = {
|
||||
render() {
|
||||
return h(Child, { 'aria-hidden': aria.value, class: cls.value })
|
||||
}
|
||||
}
|
||||
|
||||
const Child = {
|
||||
props: [],
|
||||
render() {
|
||||
return openBlock(), createBlock('div')
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(Parent), root)
|
||||
|
||||
expect(root.innerHTML).toBe(`<div aria-hidden="true" class="bar"></div>`)
|
||||
|
||||
aria.value = 'false'
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="bar"></div>`)
|
||||
|
||||
cls.value = 'barr'
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
|
||||
})
|
||||
|
||||
it('should not let listener fallthrough when declared in emits (stateful)', () => {
|
||||
const Child = defineComponent({
|
||||
emits: ['click'],
|
||||
render() {
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
onClick: () => {
|
||||
this.$emit('click', 'custom')
|
||||
}
|
||||
},
|
||||
'hello'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const onClick = jest.fn()
|
||||
const App = {
|
||||
render() {
|
||||
return h(Child, {
|
||||
onClick
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(App), root)
|
||||
|
||||
const node = root.children[0] as HTMLElement
|
||||
node.dispatchEvent(new CustomEvent('click'))
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
expect(onClick).toHaveBeenCalledWith('custom')
|
||||
})
|
||||
|
||||
it('should not let listener fallthrough when declared in emits (functional)', () => {
|
||||
const Child: FunctionalComponent<{}, { click: any }> = (_, { emit }) => {
|
||||
// should not be in props
|
||||
expect((_ as any).onClick).toBeUndefined()
|
||||
return h('button', {
|
||||
onClick: () => {
|
||||
emit('click', 'custom')
|
||||
}
|
||||
})
|
||||
}
|
||||
Child.emits = ['click']
|
||||
|
||||
const onClick = jest.fn()
|
||||
const App = {
|
||||
render() {
|
||||
return h(Child, {
|
||||
onClick
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(App), root)
|
||||
|
||||
const node = root.children[0] as HTMLElement
|
||||
node.dispatchEvent(new CustomEvent('click'))
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
expect(onClick).toHaveBeenCalledWith('custom')
|
||||
})
|
||||
|
||||
it('should support fallthrough for fragments with single element + comments', () => {
|
||||
const click = jest.fn()
|
||||
|
||||
const Hello = {
|
||||
setup() {
|
||||
return () => h(Child, { class: 'foo', onClick: click })
|
||||
}
|
||||
}
|
||||
|
||||
const Child = {
|
||||
setup(props: any) {
|
||||
return () => [
|
||||
createCommentVNode('hello'),
|
||||
h('button'),
|
||||
createCommentVNode('world')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(Hello), root)
|
||||
|
||||
expect(root.innerHTML).toBe(
|
||||
`<!----><div></div><div class="parent"></div><!---->`
|
||||
`<!--hello--><button class="foo"></button><!--world-->`
|
||||
)
|
||||
const button = root.children[0] as HTMLElement
|
||||
button.dispatchEvent(new CustomEvent('click'))
|
||||
expect(click).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
NodeTypes,
|
||||
TestElement,
|
||||
serialize,
|
||||
serializeInner,
|
||||
mockWarn
|
||||
serializeInner
|
||||
} from '@vue/runtime-test'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
mockWarn()
|
||||
|
||||
|
||||
@@ -1,9 +1,51 @@
|
||||
import {
|
||||
h,
|
||||
render,
|
||||
nodeOps,
|
||||
TestElement,
|
||||
serializeInner as inner
|
||||
} from '@vue/runtime-test'
|
||||
|
||||
describe('renderer: element', () => {
|
||||
test.todo('with props')
|
||||
let root: TestElement
|
||||
|
||||
test.todo('with direct text children')
|
||||
beforeEach(() => {
|
||||
root = nodeOps.createElement('div')
|
||||
})
|
||||
|
||||
test.todo('with text node children')
|
||||
it('should create an element', () => {
|
||||
render(h('div'), root)
|
||||
expect(inner(root)).toBe('<div></div>')
|
||||
})
|
||||
|
||||
test.todo('handle already mounted VNode')
|
||||
it('should create an element with props', () => {
|
||||
render(h('div', { id: 'foo', class: 'bar' }), root)
|
||||
expect(inner(root)).toBe('<div id="foo" class="bar"></div>')
|
||||
})
|
||||
|
||||
it('should create an element with direct text children', () => {
|
||||
render(h('div', ['foo', ' ', 'bar']), root)
|
||||
expect(inner(root)).toBe('<div>foo bar</div>')
|
||||
})
|
||||
|
||||
it('should create an element with direct text children and props', () => {
|
||||
render(h('div', { id: 'foo' }, ['bar']), root)
|
||||
expect(inner(root)).toBe('<div id="foo">bar</div>')
|
||||
})
|
||||
|
||||
it('should update an element tag which is already mounted', () => {
|
||||
render(h('div', ['foo']), root)
|
||||
expect(inner(root)).toBe('<div>foo</div>')
|
||||
|
||||
render(h('span', ['foo']), root)
|
||||
expect(inner(root)).toBe('<span>foo</span>')
|
||||
})
|
||||
|
||||
it('should update element props which is already mounted', () => {
|
||||
render(h('div', { id: 'bar' }, ['foo']), root)
|
||||
expect(inner(root)).toBe('<div id="bar">foo</div>')
|
||||
|
||||
render(h('div', { id: 'baz', class: 'bar' }, ['foo']), root)
|
||||
expect(inner(root)).toBe('<div id="baz" class="bar">foo</div>')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
NodeTypes,
|
||||
TestElement,
|
||||
Fragment,
|
||||
PatchFlags,
|
||||
resetOps,
|
||||
dumpOps,
|
||||
NodeOpTypes,
|
||||
serializeInner,
|
||||
createTextVNode
|
||||
} from '@vue/runtime-test'
|
||||
import { PatchFlags } from '@vue/shared'
|
||||
|
||||
describe('renderer: fragment', () => {
|
||||
it('should allow returning multiple component root nodes', () => {
|
||||
@@ -25,10 +25,11 @@ describe('renderer: fragment', () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(App), root)
|
||||
|
||||
expect(serializeInner(root)).toBe(`<!----><div>one</div>two<!---->`)
|
||||
expect(serializeInner(root)).toBe(`<div>one</div>two`)
|
||||
expect(root.children.length).toBe(4)
|
||||
expect(root.children[0]).toMatchObject({
|
||||
type: NodeTypes.COMMENT
|
||||
type: NodeTypes.TEXT,
|
||||
text: ''
|
||||
})
|
||||
expect(root.children[1]).toMatchObject({
|
||||
type: NodeTypes.ELEMENT,
|
||||
@@ -43,7 +44,8 @@ describe('renderer: fragment', () => {
|
||||
text: 'two'
|
||||
})
|
||||
expect(root.children[3]).toMatchObject({
|
||||
type: NodeTypes.COMMENT
|
||||
type: NodeTypes.TEXT,
|
||||
text: ''
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,7 +53,7 @@ describe('renderer: fragment', () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h('div', [h(Fragment, [h('div', 'one'), 'two'])]), root)
|
||||
const parent = root.children[0] as TestElement
|
||||
expect(serializeInner(parent)).toBe(`<!----><div>one</div>two<!---->`)
|
||||
expect(serializeInner(parent)).toBe(`<div>one</div>two`)
|
||||
})
|
||||
|
||||
it('patch fragment children (manual, keyed)', () => {
|
||||
@@ -60,18 +62,14 @@ describe('renderer: fragment', () => {
|
||||
h(Fragment, [h('div', { key: 1 }, 'one'), h('div', { key: 2 }, 'two')]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>one</div><div>two</div><!---->`
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
|
||||
|
||||
resetOps()
|
||||
render(
|
||||
h(Fragment, [h('div', { key: 2 }, 'two'), h('div', { key: 1 }, 'one')]),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>two</div><div>one</div><!---->`
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
|
||||
const ops = dumpOps()
|
||||
// should be moving nodes instead of re-creating or patching them
|
||||
expect(ops).toMatchObject([
|
||||
@@ -84,15 +82,11 @@ describe('renderer: fragment', () => {
|
||||
it('patch fragment children (manual, unkeyed)', () => {
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Fragment, [h('div', 'one'), h('div', 'two')]), root)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>one</div><div>two</div><!---->`
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
|
||||
|
||||
resetOps()
|
||||
render(h(Fragment, [h('div', 'two'), h('div', 'one')]), root)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>two</div><div>one</div><!---->`
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
|
||||
const ops = dumpOps()
|
||||
// should be patching nodes instead of moving or re-creating them
|
||||
expect(ops).toMatchObject([
|
||||
@@ -119,7 +113,7 @@ describe('renderer: fragment', () => {
|
||||
),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<!----><div>one</div>two<!---->`)
|
||||
expect(serializeInner(root)).toBe(`<div>one</div>two`)
|
||||
|
||||
render(
|
||||
createVNode(
|
||||
@@ -134,7 +128,7 @@ describe('renderer: fragment', () => {
|
||||
),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<!----><div>foo</div>barbaz<!---->`)
|
||||
expect(serializeInner(root)).toBe(`<div>foo</div>barbaz`)
|
||||
})
|
||||
|
||||
it('patch fragment children (compiler generated, keyed)', () => {
|
||||
@@ -149,9 +143,7 @@ describe('renderer: fragment', () => {
|
||||
),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>one</div><div>two</div><!---->`
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>one</div><div>two</div>`)
|
||||
|
||||
resetOps()
|
||||
render(
|
||||
@@ -163,9 +155,7 @@ describe('renderer: fragment', () => {
|
||||
),
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>two</div><div>one</div><!---->`
|
||||
)
|
||||
expect(serializeInner(root)).toBe(`<div>two</div><div>one</div>`)
|
||||
const ops = dumpOps()
|
||||
// should be moving nodes instead of re-creating or patching them
|
||||
expect(ops).toMatchObject([
|
||||
@@ -188,7 +178,7 @@ describe('renderer: fragment', () => {
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<div><div>outer</div><!----><div>one</div><div>two</div><!----></div>`
|
||||
`<div><div>outer</div><div>one</div><div>two</div></div>`
|
||||
)
|
||||
|
||||
resetOps()
|
||||
@@ -203,7 +193,7 @@ describe('renderer: fragment', () => {
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<div><!----><div>two</div><div>one</div><!----><div>outer</div></div>`
|
||||
`<div><div>two</div><div>one</div><div>outer</div></div>`
|
||||
)
|
||||
const ops = dumpOps()
|
||||
// should be moving nodes instead of re-creating them
|
||||
@@ -213,10 +203,10 @@ describe('renderer: fragment', () => {
|
||||
// 2. move entire fragment, including anchors
|
||||
// not the most efficient move, but this case is super rare
|
||||
// and optimizing for this special case complicates the algo quite a bit
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } }
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
|
||||
])
|
||||
})
|
||||
|
||||
@@ -234,7 +224,7 @@ describe('renderer: fragment', () => {
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>outer</div><!----><div>one</div><div>two</div><!----><!---->`
|
||||
`<div>outer</div><div>one</div><div>two</div>`
|
||||
)
|
||||
|
||||
resetOps()
|
||||
@@ -249,16 +239,16 @@ describe('renderer: fragment', () => {
|
||||
root
|
||||
)
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><!----><div>two</div><div>one</div><!----><div>outer</div><!---->`
|
||||
`<div>two</div><div>one</div><div>outer</div>`
|
||||
)
|
||||
const ops = dumpOps()
|
||||
// should be moving nodes instead of re-creating them
|
||||
expect(ops).toMatchObject([
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'element' } },
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'comment' } }
|
||||
{ type: NodeOpTypes.INSERT, targetNode: { type: 'text', text: '' } }
|
||||
])
|
||||
|
||||
// should properly remove nested fragments
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import {
|
||||
nodeOps,
|
||||
serializeInner,
|
||||
render,
|
||||
h,
|
||||
createComponent,
|
||||
Portal,
|
||||
Text,
|
||||
Fragment,
|
||||
ref,
|
||||
nextTick,
|
||||
TestElement,
|
||||
TestNode
|
||||
} from '@vue/runtime-test'
|
||||
import { VNodeChildren } from '../src/vnode'
|
||||
|
||||
describe('renderer: portal', () => {
|
||||
test('should work', () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
const Comp = createComponent(() => () =>
|
||||
h(Fragment, [
|
||||
h(Portal, { target }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
])
|
||||
)
|
||||
render(h(Comp), root)
|
||||
|
||||
expect(serializeInner(root)).toMatchSnapshot()
|
||||
expect(serializeInner(target)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should update target', async () => {
|
||||
const targetA = nodeOps.createElement('div')
|
||||
const targetB = nodeOps.createElement('div')
|
||||
const target = ref(targetA)
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
const Comp = createComponent(() => () =>
|
||||
h(Fragment, [
|
||||
h(Portal, { target: target.value }, h('div', 'teleported')),
|
||||
h('div', 'root')
|
||||
])
|
||||
)
|
||||
render(h(Comp), root)
|
||||
|
||||
expect(serializeInner(root)).toMatchSnapshot()
|
||||
expect(serializeInner(targetA)).toMatchSnapshot()
|
||||
expect(serializeInner(targetB)).toMatchSnapshot()
|
||||
|
||||
target.value = targetB
|
||||
await nextTick()
|
||||
|
||||
expect(serializeInner(root)).toMatchSnapshot()
|
||||
expect(serializeInner(targetA)).toMatchSnapshot()
|
||||
expect(serializeInner(targetB)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should update children', async () => {
|
||||
const target = nodeOps.createElement('div')
|
||||
const root = nodeOps.createElement('div')
|
||||
const children = ref<VNodeChildren<TestNode, TestElement>>([
|
||||
h('div', 'teleported')
|
||||
])
|
||||
|
||||
const Comp = createComponent(() => () =>
|
||||
h(Portal, { target }, children.value)
|
||||
)
|
||||
render(h(Comp), root)
|
||||
|
||||
expect(serializeInner(target)).toMatchSnapshot()
|
||||
|
||||
children.value = []
|
||||
await nextTick()
|
||||
|
||||
expect(serializeInner(target)).toMatchSnapshot()
|
||||
|
||||
children.value = [h(Text, 'teleported')]
|
||||
await nextTick()
|
||||
|
||||
expect(serializeInner(target)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,9 @@
|
||||
import { queueJob, nextTick, queuePostFlushCb } from '../src/scheduler'
|
||||
import {
|
||||
queueJob,
|
||||
nextTick,
|
||||
queuePostFlushCb,
|
||||
invalidateJob
|
||||
} from '../src/scheduler'
|
||||
|
||||
describe('scheduler', () => {
|
||||
it('nextTick', async () => {
|
||||
@@ -230,4 +235,31 @@ describe('scheduler', () => {
|
||||
expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
|
||||
})
|
||||
})
|
||||
|
||||
test('invalidateJob', async () => {
|
||||
const calls: string[] = []
|
||||
const job1 = () => {
|
||||
calls.push('job1')
|
||||
invalidateJob(job2)
|
||||
job2()
|
||||
}
|
||||
const job2 = () => {
|
||||
calls.push('job2')
|
||||
}
|
||||
const job3 = () => {
|
||||
calls.push('job3')
|
||||
}
|
||||
const job4 = () => {
|
||||
calls.push('job4')
|
||||
}
|
||||
// queue all jobs
|
||||
queueJob(job1)
|
||||
queueJob(job2)
|
||||
queueJob(job3)
|
||||
queuePostFlushCb(job4)
|
||||
expect(calls).toEqual([])
|
||||
await nextTick()
|
||||
// job2 should be called only once
|
||||
expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { createVNode } from '@vue/runtime-test'
|
||||
import {
|
||||
ShapeFlags,
|
||||
createBlock,
|
||||
createVNode,
|
||||
openBlock,
|
||||
Comment,
|
||||
Fragment,
|
||||
Text,
|
||||
cloneVNode
|
||||
} from '@vue/runtime-core'
|
||||
import { mergeProps, normalizeVNode } from '../src/vnode'
|
||||
cloneVNode,
|
||||
mergeProps,
|
||||
normalizeVNode,
|
||||
transformVNodeArgs
|
||||
} from '../src/vnode'
|
||||
import { Data } from '../src/component'
|
||||
import { ShapeFlags, PatchFlags } from '@vue/shared'
|
||||
import { h } from '../src'
|
||||
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
|
||||
|
||||
describe('vnode', () => {
|
||||
test('create with just tag', () => {
|
||||
@@ -35,6 +41,23 @@ describe('vnode', () => {
|
||||
expect(vnode.props).toBe(null)
|
||||
})
|
||||
|
||||
test('vnode keys', () => {
|
||||
for (const key of ['', 'a', 0, 1, NaN]) {
|
||||
expect(createVNode('div', { key }).key).toBe(key)
|
||||
}
|
||||
expect(createVNode('div').key).toBe(null)
|
||||
expect(createVNode('div', { key: undefined }).key).toBe(null)
|
||||
})
|
||||
|
||||
test('create with class component', () => {
|
||||
class Component {
|
||||
$props: any
|
||||
static __vccOpts = { template: '<div />' }
|
||||
}
|
||||
const vnode = createVNode(Component)
|
||||
expect(vnode.type).toEqual(Component.__vccOpts)
|
||||
})
|
||||
|
||||
describe('class normalization', () => {
|
||||
test('string', () => {
|
||||
const vnode = createVNode('p', { class: 'foo baz' })
|
||||
@@ -86,7 +109,7 @@ describe('vnode', () => {
|
||||
const vnode = createVNode('p', null, ['foo'])
|
||||
expect(vnode.children).toMatchObject(['foo'])
|
||||
expect(vnode.shapeFlag).toBe(
|
||||
ShapeFlags.ELEMENT + ShapeFlags.ARRAY_CHILDREN
|
||||
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
|
||||
)
|
||||
})
|
||||
|
||||
@@ -94,7 +117,7 @@ describe('vnode', () => {
|
||||
const vnode = createVNode('p', null, { foo: 'foo' })
|
||||
expect(vnode.children).toMatchObject({ foo: 'foo' })
|
||||
expect(vnode.shapeFlag).toBe(
|
||||
ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN
|
||||
ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
|
||||
)
|
||||
})
|
||||
|
||||
@@ -102,7 +125,7 @@ describe('vnode', () => {
|
||||
const vnode = createVNode('p', null, nop)
|
||||
expect(vnode.children).toMatchObject({ default: nop })
|
||||
expect(vnode.shapeFlag).toBe(
|
||||
ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN
|
||||
ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
|
||||
)
|
||||
})
|
||||
|
||||
@@ -110,7 +133,19 @@ describe('vnode', () => {
|
||||
const vnode = createVNode('p', null, 'foo')
|
||||
expect(vnode.children).toBe('foo')
|
||||
expect(vnode.shapeFlag).toBe(
|
||||
ShapeFlags.ELEMENT + ShapeFlags.TEXT_CHILDREN
|
||||
ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
|
||||
)
|
||||
})
|
||||
|
||||
test('element with slots', () => {
|
||||
const children = [createVNode('span', null, 'hello')]
|
||||
const vnode = createVNode('div', null, {
|
||||
default: () => children
|
||||
})
|
||||
|
||||
expect(vnode.children).toBe(children)
|
||||
expect(vnode.shapeFlag).toBe(
|
||||
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -120,6 +155,12 @@ describe('vnode', () => {
|
||||
expect(normalizeVNode(null)).toMatchObject({ type: Comment })
|
||||
expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })
|
||||
|
||||
// boolean -> Comment
|
||||
// this is for usage like `someBoolean && h('div')` and behavior consistency
|
||||
// with 2.x (#574)
|
||||
expect(normalizeVNode(true)).toMatchObject({ type: Comment })
|
||||
expect(normalizeVNode(false)).toMatchObject({ type: Comment })
|
||||
|
||||
// array -> Fragment
|
||||
expect(normalizeVNode(['foo'])).toMatchObject({ type: Fragment })
|
||||
|
||||
@@ -137,7 +178,6 @@ describe('vnode', () => {
|
||||
// primitive types
|
||||
expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
|
||||
expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })
|
||||
expect(normalizeVNode(true)).toMatchObject({ type: Text, children: `true` })
|
||||
})
|
||||
|
||||
test('type shapeFlag inference', () => {
|
||||
@@ -225,4 +265,126 @@ describe('vnode', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic children', () => {
|
||||
test('with patchFlags', () => {
|
||||
const hoist = createVNode('div')
|
||||
let vnode1
|
||||
const vnode = (openBlock(),
|
||||
createBlock('div', null, [
|
||||
hoist,
|
||||
(vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
|
||||
]))
|
||||
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
|
||||
})
|
||||
|
||||
test('should not track vnodes with only HYDRATE_EVENTS flag', () => {
|
||||
const hoist = createVNode('div')
|
||||
const vnode = (openBlock(),
|
||||
createBlock('div', null, [
|
||||
hoist,
|
||||
createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS)
|
||||
]))
|
||||
expect(vnode.dynamicChildren).toStrictEqual([])
|
||||
})
|
||||
|
||||
test('many times call openBlock', () => {
|
||||
const hoist = createVNode('div')
|
||||
let vnode1, vnode2, vnode3
|
||||
const vnode = (openBlock(),
|
||||
createBlock('div', null, [
|
||||
hoist,
|
||||
(vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
|
||||
(vnode2 = (openBlock(),
|
||||
createBlock('div', null, [
|
||||
hoist,
|
||||
(vnode3 = createVNode('div', null, 'text', PatchFlags.TEXT))
|
||||
])))
|
||||
]))
|
||||
expect(vnode.dynamicChildren).toStrictEqual([vnode1, vnode2])
|
||||
expect(vnode2.dynamicChildren).toStrictEqual([vnode3])
|
||||
})
|
||||
|
||||
test('with stateful component', () => {
|
||||
const hoist = createVNode('div')
|
||||
let vnode1
|
||||
const vnode = (openBlock(),
|
||||
createBlock('div', null, [
|
||||
hoist,
|
||||
(vnode1 = createVNode({}, null, 'text'))
|
||||
]))
|
||||
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
|
||||
})
|
||||
|
||||
test('with functional component', () => {
|
||||
const hoist = createVNode('div')
|
||||
let vnode1
|
||||
const vnode = (openBlock(),
|
||||
createBlock('div', null, [
|
||||
hoist,
|
||||
(vnode1 = createVNode(() => {}, null, 'text'))
|
||||
]))
|
||||
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
|
||||
})
|
||||
|
||||
test('with suspense', () => {
|
||||
const hoist = createVNode('div')
|
||||
let vnode1
|
||||
const vnode = (openBlock(),
|
||||
createBlock('div', null, [
|
||||
hoist,
|
||||
(vnode1 = createVNode(() => {}, null, 'text'))
|
||||
]))
|
||||
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
|
||||
})
|
||||
})
|
||||
|
||||
describe('transformVNodeArgs', () => {
|
||||
afterEach(() => {
|
||||
// reset
|
||||
transformVNodeArgs()
|
||||
})
|
||||
|
||||
test('no-op pass through', () => {
|
||||
transformVNodeArgs(args => args)
|
||||
const vnode = createVNode('div', { id: 'foo' }, 'hello')
|
||||
expect(vnode).toMatchObject({
|
||||
type: 'div',
|
||||
props: { id: 'foo' },
|
||||
children: 'hello',
|
||||
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
|
||||
})
|
||||
})
|
||||
|
||||
test('direct override', () => {
|
||||
transformVNodeArgs(() => ['div', { id: 'foo' }, 'hello'])
|
||||
const vnode = createVNode('p')
|
||||
expect(vnode).toMatchObject({
|
||||
type: 'div',
|
||||
props: { id: 'foo' },
|
||||
children: 'hello',
|
||||
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
|
||||
})
|
||||
})
|
||||
|
||||
test('receive component instance as 2nd arg', () => {
|
||||
transformVNodeArgs((args, instance) => {
|
||||
if (instance) {
|
||||
return ['h1', null, instance.type.name]
|
||||
} else {
|
||||
return args
|
||||
}
|
||||
})
|
||||
const App = {
|
||||
// this will be the name of the component in the h1
|
||||
name: 'Root Component',
|
||||
render() {
|
||||
return h('p') // this will be overwritten by the transform
|
||||
}
|
||||
}
|
||||
const root = nodeOps.createElement('div')
|
||||
createApp(App).mount(root)
|
||||
expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
98
packages/runtime-core/__tests__/vnodeHooks.spec.ts
Normal file
98
packages/runtime-core/__tests__/vnodeHooks.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
h,
|
||||
render,
|
||||
nodeOps,
|
||||
VNodeProps,
|
||||
TestElement,
|
||||
NodeTypes,
|
||||
VNode
|
||||
} from '@vue/runtime-test'
|
||||
|
||||
describe('renderer: vnode hooks', () => {
|
||||
function assertHooks(hooks: VNodeProps, vnode1: VNode, vnode2: VNode) {
|
||||
const root = nodeOps.createElement('div')
|
||||
render(vnode1, root)
|
||||
expect(hooks.onVnodeBeforeMount).toHaveBeenCalledWith(vnode1, null)
|
||||
expect(hooks.onVnodeMounted).toHaveBeenCalledWith(vnode1, null)
|
||||
expect(hooks.onVnodeBeforeUpdate).not.toHaveBeenCalled()
|
||||
expect(hooks.onVnodeUpdated).not.toHaveBeenCalled()
|
||||
expect(hooks.onVnodeBeforeUnmount).not.toHaveBeenCalled()
|
||||
expect(hooks.onVnodeUnmounted).not.toHaveBeenCalled()
|
||||
|
||||
// update
|
||||
render(vnode2, root)
|
||||
expect(hooks.onVnodeBeforeMount).toHaveBeenCalledTimes(1)
|
||||
expect(hooks.onVnodeMounted).toHaveBeenCalledTimes(1)
|
||||
expect(hooks.onVnodeBeforeUpdate).toHaveBeenCalledWith(vnode2, vnode1)
|
||||
expect(hooks.onVnodeUpdated).toHaveBeenCalledWith(vnode2, vnode1)
|
||||
expect(hooks.onVnodeBeforeUnmount).not.toHaveBeenCalled()
|
||||
expect(hooks.onVnodeUnmounted).not.toHaveBeenCalled()
|
||||
|
||||
// unmount
|
||||
render(null, root)
|
||||
expect(hooks.onVnodeBeforeMount).toHaveBeenCalledTimes(1)
|
||||
expect(hooks.onVnodeMounted).toHaveBeenCalledTimes(1)
|
||||
expect(hooks.onVnodeBeforeUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(hooks.onVnodeUpdated).toHaveBeenCalledTimes(1)
|
||||
expect(hooks.onVnodeBeforeUnmount).toHaveBeenCalledWith(vnode2, null)
|
||||
expect(hooks.onVnodeUnmounted).toHaveBeenCalledWith(vnode2, null)
|
||||
}
|
||||
|
||||
test('should work on element', () => {
|
||||
const hooks: VNodeProps = {
|
||||
onVnodeBeforeMount: jest.fn(),
|
||||
onVnodeMounted: jest.fn(),
|
||||
onVnodeBeforeUpdate: jest.fn(vnode => {
|
||||
expect((vnode.el as TestElement).children[0]).toMatchObject({
|
||||
type: NodeTypes.TEXT,
|
||||
text: 'foo'
|
||||
})
|
||||
}),
|
||||
onVnodeUpdated: jest.fn(vnode => {
|
||||
expect((vnode.el as TestElement).children[0]).toMatchObject({
|
||||
type: NodeTypes.TEXT,
|
||||
text: 'bar'
|
||||
})
|
||||
}),
|
||||
onVnodeBeforeUnmount: jest.fn(),
|
||||
onVnodeUnmounted: jest.fn()
|
||||
}
|
||||
|
||||
assertHooks(hooks, h('div', hooks, 'foo'), h('div', hooks, 'bar'))
|
||||
})
|
||||
|
||||
test('should work on component', () => {
|
||||
const Comp = (props: { msg: string }) => props.msg
|
||||
|
||||
const hooks: VNodeProps = {
|
||||
onVnodeBeforeMount: jest.fn(),
|
||||
onVnodeMounted: jest.fn(),
|
||||
onVnodeBeforeUpdate: jest.fn(vnode => {
|
||||
expect(vnode.el as TestElement).toMatchObject({
|
||||
type: NodeTypes.TEXT,
|
||||
text: 'foo'
|
||||
})
|
||||
}),
|
||||
onVnodeUpdated: jest.fn(vnode => {
|
||||
expect(vnode.el as TestElement).toMatchObject({
|
||||
type: NodeTypes.TEXT,
|
||||
text: 'bar'
|
||||
})
|
||||
}),
|
||||
onVnodeBeforeUnmount: jest.fn(),
|
||||
onVnodeUnmounted: jest.fn()
|
||||
}
|
||||
|
||||
assertHooks(
|
||||
hooks,
|
||||
h(Comp, {
|
||||
...hooks,
|
||||
msg: 'foo'
|
||||
}),
|
||||
h(Comp, {
|
||||
...hooks,
|
||||
msg: 'bar'
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "@vue/runtime-core",
|
||||
"version": "3.0.0-alpha.0",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"description": "@vue/runtime-core",
|
||||
"main": "index.js",
|
||||
"module": "dist/runtime-core.esm-bundler.js",
|
||||
"types": "dist/runtime-core.d.ts",
|
||||
"files": [
|
||||
"index.js",
|
||||
"dist"
|
||||
],
|
||||
"types": "dist/runtime-core.d.ts",
|
||||
"buildOptions": {
|
||||
"name": "VueRuntimeCore",
|
||||
"formats": [
|
||||
"esm-bundler",
|
||||
"cjs"
|
||||
@@ -18,7 +19,7 @@
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vuejs/vue.git"
|
||||
"url": "git+https://github.com/vuejs/vue-next.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vue"
|
||||
@@ -26,10 +27,11 @@
|
||||
"author": "Evan You",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vuejs/vue/issues"
|
||||
"url": "https://github.com/vuejs/vue-next/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/runtime-core#readme",
|
||||
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-core#readme",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.0.0-alpha.0"
|
||||
"@vue/shared": "3.0.0-alpha.11",
|
||||
"@vue/reactivity": "3.0.0-alpha.11"
|
||||
}
|
||||
}
|
||||
|
||||
194
packages/runtime-core/src/apiAsyncComponent.ts
Normal file
194
packages/runtime-core/src/apiAsyncComponent.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
PublicAPIComponent,
|
||||
Component,
|
||||
currentInstance,
|
||||
ComponentInternalInstance,
|
||||
isInSSRComponentSetup
|
||||
} from './component'
|
||||
import { isFunction, isObject } from '@vue/shared'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { createVNode } from './vnode'
|
||||
import { defineComponent } from './apiDefineComponent'
|
||||
import { warn } from './warning'
|
||||
import { ref } from '@vue/reactivity'
|
||||
import { handleError, ErrorCodes } from './errorHandling'
|
||||
|
||||
export type AsyncComponentResolveResult<T = PublicAPIComponent> =
|
||||
| T
|
||||
| { default: T } // es modules
|
||||
|
||||
export type AsyncComponentLoader<T = any> = () => Promise<
|
||||
AsyncComponentResolveResult<T>
|
||||
>
|
||||
|
||||
export interface AsyncComponentOptions<T = any> {
|
||||
loader: AsyncComponentLoader<T>
|
||||
loadingComponent?: PublicAPIComponent
|
||||
errorComponent?: PublicAPIComponent
|
||||
delay?: number
|
||||
timeout?: number
|
||||
suspensible?: boolean
|
||||
onError?: (
|
||||
error: Error,
|
||||
retry: () => void,
|
||||
fail: () => void,
|
||||
attempts: number
|
||||
) => any
|
||||
}
|
||||
|
||||
export function defineAsyncComponent<
|
||||
T extends PublicAPIComponent = { new (): ComponentPublicInstance }
|
||||
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
|
||||
if (isFunction(source)) {
|
||||
source = { loader: source }
|
||||
}
|
||||
|
||||
const {
|
||||
loader,
|
||||
loadingComponent: loadingComponent,
|
||||
errorComponent: errorComponent,
|
||||
delay = 200,
|
||||
timeout, // undefined = never times out
|
||||
suspensible = true,
|
||||
onError: userOnError
|
||||
} = source
|
||||
|
||||
let pendingRequest: Promise<Component> | null = null
|
||||
let resolvedComp: Component | undefined
|
||||
|
||||
let retries = 0
|
||||
const retry = () => {
|
||||
retries++
|
||||
pendingRequest = null
|
||||
return load()
|
||||
}
|
||||
|
||||
const load = (): Promise<Component> => {
|
||||
let thisRequest: Promise<Component>
|
||||
return (
|
||||
pendingRequest ||
|
||||
(thisRequest = pendingRequest = loader()
|
||||
.catch(err => {
|
||||
err = err instanceof Error ? err : new Error(String(err))
|
||||
if (userOnError) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const userRetry = () => resolve(retry())
|
||||
const userFail = () => reject(err)
|
||||
userOnError(err, userRetry, userFail, retries + 1)
|
||||
})
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
.then((comp: any) => {
|
||||
if (thisRequest !== pendingRequest && pendingRequest) {
|
||||
return pendingRequest
|
||||
}
|
||||
if (__DEV__ && !comp) {
|
||||
warn(
|
||||
`Async component loader resolved to undefined. ` +
|
||||
`If you are using retry(), make sure to return its return value.`
|
||||
)
|
||||
}
|
||||
// interop module default
|
||||
if (
|
||||
comp &&
|
||||
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
|
||||
) {
|
||||
comp = comp.default
|
||||
}
|
||||
if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
|
||||
throw new Error(`Invalid async component load result: ${comp}`)
|
||||
}
|
||||
resolvedComp = comp
|
||||
return comp
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
return defineComponent({
|
||||
__asyncLoader: load,
|
||||
name: 'AsyncComponentWrapper',
|
||||
setup() {
|
||||
const instance = currentInstance!
|
||||
|
||||
// already resolved
|
||||
if (resolvedComp) {
|
||||
return () => createInnerComp(resolvedComp!, instance)
|
||||
}
|
||||
|
||||
const onError = (err: Error) => {
|
||||
pendingRequest = null
|
||||
handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER)
|
||||
}
|
||||
|
||||
// suspense-controlled or SSR.
|
||||
if (
|
||||
(__FEATURE_SUSPENSE__ && suspensible && instance.suspense) ||
|
||||
(__NODE_JS__ && isInSSRComponentSetup)
|
||||
) {
|
||||
return load()
|
||||
.then(comp => {
|
||||
return () => createInnerComp(comp, instance)
|
||||
})
|
||||
.catch(err => {
|
||||
onError(err)
|
||||
return () =>
|
||||
errorComponent
|
||||
? createVNode(errorComponent as Component, { error: err })
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
const loaded = ref(false)
|
||||
const error = ref()
|
||||
const delayed = ref(!!delay)
|
||||
|
||||
if (delay) {
|
||||
setTimeout(() => {
|
||||
delayed.value = false
|
||||
}, delay)
|
||||
}
|
||||
|
||||
if (timeout != null) {
|
||||
setTimeout(() => {
|
||||
if (!loaded.value) {
|
||||
const err = new Error(
|
||||
`Async component timed out after ${timeout}ms.`
|
||||
)
|
||||
onError(err)
|
||||
error.value = err
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
load()
|
||||
.then(() => {
|
||||
loaded.value = true
|
||||
})
|
||||
.catch(err => {
|
||||
onError(err)
|
||||
error.value = err
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (loaded.value && resolvedComp) {
|
||||
return createInnerComp(resolvedComp, instance)
|
||||
} else if (error.value && errorComponent) {
|
||||
return createVNode(errorComponent as Component, {
|
||||
error: error.value
|
||||
})
|
||||
} else if (loadingComponent && !delayed.value) {
|
||||
return createVNode(loadingComponent as Component)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as any
|
||||
}
|
||||
|
||||
function createInnerComp(
|
||||
comp: Component,
|
||||
{ vnode: { props, children } }: ComponentInternalInstance
|
||||
) {
|
||||
return createVNode(comp, props, children)
|
||||
}
|
||||
20
packages/runtime-core/src/apiComputed.ts
Normal file
20
packages/runtime-core/src/apiComputed.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
computed as _computed,
|
||||
ComputedRef,
|
||||
WritableComputedOptions,
|
||||
WritableComputedRef,
|
||||
ComputedGetter
|
||||
} from '@vue/reactivity'
|
||||
import { recordInstanceBoundEffect } from './component'
|
||||
|
||||
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
|
||||
export function computed<T>(
|
||||
options: WritableComputedOptions<T>
|
||||
): WritableComputedRef<T>
|
||||
export function computed<T>(
|
||||
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
|
||||
) {
|
||||
const c = _computed(getterOrOptions as any)
|
||||
recordInstanceBoundEffect(c.effect)
|
||||
return c
|
||||
}
|
||||
@@ -1,36 +1,59 @@
|
||||
import { Component, Data, validateComponentName } from './component'
|
||||
import { ComponentOptions } from './apiOptions'
|
||||
import {
|
||||
Component,
|
||||
Data,
|
||||
validateComponentName,
|
||||
PublicAPIComponent
|
||||
} from './component'
|
||||
import { ComponentOptions } from './componentOptions'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { Directive, validateDirectiveName } from './directives'
|
||||
import { RootRenderFunction } from './renderer'
|
||||
import { InjectionKey } from './apiInject'
|
||||
import { isFunction, NO, isObject } from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { createVNode } from './vnode'
|
||||
import { createVNode, cloneVNode, VNode } from './vnode'
|
||||
import { RootHydrateFunction } from './hydration'
|
||||
|
||||
export interface App<HostElement = any> {
|
||||
config: AppConfig
|
||||
use(plugin: Plugin, options?: any): this
|
||||
use(plugin: Plugin, ...options: any[]): this
|
||||
mixin(mixin: ComponentOptions): this
|
||||
component(name: string): Component | undefined
|
||||
component(name: string, component: Component): this
|
||||
component(name: string): PublicAPIComponent | undefined
|
||||
component(name: string, component: PublicAPIComponent): this
|
||||
directive(name: string): Directive | undefined
|
||||
directive(name: string, directive: Directive): this
|
||||
mount(
|
||||
rootComponent: Component,
|
||||
rootContainer: HostElement | string,
|
||||
rootProps?: Data
|
||||
isHydrate?: boolean
|
||||
): ComponentPublicInstance
|
||||
unmount(rootContainer: HostElement | string): void
|
||||
provide<T>(key: InjectionKey<T> | string, value: T): this
|
||||
|
||||
// internal. We need to expose these for the server-renderer
|
||||
_component: Component
|
||||
_props: Data | null
|
||||
_container: HostElement | null
|
||||
_context: AppContext
|
||||
}
|
||||
|
||||
export type OptionMergeFunction = (
|
||||
to: unknown,
|
||||
from: unknown,
|
||||
instance: any,
|
||||
key: string
|
||||
) => any
|
||||
|
||||
export interface AppConfig {
|
||||
// @private
|
||||
readonly isNativeTag?: (tag: string) => boolean
|
||||
|
||||
devtools: boolean
|
||||
performance: boolean
|
||||
readonly isNativeTag?: (tag: string) => boolean
|
||||
isCustomElement?: (tag: string) => boolean
|
||||
optionMergeStrategies: Record<string, OptionMergeFunction>
|
||||
globalProperties: Record<string, any>
|
||||
isCustomElement: (tag: string) => boolean
|
||||
errorHandler?: (
|
||||
err: Error,
|
||||
err: unknown,
|
||||
instance: ComponentPublicInstance | null,
|
||||
info: string
|
||||
) => void
|
||||
@@ -44,15 +67,16 @@ export interface AppConfig {
|
||||
export interface AppContext {
|
||||
config: AppConfig
|
||||
mixins: ComponentOptions[]
|
||||
components: Record<string, Component>
|
||||
components: Record<string, PublicAPIComponent>
|
||||
directives: Record<string, Directive>
|
||||
provides: Record<string | symbol, any>
|
||||
reload?: () => void // HMR only
|
||||
}
|
||||
|
||||
type PluginInstallFunction = (app: App) => any
|
||||
type PluginInstallFunction = (app: App, ...options: any[]) => any
|
||||
|
||||
export type Plugin =
|
||||
| PluginInstallFunction
|
||||
| PluginInstallFunction & { install?: PluginInstallFunction }
|
||||
| {
|
||||
install: PluginInstallFunction
|
||||
}
|
||||
@@ -60,9 +84,11 @@ export type Plugin =
|
||||
export function createAppContext(): AppContext {
|
||||
return {
|
||||
config: {
|
||||
isNativeTag: NO,
|
||||
devtools: true,
|
||||
performance: false,
|
||||
isNativeTag: NO,
|
||||
globalProperties: {},
|
||||
optionMergeStrategies: {},
|
||||
isCustomElement: NO,
|
||||
errorHandler: undefined,
|
||||
warnHandler: undefined
|
||||
@@ -70,20 +96,36 @@ export function createAppContext(): AppContext {
|
||||
mixins: [],
|
||||
components: {},
|
||||
directives: {},
|
||||
provides: {}
|
||||
provides: Object.create(null)
|
||||
}
|
||||
}
|
||||
|
||||
export function createAppAPI<HostNode, HostElement>(
|
||||
render: RootRenderFunction<HostNode, HostElement>
|
||||
): () => App<HostElement> {
|
||||
return function createApp(): App {
|
||||
export type CreateAppFunction<HostElement> = (
|
||||
rootComponent: PublicAPIComponent,
|
||||
rootProps?: Data | null
|
||||
) => App<HostElement>
|
||||
|
||||
export function createAppAPI<HostElement>(
|
||||
render: RootRenderFunction,
|
||||
hydrate?: RootHydrateFunction
|
||||
): CreateAppFunction<HostElement> {
|
||||
return function createApp(rootComponent, rootProps = null) {
|
||||
if (rootProps != null && !isObject(rootProps)) {
|
||||
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
|
||||
rootProps = null
|
||||
}
|
||||
|
||||
const context = createAppContext()
|
||||
const installedPlugins = new Set()
|
||||
|
||||
let isMounted = false
|
||||
|
||||
const app: App = {
|
||||
_component: rootComponent as Component,
|
||||
_props: rootProps,
|
||||
_container: null,
|
||||
_context: context,
|
||||
|
||||
get config() {
|
||||
return context.config
|
||||
},
|
||||
@@ -96,15 +138,15 @@ export function createAppAPI<HostNode, HostElement>(
|
||||
}
|
||||
},
|
||||
|
||||
use(plugin: Plugin) {
|
||||
use(plugin: Plugin, ...options: any[]) {
|
||||
if (installedPlugins.has(plugin)) {
|
||||
__DEV__ && warn(`Plugin has already been applied to target app.`)
|
||||
} else if (isFunction(plugin)) {
|
||||
installedPlugins.add(plugin)
|
||||
plugin(app)
|
||||
} else if (plugin && isFunction(plugin.install)) {
|
||||
installedPlugins.add(plugin)
|
||||
plugin.install(app)
|
||||
plugin.install(app, ...options)
|
||||
} else if (isFunction(plugin)) {
|
||||
installedPlugins.add(plugin)
|
||||
plugin(app, ...options)
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
`A plugin must either be a function or an object with an "install" ` +
|
||||
@@ -115,23 +157,22 @@ export function createAppAPI<HostNode, HostElement>(
|
||||
},
|
||||
|
||||
mixin(mixin: ComponentOptions) {
|
||||
if (__DEV__ && !__FEATURE_OPTIONS__) {
|
||||
if (__FEATURE_OPTIONS__) {
|
||||
if (!context.mixins.includes(mixin)) {
|
||||
context.mixins.push(mixin)
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
'Mixin has already been applied to target app' +
|
||||
(mixin.name ? `: ${mixin.name}` : '')
|
||||
)
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
warn('Mixins are only available in builds supporting Options API')
|
||||
}
|
||||
|
||||
if (!context.mixins.includes(mixin)) {
|
||||
context.mixins.push(mixin)
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
'Mixin has already been applied to target app' +
|
||||
(mixin.name ? `: ${mixin.name}` : '')
|
||||
)
|
||||
}
|
||||
|
||||
return app
|
||||
},
|
||||
|
||||
component(name: string, component?: Component): any {
|
||||
component(name: string, component?: PublicAPIComponent): any {
|
||||
if (__DEV__) {
|
||||
validateComponentName(name, context.config)
|
||||
}
|
||||
@@ -160,23 +201,27 @@ export function createAppAPI<HostNode, HostElement>(
|
||||
return app
|
||||
},
|
||||
|
||||
mount(
|
||||
rootComponent: Component,
|
||||
rootContainer: HostElement,
|
||||
rootProps?: Data | null
|
||||
): any {
|
||||
mount(rootContainer: HostElement, isHydrate?: boolean): any {
|
||||
if (!isMounted) {
|
||||
if (rootProps != null && !isObject(rootProps)) {
|
||||
__DEV__ &&
|
||||
warn(`root props passed to app.mount() must be an object.`)
|
||||
rootProps = null
|
||||
}
|
||||
const vnode = createVNode(rootComponent, rootProps)
|
||||
const vnode = createVNode(rootComponent as Component, rootProps)
|
||||
// store app context on the root VNode.
|
||||
// this will be set on the root instance on initial mount.
|
||||
vnode.appContext = context
|
||||
render(vnode, rootContainer)
|
||||
|
||||
// HMR root reload
|
||||
if (__BUNDLER__ && __DEV__) {
|
||||
context.reload = () => {
|
||||
render(cloneVNode(vnode), rootContainer)
|
||||
}
|
||||
}
|
||||
|
||||
if (isHydrate && hydrate) {
|
||||
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
|
||||
} else {
|
||||
render(vnode, rootContainer)
|
||||
}
|
||||
isMounted = true
|
||||
app._container = rootContainer
|
||||
return vnode.component!.proxy
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
@@ -185,6 +230,14 @@ export function createAppAPI<HostNode, HostElement>(
|
||||
}
|
||||
},
|
||||
|
||||
unmount() {
|
||||
if (isMounted) {
|
||||
render(null, app._container)
|
||||
} else if (__DEV__) {
|
||||
warn(`Cannot unmount an app that is not mounted.`)
|
||||
}
|
||||
},
|
||||
|
||||
provide(key, value) {
|
||||
if (__DEV__ && key in context.provides) {
|
||||
warn(
|
||||
@@ -4,21 +4,22 @@ import {
|
||||
ComponentOptionsWithoutProps,
|
||||
ComponentOptionsWithArrayProps,
|
||||
ComponentOptionsWithObjectProps
|
||||
} from './apiOptions'
|
||||
} from './componentOptions'
|
||||
import { SetupContext, RenderFunction } from './component'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { ExtractPropTypes, ComponentPropsOptions } from './componentProps'
|
||||
import { EmitsOptions } from './componentEmits'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { VNodeProps } from './vnode'
|
||||
|
||||
// createComponent is a utility that is primarily used for type inference
|
||||
// defineComponent is a utility that is primarily used for type inference
|
||||
// when declaring components. Type inference is provided in the component
|
||||
// options (provided as the argument). The returned value has artifical types
|
||||
// for TSX / manual render function / IDE support.
|
||||
|
||||
// overload 1: direct setup function
|
||||
// (uses user defined props interface)
|
||||
export function createComponent<Props, RawBindings = object>(
|
||||
export function defineComponent<Props, RawBindings = object>(
|
||||
setup: (
|
||||
props: Readonly<Props>,
|
||||
ctx: SetupContext
|
||||
@@ -38,14 +39,16 @@ export function createComponent<Props, RawBindings = object>(
|
||||
// overload 2: object format with no props
|
||||
// (uses user defined props interface)
|
||||
// return type is for Vetur and TSX support
|
||||
export function createComponent<
|
||||
Props,
|
||||
RawBindings,
|
||||
D,
|
||||
export function defineComponent<
|
||||
Props = {},
|
||||
RawBindings = {},
|
||||
D = {},
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {}
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string
|
||||
>(
|
||||
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M>
|
||||
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, E, EE>
|
||||
): {
|
||||
new (): ComponentPublicInstance<
|
||||
Props,
|
||||
@@ -53,6 +56,7 @@ export function createComponent<
|
||||
D,
|
||||
C,
|
||||
M,
|
||||
E,
|
||||
VNodeProps & Props
|
||||
>
|
||||
}
|
||||
@@ -60,31 +64,51 @@ export function createComponent<
|
||||
// overload 3: object format with array props declaration
|
||||
// props inferred as { [key in PropNames]?: any }
|
||||
// return type is for Vetur and TSX support
|
||||
export function createComponent<
|
||||
export function defineComponent<
|
||||
PropNames extends string,
|
||||
RawBindings,
|
||||
D,
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {}
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string
|
||||
>(
|
||||
options: ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M>
|
||||
options: ComponentOptionsWithArrayProps<
|
||||
PropNames,
|
||||
RawBindings,
|
||||
D,
|
||||
C,
|
||||
M,
|
||||
E,
|
||||
EE
|
||||
>
|
||||
): {
|
||||
// array props technically doesn't place any contraints on props in TSX
|
||||
new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M>
|
||||
new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M, E>
|
||||
}
|
||||
|
||||
// overload 4: object format with object props declaration
|
||||
// see `ExtractPropTypes` in ./componentProps.ts
|
||||
export function createComponent<
|
||||
export function defineComponent<
|
||||
// the Readonly constraint allows TS to treat the type of { required: true }
|
||||
// as constant instead of boolean.
|
||||
PropsOptions extends Readonly<ComponentPropsOptions>,
|
||||
RawBindings,
|
||||
D,
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {}
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string
|
||||
>(
|
||||
options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M>
|
||||
options: ComponentOptionsWithObjectProps<
|
||||
PropsOptions,
|
||||
RawBindings,
|
||||
D,
|
||||
C,
|
||||
M,
|
||||
E,
|
||||
EE
|
||||
>
|
||||
): {
|
||||
new (): ComponentPublicInstance<
|
||||
ExtractPropTypes<PropsOptions>,
|
||||
@@ -92,11 +116,12 @@ export function createComponent<
|
||||
D,
|
||||
C,
|
||||
M,
|
||||
E,
|
||||
VNodeProps & ExtractPropTypes<PropsOptions, false>
|
||||
>
|
||||
}
|
||||
|
||||
// implementation, close to no-op
|
||||
export function createComponent(options: unknown) {
|
||||
export function defineComponent(options: unknown) {
|
||||
return isFunction(options) ? { setup: options } : options
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export function inject(
|
||||
if (key in provides) {
|
||||
// TS doesn't allow symbol as index type
|
||||
return provides[key as string]
|
||||
} else if (defaultValue !== undefined) {
|
||||
} else if (arguments.length > 1) {
|
||||
return defaultValue
|
||||
} else if (__DEV__) {
|
||||
warn(`injection "${String(key)}" not found.`)
|
||||
|
||||
@@ -2,13 +2,14 @@ import {
|
||||
ComponentInternalInstance,
|
||||
LifecycleHooks,
|
||||
currentInstance,
|
||||
setCurrentInstance
|
||||
setCurrentInstance,
|
||||
isInSSRComponentSetup
|
||||
} from './component'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
|
||||
import { warn } from './warning'
|
||||
import { capitalize } from '@vue/shared'
|
||||
import { pauseTracking, resumeTracking, DebuggerEvent } from '@vue/reactivity'
|
||||
import { pauseTracking, resetTracking, DebuggerEvent } from '@vue/reactivity'
|
||||
|
||||
export { onActivated, onDeactivated } from './components/KeepAlive'
|
||||
|
||||
@@ -38,7 +39,7 @@ export function injectHook(
|
||||
setCurrentInstance(target)
|
||||
const res = callWithAsyncErrorHandling(hook, target, type, args)
|
||||
setCurrentInstance(null)
|
||||
resumeTracking()
|
||||
resetTracking()
|
||||
return res
|
||||
})
|
||||
if (prepend) {
|
||||
@@ -65,7 +66,8 @@ export function injectHook(
|
||||
export const createHook = <T extends Function = () => any>(
|
||||
lifecycle: LifecycleHooks
|
||||
) => (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
|
||||
injectHook(lifecycle, hook, target)
|
||||
// post-create lifecycle registrations are noops during SSR
|
||||
!isInSSRComponentSetup && injectHook(lifecycle, hook, target)
|
||||
|
||||
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
|
||||
export const onMounted = createHook(LifecycleHooks.MOUNTED)
|
||||
@@ -83,10 +85,14 @@ export const onRenderTracked = createHook<DebuggerHook>(
|
||||
)
|
||||
|
||||
export type ErrorCapturedHook = (
|
||||
err: Error,
|
||||
err: unknown,
|
||||
instance: ComponentPublicInstance | null,
|
||||
info: string
|
||||
) => boolean | void
|
||||
export const onErrorCaptured = createHook<ErrorCapturedHook>(
|
||||
LifecycleHooks.ERROR_CAPTURED
|
||||
)
|
||||
|
||||
export const onErrorCaptured = (
|
||||
hook: ErrorCapturedHook,
|
||||
target: ComponentInternalInstance | null = currentInstance
|
||||
) => {
|
||||
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
export {
|
||||
ref,
|
||||
isRef,
|
||||
toRefs,
|
||||
reactive,
|
||||
isReactive,
|
||||
readonly,
|
||||
isReadonly,
|
||||
toRaw,
|
||||
markReadonly,
|
||||
markNonReactive,
|
||||
effect,
|
||||
// types
|
||||
ReactiveEffect,
|
||||
ReactiveEffectOptions,
|
||||
DebuggerEvent,
|
||||
TrackOpTypes,
|
||||
TriggerOpTypes,
|
||||
Ref,
|
||||
ComputedRef,
|
||||
UnwrapRef,
|
||||
WritableComputedOptions
|
||||
} from '@vue/reactivity'
|
||||
|
||||
import {
|
||||
computed as _computed,
|
||||
ComputedRef,
|
||||
WritableComputedOptions,
|
||||
ReactiveEffect,
|
||||
WritableComputedRef,
|
||||
ComputedGetter
|
||||
} from '@vue/reactivity'
|
||||
|
||||
import { currentInstance } from './component'
|
||||
|
||||
// record effects created during a component's setup() so that they can be
|
||||
// stopped when the component unmounts
|
||||
export function recordEffect(effect: ReactiveEffect) {
|
||||
if (currentInstance) {
|
||||
;(currentInstance.effects || (currentInstance.effects = [])).push(effect)
|
||||
}
|
||||
}
|
||||
|
||||
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
|
||||
export function computed<T>(
|
||||
options: WritableComputedOptions<T>
|
||||
): WritableComputedRef<T>
|
||||
export function computed<T>(
|
||||
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
|
||||
) {
|
||||
const c = _computed(getterOrOptions as any)
|
||||
recordEffect(c.effect)
|
||||
return c
|
||||
}
|
||||
@@ -13,14 +13,16 @@ import {
|
||||
isArray,
|
||||
isFunction,
|
||||
isString,
|
||||
hasChanged
|
||||
hasChanged,
|
||||
NOOP,
|
||||
remove
|
||||
} from '@vue/shared'
|
||||
import { recordEffect } from './apiReactivity'
|
||||
import {
|
||||
currentInstance,
|
||||
ComponentInternalInstance,
|
||||
currentSuspense,
|
||||
Data
|
||||
Data,
|
||||
isInSSRComponentSetup,
|
||||
recordInstanceBoundEffect
|
||||
} from './component'
|
||||
import {
|
||||
ErrorCodes,
|
||||
@@ -29,81 +31,113 @@ import {
|
||||
} from './errorHandling'
|
||||
import { onBeforeUnmount } from './apiLifecycle'
|
||||
import { queuePostRenderEffect } from './renderer'
|
||||
import { warn } from './warning'
|
||||
|
||||
export type WatchHandler<T = any> = (
|
||||
value: T,
|
||||
oldValue: T,
|
||||
onCleanup: CleanupRegistrator
|
||||
export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void
|
||||
|
||||
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
|
||||
|
||||
export type WatchCallback<V = any, OV = any> = (
|
||||
value: V,
|
||||
oldValue: OV,
|
||||
onInvalidate: InvalidateCbRegistrator
|
||||
) => any
|
||||
|
||||
export interface WatchOptions {
|
||||
lazy?: boolean
|
||||
type MapSources<T> = {
|
||||
[K in keyof T]: T[K] extends WatchSource<infer V> ? V : never
|
||||
}
|
||||
|
||||
type MapOldSources<T, Immediate> = {
|
||||
[K in keyof T]: T[K] extends WatchSource<infer V>
|
||||
? Immediate extends true ? (V | undefined) : V
|
||||
: never
|
||||
}
|
||||
|
||||
type InvalidateCbRegistrator = (cb: () => void) => void
|
||||
|
||||
export interface BaseWatchOptions {
|
||||
flush?: 'pre' | 'post' | 'sync'
|
||||
deep?: boolean
|
||||
onTrack?: ReactiveEffectOptions['onTrack']
|
||||
onTrigger?: ReactiveEffectOptions['onTrigger']
|
||||
}
|
||||
|
||||
type StopHandle = () => void
|
||||
|
||||
type WatcherSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
|
||||
|
||||
type MapSources<T> = {
|
||||
[K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
|
||||
export interface WatchOptions<Immediate = boolean> extends BaseWatchOptions {
|
||||
immediate?: Immediate
|
||||
deep?: boolean
|
||||
}
|
||||
|
||||
export type CleanupRegistrator = (invalidate: () => void) => void
|
||||
|
||||
type SimpleEffect = (onCleanup: CleanupRegistrator) => void
|
||||
export type StopHandle = () => void
|
||||
|
||||
const invoke = (fn: Function) => fn()
|
||||
|
||||
// overload #1: simple effect
|
||||
export function watch(effect: SimpleEffect, options?: WatchOptions): StopHandle
|
||||
// Simple effect.
|
||||
export function watchEffect(
|
||||
effect: WatchEffect,
|
||||
options?: BaseWatchOptions
|
||||
): StopHandle {
|
||||
return doWatch(effect, null, options)
|
||||
}
|
||||
|
||||
// overload #2: single source + cb
|
||||
export function watch<T>(
|
||||
source: WatcherSource<T>,
|
||||
cb: WatchHandler<T>,
|
||||
options?: WatchOptions
|
||||
// initial value for watchers to trigger on undefined initial values
|
||||
const INITIAL_WATCHER_VALUE = {}
|
||||
|
||||
// overload #1: single source + cb
|
||||
export function watch<T, Immediate extends Readonly<boolean> = false>(
|
||||
source: WatchSource<T>,
|
||||
cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
|
||||
options?: WatchOptions<Immediate>
|
||||
): StopHandle
|
||||
|
||||
// overload #3: array of multiple sources + cb
|
||||
// overload #2: array of multiple sources + cb
|
||||
// Readonly constraint helps the callback to correctly infer value types based
|
||||
// on position in the source array. Otherwise the values will get a union type
|
||||
// of all possible value types.
|
||||
export function watch<
|
||||
T extends Readonly<WatcherSource<TArgs>>,
|
||||
TArgs extends Array<any> = any[]
|
||||
T extends Readonly<WatchSource<unknown>[]>,
|
||||
Immediate extends Readonly<boolean> = false
|
||||
>(
|
||||
sources: T,
|
||||
cb: WatchHandler<MapSources<T>>,
|
||||
options?: WatchOptions
|
||||
cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,
|
||||
options?: WatchOptions<Immediate>
|
||||
): StopHandle
|
||||
|
||||
// implementation
|
||||
export function watch<T = any>(
|
||||
effectOrSource: WatcherSource<T> | WatcherSource<T>[] | SimpleEffect,
|
||||
cbOrOptions?: WatchHandler<T> | WatchOptions,
|
||||
source: WatchSource<T> | WatchSource<T>[],
|
||||
cb: WatchCallback<T>,
|
||||
options?: WatchOptions
|
||||
): StopHandle {
|
||||
if (isFunction(cbOrOptions)) {
|
||||
// effect callback as 2nd argument - this is a source watcher
|
||||
return doWatch(effectOrSource, cbOrOptions, options)
|
||||
} else {
|
||||
// 2nd argument is either missing or an options object
|
||||
// - this is a simple effect watcher
|
||||
return doWatch(effectOrSource, null, cbOrOptions)
|
||||
if (__DEV__ && !isFunction(cb)) {
|
||||
warn(
|
||||
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
|
||||
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
|
||||
`supports \`watch(source, cb, options?) signature.`
|
||||
)
|
||||
}
|
||||
return doWatch(source, cb, options)
|
||||
}
|
||||
|
||||
function doWatch(
|
||||
source: WatcherSource | WatcherSource[] | SimpleEffect,
|
||||
cb: WatchHandler | null,
|
||||
{ lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
|
||||
source: WatchSource | WatchSource[] | WatchEffect,
|
||||
cb: WatchCallback | null,
|
||||
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
|
||||
): StopHandle {
|
||||
if (__DEV__ && !cb) {
|
||||
if (immediate !== undefined) {
|
||||
warn(
|
||||
`watch() "immediate" option is only respected when using the ` +
|
||||
`watch(source, callback, options?) signature.`
|
||||
)
|
||||
}
|
||||
if (deep !== undefined) {
|
||||
warn(
|
||||
`watch() "deep" option is only respected when using the ` +
|
||||
`watch(source, callback, options?) signature.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const instance = currentInstance
|
||||
const suspense = currentSuspense
|
||||
|
||||
let getter: () => any
|
||||
if (isArray(source)) {
|
||||
@@ -133,24 +167,39 @@ function doWatch(
|
||||
source,
|
||||
instance,
|
||||
ErrorCodes.WATCH_CALLBACK,
|
||||
[registerCleanup]
|
||||
[onInvalidate]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (deep) {
|
||||
if (cb && deep) {
|
||||
const baseGetter = getter
|
||||
getter = () => traverse(baseGetter())
|
||||
}
|
||||
|
||||
let cleanup: Function
|
||||
const registerCleanup: CleanupRegistrator = (fn: () => void) => {
|
||||
let cleanup: () => void
|
||||
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
|
||||
cleanup = runner.options.onStop = () => {
|
||||
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
|
||||
}
|
||||
}
|
||||
|
||||
let oldValue = isArray(source) ? [] : undefined
|
||||
// in SSR there is no need to setup an actual effect, and it should be noop
|
||||
// unless it's eager
|
||||
if (__NODE_JS__ && isInSSRComponentSetup) {
|
||||
if (!cb) {
|
||||
getter()
|
||||
} else if (immediate) {
|
||||
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
|
||||
getter(),
|
||||
undefined,
|
||||
onInvalidate
|
||||
])
|
||||
}
|
||||
return NOOP
|
||||
}
|
||||
|
||||
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
|
||||
const applyCb = cb
|
||||
? () => {
|
||||
if (instance && instance.isUnmounted) {
|
||||
@@ -164,8 +213,9 @@ function doWatch(
|
||||
}
|
||||
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
|
||||
newValue,
|
||||
oldValue,
|
||||
registerCleanup
|
||||
// pass undefined as the old value when it's changed for the first time
|
||||
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
|
||||
onInvalidate
|
||||
])
|
||||
oldValue = newValue
|
||||
}
|
||||
@@ -177,7 +227,7 @@ function doWatch(
|
||||
scheduler = invoke
|
||||
} else if (flush === 'pre') {
|
||||
scheduler = job => {
|
||||
if (!instance || instance.vnode.el != null) {
|
||||
if (!instance || instance.isMounted) {
|
||||
queueJob(job)
|
||||
} else {
|
||||
// with 'pre' option, the first call must happen before
|
||||
@@ -186,9 +236,7 @@ function doWatch(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scheduler = job => {
|
||||
queuePostRenderEffect(job, suspense)
|
||||
}
|
||||
scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
|
||||
}
|
||||
|
||||
const runner = effect(getter, {
|
||||
@@ -200,19 +248,24 @@ function doWatch(
|
||||
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
|
||||
})
|
||||
|
||||
if (!lazy) {
|
||||
if (applyCb) {
|
||||
scheduler(applyCb)
|
||||
recordInstanceBoundEffect(runner)
|
||||
|
||||
// initial run
|
||||
if (applyCb) {
|
||||
if (immediate) {
|
||||
applyCb()
|
||||
} else {
|
||||
scheduler(runner)
|
||||
oldValue = runner()
|
||||
}
|
||||
} else {
|
||||
oldValue = runner()
|
||||
runner()
|
||||
}
|
||||
|
||||
recordEffect(runner)
|
||||
return () => {
|
||||
stop(runner)
|
||||
if (instance) {
|
||||
remove(instance.effects!, runner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +1,83 @@
|
||||
import { VNode, VNodeChild, isVNode } from './vnode'
|
||||
import { ReactiveEffect, reactive, shallowReadonly } from '@vue/reactivity'
|
||||
import {
|
||||
PublicInstanceProxyHandlers,
|
||||
reactive,
|
||||
ReactiveEffect,
|
||||
pauseTracking,
|
||||
resetTracking
|
||||
} from '@vue/reactivity'
|
||||
import {
|
||||
ComponentPublicInstance,
|
||||
runtimeCompiledRenderProxyHandlers
|
||||
ComponentPublicProxyTarget,
|
||||
PublicInstanceProxyHandlers,
|
||||
RuntimeCompiledPublicInstanceProxyHandlers,
|
||||
createDevProxyTarget,
|
||||
exposePropsOnDevProxyTarget,
|
||||
exposeRenderContextOnDevProxyTarget
|
||||
} from './componentProxy'
|
||||
import { ComponentPropsOptions } from './componentProps'
|
||||
import { Slots } from './componentSlots'
|
||||
import { ComponentPropsOptions, initProps } from './componentProps'
|
||||
import { Slots, initSlots, InternalSlots } from './componentSlots'
|
||||
import { warn } from './warning'
|
||||
import {
|
||||
ErrorCodes,
|
||||
callWithErrorHandling,
|
||||
callWithAsyncErrorHandling
|
||||
} from './errorHandling'
|
||||
import { AppContext, createAppContext, AppConfig } from './apiApp'
|
||||
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
||||
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
|
||||
import { Directive, validateDirectiveName } from './directives'
|
||||
import { applyOptions, ComponentOptions } from './apiOptions'
|
||||
import { applyOptions, ComponentOptions } from './componentOptions'
|
||||
import {
|
||||
EmitsOptions,
|
||||
ObjectEmitsOptions,
|
||||
EmitFn,
|
||||
emit
|
||||
} from './componentEmits'
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
isFunction,
|
||||
capitalize,
|
||||
NOOP,
|
||||
isObject,
|
||||
NO,
|
||||
makeMap,
|
||||
isPromise
|
||||
isPromise,
|
||||
ShapeFlags
|
||||
} from '@vue/shared'
|
||||
import { SuspenseBoundary } from './components/Suspense'
|
||||
import { CompilerOptions } from '@vue/compiler-core'
|
||||
import { currentRenderingInstance } from './componentRenderUtils'
|
||||
import {
|
||||
currentRenderingInstance,
|
||||
markAttrsAccessed
|
||||
} from './componentRenderUtils'
|
||||
import { startMeasure, endMeasure } from './profiling'
|
||||
|
||||
export type Data = { [key: string]: unknown }
|
||||
|
||||
export interface FunctionalComponent<P = {}> {
|
||||
(props: P, ctx: SetupContext): VNodeChild
|
||||
props?: ComponentPropsOptions<P>
|
||||
inheritAttrs?: boolean
|
||||
displayName?: string
|
||||
|
||||
// internal HMR related flags
|
||||
export interface SFCInternalOptions {
|
||||
__scopeId?: string
|
||||
__cssModules?: Data
|
||||
__hmrId?: string
|
||||
__hmrUpdated?: boolean
|
||||
}
|
||||
|
||||
export type Component = ComponentOptions | FunctionalComponent
|
||||
export interface FunctionalComponent<
|
||||
P = {},
|
||||
E extends EmitsOptions = Record<string, any>
|
||||
> extends SFCInternalOptions {
|
||||
(props: P, ctx: SetupContext<E>): any
|
||||
props?: ComponentPropsOptions<P>
|
||||
emits?: E | (keyof E)[]
|
||||
inheritAttrs?: boolean
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
export interface ClassComponent {
|
||||
new (...args: any[]): ComponentPublicInstance<any, any, any, any, any>
|
||||
__vccOpts: ComponentOptions
|
||||
}
|
||||
|
||||
export type Component = ComponentOptions | FunctionalComponent<any>
|
||||
|
||||
// A type used in public APIs where a component type is expected.
|
||||
// The constructor type is an artificial type returned by defineComponent().
|
||||
export type PublicAPIComponent =
|
||||
| Component
|
||||
| { new (...args: any[]): ComponentPublicInstance<any, any, any, any, any> }
|
||||
|
||||
export { ComponentOptions }
|
||||
|
||||
type LifecycleHook = Function[] | null
|
||||
@@ -64,21 +98,23 @@ export const enum LifecycleHooks {
|
||||
ERROR_CAPTURED = 'ec'
|
||||
}
|
||||
|
||||
export type Emit = (event: string, ...args: unknown[]) => void
|
||||
|
||||
export interface SetupContext {
|
||||
export interface SetupContext<E = ObjectEmitsOptions> {
|
||||
attrs: Data
|
||||
slots: Slots
|
||||
emit: Emit
|
||||
emit: EmitFn<E>
|
||||
}
|
||||
|
||||
export type RenderFunction = {
|
||||
(): VNodeChild
|
||||
isRuntimeCompiled?: boolean
|
||||
(
|
||||
ctx: ComponentPublicInstance,
|
||||
cache: ComponentInternalInstance['renderCache']
|
||||
): VNodeChild
|
||||
_rc?: boolean // isRuntimeCompiled
|
||||
}
|
||||
|
||||
export interface ComponentInternalInstance {
|
||||
type: FunctionalComponent | ComponentOptions
|
||||
uid: number
|
||||
type: Component
|
||||
parent: ComponentInternalInstance | null
|
||||
appContext: AppContext
|
||||
root: ComponentInternalInstance
|
||||
@@ -93,7 +129,7 @@ export interface ComponentInternalInstance {
|
||||
accessCache: Data | null
|
||||
// cache for render function values that rely on _ctx but won't need updates
|
||||
// after initialized (e.g. inline handlers)
|
||||
renderCache: (Function | VNode)[] | null
|
||||
renderCache: (Function | VNode)[]
|
||||
|
||||
// assets for fast resolution
|
||||
components: Record<string, Component>
|
||||
@@ -104,19 +140,19 @@ export interface ComponentInternalInstance {
|
||||
data: Data
|
||||
props: Data
|
||||
attrs: Data
|
||||
slots: Slots
|
||||
slots: InternalSlots
|
||||
proxy: ComponentPublicInstance | null
|
||||
proxyTarget: ComponentPublicProxyTarget
|
||||
// alternative proxy used only for runtime-compiled render functions using
|
||||
// `with` block
|
||||
withProxy: ComponentPublicInstance | null
|
||||
propsProxy: Data | null
|
||||
setupContext: SetupContext | null
|
||||
refs: Data
|
||||
emit: Emit
|
||||
emit: EmitFn
|
||||
|
||||
// suspense related
|
||||
suspense: SuspenseBoundary | null
|
||||
asyncDep: Promise<any> | null
|
||||
asyncResult: unknown
|
||||
asyncResolved: boolean
|
||||
|
||||
// storage for any extra properties
|
||||
@@ -146,31 +182,35 @@ export interface ComponentInternalInstance {
|
||||
|
||||
const emptyAppContext = createAppContext()
|
||||
|
||||
let uid = 0
|
||||
|
||||
export function createComponentInstance(
|
||||
vnode: VNode,
|
||||
parent: ComponentInternalInstance | null
|
||||
parent: ComponentInternalInstance | null,
|
||||
suspense: SuspenseBoundary | null
|
||||
) {
|
||||
// inherit parent app context - or - if root, adopt from root vnode
|
||||
const appContext =
|
||||
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
|
||||
const instance: ComponentInternalInstance = {
|
||||
uid: uid++,
|
||||
vnode,
|
||||
parent,
|
||||
appContext,
|
||||
type: vnode.type as Component,
|
||||
root: null!, // set later so it can point to itself
|
||||
root: null!, // to be immediately set
|
||||
next: null,
|
||||
subTree: null!, // will be set synchronously right after creation
|
||||
update: null!, // will be set synchronously right after creation
|
||||
render: null,
|
||||
proxy: null,
|
||||
proxyTarget: null!, // to be immediately set
|
||||
withProxy: null,
|
||||
propsProxy: null,
|
||||
setupContext: null,
|
||||
effects: null,
|
||||
provides: parent ? parent.provides : Object.create(appContext.provides),
|
||||
accessCache: null!,
|
||||
renderCache: null,
|
||||
renderCache: [],
|
||||
|
||||
// setup context properties
|
||||
renderContext: EMPTY_OBJ,
|
||||
@@ -184,9 +224,9 @@ export function createComponentInstance(
|
||||
components: Object.create(appContext.components),
|
||||
directives: Object.create(appContext.directives),
|
||||
|
||||
// async dependency management
|
||||
// suspense related
|
||||
suspense,
|
||||
asyncDep: null,
|
||||
asyncResult: null,
|
||||
asyncResolved: false,
|
||||
|
||||
// user namespace for storing whatever the user assigns to `this`
|
||||
@@ -211,27 +251,19 @@ export function createComponentInstance(
|
||||
rtg: null,
|
||||
rtc: null,
|
||||
ec: null,
|
||||
|
||||
emit: (event, ...args) => {
|
||||
const props = instance.vnode.props || EMPTY_OBJ
|
||||
const handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||
if (handler) {
|
||||
callWithAsyncErrorHandling(
|
||||
handler,
|
||||
instance,
|
||||
ErrorCodes.COMPONENT_EVENT_HANDLER,
|
||||
args
|
||||
)
|
||||
}
|
||||
}
|
||||
emit: null as any // to be set immediately
|
||||
}
|
||||
if (__DEV__) {
|
||||
instance.proxyTarget = createDevProxyTarget(instance)
|
||||
} else {
|
||||
instance.proxyTarget = { _: instance }
|
||||
}
|
||||
|
||||
instance.root = parent ? parent.root : instance
|
||||
instance.emit = emit.bind(null, instance)
|
||||
return instance
|
||||
}
|
||||
|
||||
export let currentInstance: ComponentInternalInstance | null = null
|
||||
export let currentSuspense: SuspenseBoundary | null = null
|
||||
|
||||
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
||||
currentInstance || currentRenderingInstance
|
||||
@@ -253,9 +285,29 @@ export function validateComponentName(name: string, config: AppConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setupStatefulComponent(
|
||||
export let isInSSRComponentSetup = false
|
||||
|
||||
export function setupComponent(
|
||||
instance: ComponentInternalInstance,
|
||||
parentSuspense: SuspenseBoundary | null
|
||||
isSSR = false
|
||||
) {
|
||||
isInSSRComponentSetup = isSSR
|
||||
|
||||
const { props, children, shapeFlag } = instance.vnode
|
||||
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
|
||||
initProps(instance, props, isStateful, isSSR)
|
||||
initSlots(instance, children)
|
||||
|
||||
const setupResult = isStateful
|
||||
? setupStatefulComponent(instance, isSSR)
|
||||
: undefined
|
||||
isInSSRComponentSetup = false
|
||||
return setupResult
|
||||
}
|
||||
|
||||
function setupStatefulComponent(
|
||||
instance: ComponentInternalInstance,
|
||||
isSSR: boolean
|
||||
) {
|
||||
const Component = instance.type as ComponentOptions
|
||||
|
||||
@@ -279,30 +331,34 @@ export function setupStatefulComponent(
|
||||
// 0. create render proxy property access cache
|
||||
instance.accessCache = {}
|
||||
// 1. create public instance / render proxy
|
||||
instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)
|
||||
// 2. create props proxy
|
||||
// the propsProxy is a reactive AND readonly proxy to the actual props.
|
||||
// it will be updated in resolveProps() on updates before render
|
||||
const propsProxy = (instance.propsProxy = shallowReadonly(instance.props))
|
||||
// 3. call setup()
|
||||
instance.proxy = new Proxy(instance.proxyTarget, PublicInstanceProxyHandlers)
|
||||
if (__DEV__) {
|
||||
exposePropsOnDevProxyTarget(instance)
|
||||
}
|
||||
// 2. call setup()
|
||||
const { setup } = Component
|
||||
if (setup) {
|
||||
const setupContext = (instance.setupContext =
|
||||
setup.length > 1 ? createSetupContext(instance) : null)
|
||||
|
||||
currentInstance = instance
|
||||
currentSuspense = parentSuspense
|
||||
pauseTracking()
|
||||
const setupResult = callWithErrorHandling(
|
||||
setup,
|
||||
instance,
|
||||
ErrorCodes.SETUP_FUNCTION,
|
||||
[propsProxy, setupContext]
|
||||
[instance.props, setupContext]
|
||||
)
|
||||
resetTracking()
|
||||
currentInstance = null
|
||||
currentSuspense = null
|
||||
|
||||
if (isPromise(setupResult)) {
|
||||
if (__FEATURE_SUSPENSE__) {
|
||||
if (isSSR) {
|
||||
// return the promise so server-renderer can wait on it
|
||||
return setupResult.then((resolvedResult: unknown) => {
|
||||
handleSetupResult(instance, resolvedResult, isSSR)
|
||||
})
|
||||
} else if (__FEATURE_SUSPENSE__) {
|
||||
// async setup returned Promise.
|
||||
// bail here and wait for re-entry.
|
||||
instance.asyncDep = setupResult
|
||||
@@ -313,17 +369,17 @@ export function setupStatefulComponent(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
handleSetupResult(instance, setupResult, parentSuspense)
|
||||
handleSetupResult(instance, setupResult, isSSR)
|
||||
}
|
||||
} else {
|
||||
finishComponentSetup(instance, parentSuspense)
|
||||
finishComponentSetup(instance, isSSR)
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSetupResult(
|
||||
instance: ComponentInternalInstance,
|
||||
setupResult: unknown,
|
||||
parentSuspense: SuspenseBoundary | null
|
||||
isSSR: boolean
|
||||
) {
|
||||
if (isFunction(setupResult)) {
|
||||
// setup returned an inline render function
|
||||
@@ -338,6 +394,9 @@ export function handleSetupResult(
|
||||
// setup returned bindings.
|
||||
// assuming a render function compiled from template is present.
|
||||
instance.renderContext = reactive(setupResult)
|
||||
if (__DEV__) {
|
||||
exposeRenderContextOnDevProxyTarget(instance)
|
||||
}
|
||||
} else if (__DEV__ && setupResult !== undefined) {
|
||||
warn(
|
||||
`setup() should return an object. Received: ${
|
||||
@@ -345,7 +404,7 @@ export function handleSetupResult(
|
||||
}`
|
||||
)
|
||||
}
|
||||
finishComponentSetup(instance, parentSuspense)
|
||||
finishComponentSetup(instance, isSSR)
|
||||
}
|
||||
|
||||
type CompileFunction = (
|
||||
@@ -362,31 +421,40 @@ export function registerRuntimeCompiler(_compile: any) {
|
||||
|
||||
function finishComponentSetup(
|
||||
instance: ComponentInternalInstance,
|
||||
parentSuspense: SuspenseBoundary | null
|
||||
isSSR: boolean
|
||||
) {
|
||||
const Component = instance.type as ComponentOptions
|
||||
if (!instance.render) {
|
||||
if (__RUNTIME_COMPILE__ && Component.template && !Component.render) {
|
||||
// __RUNTIME_COMPILE__ ensures `compile` is provided
|
||||
Component.render = compile!(Component.template, {
|
||||
|
||||
// template / render function normalization
|
||||
if (__NODE_JS__ && isSSR) {
|
||||
if (Component.render) {
|
||||
instance.render = Component.render as RenderFunction
|
||||
}
|
||||
} else if (!instance.render) {
|
||||
if (compile && Component.template && !Component.render) {
|
||||
if (__DEV__) {
|
||||
startMeasure(instance, `compile`)
|
||||
}
|
||||
Component.render = compile(Component.template, {
|
||||
isCustomElement: instance.appContext.config.isCustomElement || NO
|
||||
})
|
||||
if (__DEV__) {
|
||||
endMeasure(instance, `compile`)
|
||||
}
|
||||
// mark the function as runtime compiled
|
||||
;(Component.render as RenderFunction)._rc = true
|
||||
}
|
||||
|
||||
if (__DEV__ && !Component.render) {
|
||||
/* istanbul ignore if */
|
||||
if (!__RUNTIME_COMPILE__ && Component.template) {
|
||||
if (!compile && Component.template) {
|
||||
warn(
|
||||
`Component provides template but the build of Vue you are running ` +
|
||||
`does not support runtime template compilation. Either use the ` +
|
||||
`full build or pre-compile the template using Vue CLI.`
|
||||
)
|
||||
} else {
|
||||
warn(
|
||||
`Component is missing${
|
||||
__RUNTIME_COMPILE__ ? ` template or` : ``
|
||||
} render function.`
|
||||
)
|
||||
warn(`Component is missing template or render function.`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,10 +463,10 @@ function finishComponentSetup(
|
||||
// for runtime-compiled render functions using `with` blocks, the render
|
||||
// proxy used needs a different `has` handler which is more performant and
|
||||
// also only allows a whitelist of globals to fallthrough.
|
||||
if (__RUNTIME_COMPILE__ && instance.render.isRuntimeCompiled) {
|
||||
if (instance.render._rc) {
|
||||
instance.withProxy = new Proxy(
|
||||
instance,
|
||||
runtimeCompiledRenderProxyHandlers
|
||||
instance.proxyTarget,
|
||||
RuntimeCompiledPublicInstanceProxyHandlers
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -406,42 +474,85 @@ function finishComponentSetup(
|
||||
// support for 2.x options
|
||||
if (__FEATURE_OPTIONS__) {
|
||||
currentInstance = instance
|
||||
currentSuspense = parentSuspense
|
||||
applyOptions(instance, Component)
|
||||
currentInstance = null
|
||||
currentSuspense = null
|
||||
}
|
||||
|
||||
if (instance.renderContext === EMPTY_OBJ) {
|
||||
instance.renderContext = reactive({})
|
||||
}
|
||||
}
|
||||
|
||||
// used to identify a setup context proxy
|
||||
export const SetupProxySymbol = Symbol()
|
||||
|
||||
const SetupProxyHandlers: { [key: string]: ProxyHandler<any> } = {}
|
||||
;['attrs', 'slots'].forEach((type: string) => {
|
||||
SetupProxyHandlers[type] = {
|
||||
get: (instance, key) => instance[type][key],
|
||||
has: (instance, key) => key === SetupProxySymbol || key in instance[type],
|
||||
ownKeys: instance => Reflect.ownKeys(instance[type]),
|
||||
// this is necessary for ownKeys to work properly
|
||||
getOwnPropertyDescriptor: (instance, key) =>
|
||||
Reflect.getOwnPropertyDescriptor(instance[type], key),
|
||||
set: () => false,
|
||||
deleteProperty: () => false
|
||||
const slotsHandlers: ProxyHandler<InternalSlots> = {
|
||||
set: () => {
|
||||
warn(`setupContext.slots is readonly.`)
|
||||
return false
|
||||
},
|
||||
deleteProperty: () => {
|
||||
warn(`setupContext.slots is readonly.`)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const attrHandlers: ProxyHandler<Data> = {
|
||||
get: (target, key: string) => {
|
||||
markAttrsAccessed()
|
||||
return target[key]
|
||||
},
|
||||
set: () => {
|
||||
warn(`setupContext.attrs is readonly.`)
|
||||
return false
|
||||
},
|
||||
deleteProperty: () => {
|
||||
warn(`setupContext.attrs is readonly.`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
|
||||
const context = {
|
||||
// attrs & slots are non-reactive, but they need to always expose
|
||||
// the latest values (instance.xxx may get replaced during updates) so we
|
||||
// need to expose them through a proxy
|
||||
attrs: new Proxy(instance, SetupProxyHandlers.attrs),
|
||||
slots: new Proxy(instance, SetupProxyHandlers.slots),
|
||||
emit: instance.emit
|
||||
if (__DEV__) {
|
||||
// We use getters in dev in case libs like test-utils overwrite instance
|
||||
// properties (overwrites should not be done in prod)
|
||||
return Object.freeze({
|
||||
get attrs() {
|
||||
return new Proxy(instance.attrs, attrHandlers)
|
||||
},
|
||||
get slots() {
|
||||
return new Proxy(instance.slots, slotsHandlers)
|
||||
},
|
||||
get emit() {
|
||||
return instance.emit
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
attrs: instance.attrs,
|
||||
slots: instance.slots,
|
||||
emit: instance.emit
|
||||
}
|
||||
}
|
||||
return __DEV__ ? Object.freeze(context) : context
|
||||
}
|
||||
|
||||
// record effects created during a component's setup() so that they can be
|
||||
// stopped when the component unmounts
|
||||
export function recordInstanceBoundEffect(effect: ReactiveEffect) {
|
||||
if (currentInstance) {
|
||||
;(currentInstance.effects || (currentInstance.effects = [])).push(effect)
|
||||
}
|
||||
}
|
||||
|
||||
const classifyRE = /(?:^|[-_])(\w)/g
|
||||
const classify = (str: string): string =>
|
||||
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
|
||||
|
||||
export function formatComponentName(
|
||||
Component: Component,
|
||||
file?: string
|
||||
): string {
|
||||
let name = isFunction(Component)
|
||||
? Component.displayName || Component.name
|
||||
: Component.name
|
||||
if (!name && file) {
|
||||
const match = file.match(/([^/\\]+)\.vue$/)
|
||||
if (match) {
|
||||
name = match[1]
|
||||
}
|
||||
}
|
||||
return name ? classify(name) : 'Anonymous'
|
||||
}
|
||||
|
||||
119
packages/runtime-core/src/componentEmits.ts
Normal file
119
packages/runtime-core/src/componentEmits.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
isArray,
|
||||
isOn,
|
||||
hasOwn,
|
||||
EMPTY_OBJ,
|
||||
capitalize,
|
||||
hyphenate,
|
||||
isFunction,
|
||||
def
|
||||
} from '@vue/shared'
|
||||
import { ComponentInternalInstance } from './component'
|
||||
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
|
||||
import { warn } from './warning'
|
||||
|
||||
export type ObjectEmitsOptions = Record<
|
||||
string,
|
||||
((...args: any[]) => any) | null
|
||||
>
|
||||
export type EmitsOptions = ObjectEmitsOptions | string[]
|
||||
|
||||
type UnionToIntersection<U> = (U extends any
|
||||
? (k: U) => void
|
||||
: never) extends ((k: infer I) => void)
|
||||
? I
|
||||
: never
|
||||
|
||||
export type EmitFn<
|
||||
Options = ObjectEmitsOptions,
|
||||
Event extends keyof Options = keyof Options
|
||||
> = Options extends any[]
|
||||
? (event: Options[0], ...args: any[]) => unknown[]
|
||||
: UnionToIntersection<
|
||||
{
|
||||
[key in Event]: Options[key] extends ((...args: infer Args) => any)
|
||||
? (event: key, ...args: Args) => unknown[]
|
||||
: (event: key, ...args: any[]) => unknown[]
|
||||
}[Event]
|
||||
>
|
||||
|
||||
export function emit(
|
||||
instance: ComponentInternalInstance,
|
||||
event: string,
|
||||
...args: any[]
|
||||
): any[] {
|
||||
const props = instance.vnode.props || EMPTY_OBJ
|
||||
|
||||
if (__DEV__) {
|
||||
const options = normalizeEmitsOptions(instance.type.emits)
|
||||
if (options) {
|
||||
if (!(event in options)) {
|
||||
warn(
|
||||
`Component emitted event "${event}" but it is not declared in the ` +
|
||||
`emits option.`
|
||||
)
|
||||
} else {
|
||||
const validator = options[event]
|
||||
if (isFunction(validator)) {
|
||||
const isValid = validator(...args)
|
||||
if (!isValid) {
|
||||
warn(
|
||||
`Invalid event arguments: event validation failed for event "${event}".`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||
// for v-model update:xxx events, also trigger kebab-case equivalent
|
||||
// for props passed via kebab-case
|
||||
if (!handler && event.indexOf('update:') === 0) {
|
||||
event = hyphenate(event)
|
||||
handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||
}
|
||||
if (handler) {
|
||||
const res = callWithAsyncErrorHandling(
|
||||
handler,
|
||||
instance,
|
||||
ErrorCodes.COMPONENT_EVENT_HANDLER,
|
||||
args
|
||||
)
|
||||
return isArray(res) ? res : [res]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeEmitsOptions(
|
||||
options: EmitsOptions | undefined
|
||||
): ObjectEmitsOptions | undefined {
|
||||
if (!options) {
|
||||
return
|
||||
} else if (isArray(options)) {
|
||||
if ((options as any)._n) {
|
||||
return (options as any)._n
|
||||
}
|
||||
const normalized: ObjectEmitsOptions = {}
|
||||
options.forEach(key => (normalized[key] = null))
|
||||
def(options, '_n', normalized)
|
||||
return normalized
|
||||
} else {
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
// Check if an incoming prop key is a declared emit event listener.
|
||||
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
|
||||
// both considered matched listeners.
|
||||
export function isEmitListener(emits: EmitsOptions, key: string): boolean {
|
||||
return (
|
||||
isOn(key) &&
|
||||
(hasOwn(
|
||||
(emits = normalizeEmitsOptions(emits) as ObjectEmitsOptions),
|
||||
key[2].toLowerCase() + key.slice(3)
|
||||
) ||
|
||||
hasOwn(emits, key.slice(2)))
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
Data,
|
||||
Component,
|
||||
SetupContext,
|
||||
RenderFunction
|
||||
RenderFunction,
|
||||
SFCInternalOptions,
|
||||
PublicAPIComponent,
|
||||
Component
|
||||
} from './component'
|
||||
import {
|
||||
isFunction,
|
||||
@@ -12,10 +14,11 @@ import {
|
||||
isObject,
|
||||
isArray,
|
||||
EMPTY_OBJ,
|
||||
NOOP
|
||||
NOOP,
|
||||
hasOwn
|
||||
} from '@vue/shared'
|
||||
import { computed } from './apiReactivity'
|
||||
import { watch, WatchOptions, WatchHandler } from './apiWatch'
|
||||
import { computed } from './apiComputed'
|
||||
import { watch, WatchOptions, WatchCallback } from './apiWatch'
|
||||
import { provide, inject } from './apiInject'
|
||||
import {
|
||||
onBeforeMount,
|
||||
@@ -35,9 +38,15 @@ import {
|
||||
import {
|
||||
reactive,
|
||||
ComputedGetter,
|
||||
WritableComputedOptions
|
||||
WritableComputedOptions,
|
||||
ComputedRef
|
||||
} from '@vue/reactivity'
|
||||
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
|
||||
import {
|
||||
ComponentObjectPropsOptions,
|
||||
ExtractPropTypes,
|
||||
normalizePropsOptions
|
||||
} from './componentProps'
|
||||
import { EmitsOptions } from './componentEmits'
|
||||
import { Directive } from './directives'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { warn } from './warning'
|
||||
@@ -47,12 +56,14 @@ export interface ComponentOptionsBase<
|
||||
RawBindings,
|
||||
D,
|
||||
C extends ComputedOptions,
|
||||
M extends MethodOptions
|
||||
> extends LegacyOptions<Props, RawBindings, D, C, M> {
|
||||
M extends MethodOptions,
|
||||
E extends EmitsOptions,
|
||||
EE extends string = string
|
||||
> extends LegacyOptions<Props, D, C, M>, SFCInternalOptions {
|
||||
setup?: (
|
||||
this: null,
|
||||
this: void,
|
||||
props: Props,
|
||||
ctx: SetupContext
|
||||
ctx: SetupContext<E>
|
||||
) => RawBindings | RenderFunction | void
|
||||
name?: string
|
||||
template?: string | object // can be a direct DOM node
|
||||
@@ -62,21 +73,31 @@ export interface ComponentOptionsBase<
|
||||
// Luckily `render()` doesn't need any arguments nor does it care about return
|
||||
// type.
|
||||
render?: Function
|
||||
components?: Record<string, Component>
|
||||
// SSR only. This is produced by compiler-ssr and attached in compiler-sfc
|
||||
// not user facing, so the typing is lax and for test only.
|
||||
ssrRender?: (
|
||||
ctx: any,
|
||||
push: (item: any) => void,
|
||||
parentInstance: ComponentInternalInstance
|
||||
) => void
|
||||
components?: Record<string, PublicAPIComponent>
|
||||
directives?: Record<string, Directive>
|
||||
inheritAttrs?: boolean
|
||||
emits?: E | EE[]
|
||||
|
||||
// SFC & dev only
|
||||
__scopeId?: string
|
||||
__hmrId?: string
|
||||
__hmrUpdated?: boolean
|
||||
// Internal ------------------------------------------------------------------
|
||||
|
||||
// marker for AsyncComponentWrapper
|
||||
__asyncLoader?: () => Promise<Component>
|
||||
// cache for merged $options
|
||||
__merged?: ComponentOptions
|
||||
|
||||
// type-only differentiator to separate OptionWithoutProps from a constructor
|
||||
// type returned by createComponent() or FunctionalComponent
|
||||
// type returned by defineComponent() or FunctionalComponent
|
||||
call?: never
|
||||
// type-only differentiators for built-in Vnode types
|
||||
__isFragment?: never
|
||||
__isPortal?: never
|
||||
__isTeleport?: never
|
||||
__isSuspense?: never
|
||||
}
|
||||
|
||||
@@ -85,10 +106,14 @@ export type ComponentOptionsWithoutProps<
|
||||
RawBindings = {},
|
||||
D = {},
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {}
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = EmitsOptions,
|
||||
EE extends string = string
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||
props?: undefined
|
||||
} & ThisType<ComponentPublicInstance<{}, RawBindings, D, C, M, Readonly<Props>>>
|
||||
} & ThisType<
|
||||
ComponentPublicInstance<{}, RawBindings, D, C, M, E, Readonly<Props>>
|
||||
>
|
||||
|
||||
export type ComponentOptionsWithArrayProps<
|
||||
PropNames extends string = string,
|
||||
@@ -96,10 +121,12 @@ export type ComponentOptionsWithArrayProps<
|
||||
D = {},
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = EmitsOptions,
|
||||
EE extends string = string,
|
||||
Props = Readonly<{ [key in PropNames]?: any }>
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||
props: PropNames[]
|
||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
|
||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
|
||||
|
||||
export type ComponentOptionsWithObjectProps<
|
||||
PropsOptions = ComponentObjectPropsOptions,
|
||||
@@ -107,18 +134,17 @@ export type ComponentOptionsWithObjectProps<
|
||||
D = {},
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = EmitsOptions,
|
||||
EE extends string = string,
|
||||
Props = Readonly<ExtractPropTypes<PropsOptions>>
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||
props: PropsOptions
|
||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
|
||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
|
||||
|
||||
export type ComponentOptions =
|
||||
| ComponentOptionsWithoutProps
|
||||
| ComponentOptionsWithObjectProps
|
||||
| ComponentOptionsWithArrayProps
|
||||
|
||||
// TODO legacy component definition also supports constructors with .options
|
||||
type LegacyComponent = ComponentOptions
|
||||
| ComponentOptionsWithoutProps<any, any, any, any, any>
|
||||
| ComponentOptionsWithObjectProps<any, any, any, any, any>
|
||||
| ComponentOptionsWithArrayProps<any, any, any, any, any>
|
||||
|
||||
export type ComputedOptions = Record<
|
||||
string,
|
||||
@@ -137,8 +163,8 @@ export type ExtractComputedReturns<T extends any> = {
|
||||
|
||||
type WatchOptionItem =
|
||||
| string
|
||||
| WatchHandler
|
||||
| { handler: WatchHandler } & WatchOptions
|
||||
| WatchCallback
|
||||
| { handler: WatchCallback } & WatchOptions
|
||||
|
||||
type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]
|
||||
|
||||
@@ -153,18 +179,21 @@ type ComponentInjectOptions =
|
||||
|
||||
export interface LegacyOptions<
|
||||
Props,
|
||||
RawBindings,
|
||||
D,
|
||||
C extends ComputedOptions,
|
||||
M extends MethodOptions
|
||||
> {
|
||||
el?: any
|
||||
// allow any custom options
|
||||
[key: string]: any
|
||||
|
||||
// state
|
||||
// Limitation: we cannot expose RawBindings on the `this` context for data
|
||||
// since that leads to some sort of circular inference and breaks ThisType
|
||||
// for the entire component.
|
||||
data?: D | ((this: ComponentPublicInstance<Props>) => D)
|
||||
data?: (
|
||||
this: ComponentPublicInstance<Props>,
|
||||
vm: ComponentPublicInstance<Props>
|
||||
) => D
|
||||
computed?: C
|
||||
methods?: M
|
||||
watch?: ComponentWatchOptions
|
||||
@@ -172,8 +201,8 @@ export interface LegacyOptions<
|
||||
inject?: ComponentInjectOptions
|
||||
|
||||
// composition
|
||||
mixins?: LegacyComponent[]
|
||||
extends?: LegacyComponent
|
||||
mixins?: ComponentOptions[]
|
||||
extends?: ComponentOptions
|
||||
|
||||
// lifecycle
|
||||
beforeCreate?(): void
|
||||
@@ -215,10 +244,7 @@ export function applyOptions(
|
||||
options: ComponentOptions,
|
||||
asMixin: boolean = false
|
||||
) {
|
||||
const renderContext =
|
||||
instance.renderContext === EMPTY_OBJ
|
||||
? (instance.renderContext = reactive({}))
|
||||
: instance.renderContext
|
||||
const proxyTarget = instance.proxyTarget
|
||||
const ctx = instance.proxy!
|
||||
const {
|
||||
// composition
|
||||
@@ -249,9 +275,15 @@ export function applyOptions(
|
||||
errorCaptured
|
||||
} = options
|
||||
|
||||
const renderContext =
|
||||
instance.renderContext === EMPTY_OBJ &&
|
||||
(computedOptions || methods || watchOptions || injectOptions)
|
||||
? (instance.renderContext = reactive({}))
|
||||
: instance.renderContext
|
||||
|
||||
const globalMixins = instance.appContext.mixins
|
||||
// call it only during dev
|
||||
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
|
||||
|
||||
// applyOptions is called non-as-mixin once per instance
|
||||
if (!asMixin) {
|
||||
callSyncHook('beforeCreate', options, ctx, globalMixins)
|
||||
@@ -267,21 +299,30 @@ export function applyOptions(
|
||||
applyMixins(instance, mixins)
|
||||
}
|
||||
|
||||
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
|
||||
|
||||
if (__DEV__ && propsOptions) {
|
||||
for (const key in propsOptions) {
|
||||
for (const key in normalizePropsOptions(propsOptions)[0]) {
|
||||
checkDuplicateProperties!(OptionTypes.PROPS, key)
|
||||
}
|
||||
}
|
||||
|
||||
// state options
|
||||
if (dataOptions) {
|
||||
const data = isFunction(dataOptions) ? dataOptions.call(ctx) : dataOptions
|
||||
if (__DEV__ && !isFunction(dataOptions)) {
|
||||
warn(
|
||||
`The data option must be a function. ` +
|
||||
`Plain object usage is no longer supported.`
|
||||
)
|
||||
}
|
||||
const data = dataOptions.call(ctx, ctx)
|
||||
if (!isObject(data)) {
|
||||
__DEV__ && warn(`data() should return an object.`)
|
||||
} else if (instance.data === EMPTY_OBJ) {
|
||||
if (__DEV__) {
|
||||
for (const key in data) {
|
||||
checkDuplicateProperties!(OptionTypes.DATA, key)
|
||||
if (!(key in proxyTarget)) proxyTarget[key] = data[key]
|
||||
}
|
||||
}
|
||||
instance.data = reactive(data)
|
||||
@@ -290,19 +331,17 @@ export function applyOptions(
|
||||
extend(instance.data, data)
|
||||
}
|
||||
}
|
||||
|
||||
if (computedOptions) {
|
||||
for (const key in computedOptions) {
|
||||
const opt = (computedOptions as ComputedOptions)[key]
|
||||
|
||||
__DEV__ && checkDuplicateProperties!(OptionTypes.COMPUTED, key)
|
||||
|
||||
if (isFunction(opt)) {
|
||||
renderContext[key] = computed(opt.bind(ctx))
|
||||
renderContext[key] = computed(opt.bind(ctx, ctx))
|
||||
} else {
|
||||
const { get, set } = opt
|
||||
if (isFunction(get)) {
|
||||
renderContext[key] = computed({
|
||||
get: get.bind(ctx),
|
||||
get: get.bind(ctx, ctx),
|
||||
set: isFunction(set)
|
||||
? set.bind(ctx)
|
||||
: __DEV__
|
||||
@@ -317,6 +356,15 @@ export function applyOptions(
|
||||
warn(`Computed property "${key}" has no getter.`)
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
checkDuplicateProperties!(OptionTypes.COMPUTED, key)
|
||||
if (renderContext[key] && !(key in proxyTarget)) {
|
||||
Object.defineProperty(proxyTarget, key, {
|
||||
enumerable: true,
|
||||
get: () => (renderContext[key] as ComputedRef).value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,8 +372,13 @@ export function applyOptions(
|
||||
for (const key in methods) {
|
||||
const methodHandler = (methods as MethodOptions)[key]
|
||||
if (isFunction(methodHandler)) {
|
||||
__DEV__ && checkDuplicateProperties!(OptionTypes.METHODS, key)
|
||||
renderContext[key] = methodHandler.bind(ctx)
|
||||
if (__DEV__) {
|
||||
checkDuplicateProperties!(OptionTypes.METHODS, key)
|
||||
if (!(key in proxyTarget)) {
|
||||
proxyTarget[key] = renderContext[key]
|
||||
}
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
`Method "${key}" has type "${typeof methodHandler}" in the component definition. ` +
|
||||
@@ -334,11 +387,13 @@ export function applyOptions(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (watchOptions) {
|
||||
for (const key in watchOptions) {
|
||||
createWatcher(watchOptions[key], renderContext, ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
if (provideOptions) {
|
||||
const provides = isFunction(provideOptions)
|
||||
? provideOptions.call(ctx)
|
||||
@@ -347,22 +402,29 @@ export function applyOptions(
|
||||
provide(key, provides[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (injectOptions) {
|
||||
if (isArray(injectOptions)) {
|
||||
for (let i = 0; i < injectOptions.length; i++) {
|
||||
const key = injectOptions[i]
|
||||
__DEV__ && checkDuplicateProperties!(OptionTypes.INJECT, key)
|
||||
renderContext[key] = inject(key)
|
||||
if (__DEV__) {
|
||||
checkDuplicateProperties!(OptionTypes.INJECT, key)
|
||||
proxyTarget[key] = renderContext[key]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const key in injectOptions) {
|
||||
__DEV__ && checkDuplicateProperties!(OptionTypes.INJECT, key)
|
||||
const opt = injectOptions[key]
|
||||
if (isObject(opt)) {
|
||||
renderContext[key] = inject(opt.from, opt.default)
|
||||
} else {
|
||||
renderContext[key] = inject(opt)
|
||||
}
|
||||
if (__DEV__) {
|
||||
checkDuplicateProperties!(OptionTypes.INJECT, key)
|
||||
proxyTarget[key] = renderContext[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,7 +529,7 @@ function createWatcher(
|
||||
if (isString(raw)) {
|
||||
const handler = renderContext[raw]
|
||||
if (isFunction(handler)) {
|
||||
watch(getter, handler as WatchHandler)
|
||||
watch(getter, handler as WatchCallback)
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid watch handler specified by key "${raw}"`, handler)
|
||||
}
|
||||
@@ -483,3 +545,31 @@ function createWatcher(
|
||||
warn(`Invalid watch option: "${key}"`)
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveMergedOptions(
|
||||
instance: ComponentInternalInstance
|
||||
): ComponentOptions {
|
||||
const raw = instance.type as ComponentOptions
|
||||
const { __merged, mixins, extends: extendsOptions } = raw
|
||||
if (__merged) return __merged
|
||||
const globalMixins = instance.appContext.mixins
|
||||
if (!globalMixins.length && !mixins && !extendsOptions) return raw
|
||||
const options = {}
|
||||
globalMixins.forEach(m => mergeOptions(options, m, instance))
|
||||
extendsOptions && mergeOptions(options, extendsOptions, instance)
|
||||
mixins && mixins.forEach(m => mergeOptions(options, m, instance))
|
||||
mergeOptions(options, raw, instance)
|
||||
return (raw.__merged = options)
|
||||
}
|
||||
|
||||
function mergeOptions(to: any, from: any, instance: ComponentInternalInstance) {
|
||||
const strats = instance.appContext.config.optionMergeStrategies
|
||||
for (const key in from) {
|
||||
const strat = strats && strats[key]
|
||||
if (strat) {
|
||||
to[key] = strat(to[key], from[key], instance.proxy, key)
|
||||
} else if (!hasOwn(to, key)) {
|
||||
to[key] = from[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { toRaw, lock, unlock } from '@vue/reactivity'
|
||||
import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity'
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
camelize,
|
||||
@@ -11,10 +11,15 @@ import {
|
||||
hasOwn,
|
||||
toRawType,
|
||||
PatchFlags,
|
||||
makeMap
|
||||
makeMap,
|
||||
isReservedProp,
|
||||
EMPTY_ARR,
|
||||
def
|
||||
} from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { Data, ComponentInternalInstance } from './component'
|
||||
import { isEmitListener } from './componentEmits'
|
||||
import { InternalObjectSymbol } from './vnode'
|
||||
|
||||
export type ComponentPropsOptions<P = Data> =
|
||||
| ComponentObjectPropsOptions<P>
|
||||
@@ -37,7 +42,14 @@ interface PropOptions<T = any> {
|
||||
|
||||
export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
|
||||
|
||||
type PropConstructor<T = any> = { new (...args: any[]): T & object } | { (): T }
|
||||
type PropConstructor<T = any> =
|
||||
| { new (...args: any[]): T & object }
|
||||
| { (): T }
|
||||
| PropMethod<T>
|
||||
|
||||
type PropMethod<T> = T extends (...args: any) => any // if is function with args
|
||||
? { new (): T; (): T; readonly proptotype: Function } // Create Function like contructor
|
||||
: never
|
||||
|
||||
type RequiredKeys<T, MakeDefaultRequired> = {
|
||||
[K in keyof T]: T[K] extends
|
||||
@@ -84,115 +96,95 @@ type NormalizedProp =
|
||||
// and an array of prop keys that need value casting (booleans and defaults)
|
||||
type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
|
||||
|
||||
// resolve raw VNode data.
|
||||
// - filter out reserved keys (key, ref, slots)
|
||||
// - extract class and style into $attrs (to be merged onto child
|
||||
// component root)
|
||||
// - for the rest:
|
||||
// - if has declared props: put declared ones in `props`, the rest in `attrs`
|
||||
// - else: everything goes in `props`.
|
||||
|
||||
export function resolveProps(
|
||||
export function initProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null,
|
||||
_options: ComponentPropsOptions | void
|
||||
isStateful: number, // result of bitwise flag comparison
|
||||
isSSR = false
|
||||
) {
|
||||
const hasDeclaredProps = _options != null
|
||||
if (!rawProps && !hasDeclaredProps) {
|
||||
return
|
||||
const props: Data = {}
|
||||
const attrs: Data = {}
|
||||
def(attrs, InternalObjectSymbol, true)
|
||||
setFullProps(instance, rawProps, props, attrs)
|
||||
const options = instance.type.props
|
||||
// validation
|
||||
if (__DEV__ && options && rawProps) {
|
||||
validateProps(props, options)
|
||||
}
|
||||
|
||||
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
|
||||
const props: Data = {}
|
||||
let attrs: Data | undefined = void 0
|
||||
|
||||
// update the instance propsProxy (passed to setup()) to trigger potential
|
||||
// changes
|
||||
const propsProxy = instance.propsProxy
|
||||
const setProp = propsProxy
|
||||
? (key: string, val: unknown) => {
|
||||
props[key] = val
|
||||
propsProxy[key] = val
|
||||
}
|
||||
: (key: string, val: unknown) => {
|
||||
props[key] = val
|
||||
}
|
||||
if (isStateful) {
|
||||
// stateful
|
||||
instance.props = isSSR ? props : shallowReadonly(props)
|
||||
} else {
|
||||
if (!options) {
|
||||
// functional w/ optional props, props === attrs
|
||||
instance.props = attrs
|
||||
} else {
|
||||
// functional w/ declared props
|
||||
instance.props = props
|
||||
}
|
||||
}
|
||||
instance.attrs = attrs
|
||||
}
|
||||
|
||||
export function updateProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null,
|
||||
optimized: boolean
|
||||
) {
|
||||
// allow mutation of propsProxy (which is readonly by default)
|
||||
unlock()
|
||||
|
||||
if (rawProps != null) {
|
||||
for (const key in rawProps) {
|
||||
// key, ref are reserved and never passed down
|
||||
if (key === 'key' || key === 'ref') continue
|
||||
// prop option names are camelized during normalization, so to support
|
||||
// kebab -> camel conversion here we need to camelize the key.
|
||||
const camelKey = camelize(key)
|
||||
if (hasDeclaredProps && !hasOwn(options, camelKey)) {
|
||||
// Any non-declared props are put into a separate `attrs` object
|
||||
// for spreading. Make sure to preserve original key casing
|
||||
;(attrs || (attrs = {}))[key] = rawProps[key]
|
||||
} else {
|
||||
setProp(camelKey, rawProps[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasDeclaredProps) {
|
||||
// set default values & cast booleans
|
||||
for (let i = 0; i < needCastKeys.length; i++) {
|
||||
const key = needCastKeys[i]
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
const isAbsent = !hasOwn(props, key)
|
||||
const hasDefault = hasOwn(opt, 'default')
|
||||
const currentValue = props[key]
|
||||
// default values
|
||||
if (hasDefault && currentValue === undefined) {
|
||||
const defaultValue = opt.default
|
||||
setProp(key, isFunction(defaultValue) ? defaultValue() : defaultValue)
|
||||
}
|
||||
// boolean casting
|
||||
if (opt[BooleanFlags.shouldCast]) {
|
||||
if (isAbsent && !hasDefault) {
|
||||
setProp(key, false)
|
||||
} else if (
|
||||
opt[BooleanFlags.shouldCastTrue] &&
|
||||
(currentValue === '' || currentValue === hyphenate(key))
|
||||
) {
|
||||
setProp(key, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
// validation
|
||||
if (__DEV__ && rawProps) {
|
||||
for (const key in options) {
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
let rawValue
|
||||
if (!(key in rawProps) && hyphenate(key) in rawProps) {
|
||||
rawValue = rawProps[hyphenate(key)]
|
||||
const {
|
||||
props,
|
||||
attrs,
|
||||
vnode: { patchFlag }
|
||||
} = instance
|
||||
const rawOptions = instance.type.props
|
||||
const rawCurrentProps = toRaw(props)
|
||||
const { 0: options } = normalizePropsOptions(rawOptions)
|
||||
|
||||
if ((optimized || patchFlag > 0) && !(patchFlag & PatchFlags.FULL_PROPS)) {
|
||||
if (patchFlag & PatchFlags.PROPS) {
|
||||
// Compiler-generated props & no keys change, just set the updated
|
||||
// the props.
|
||||
const propsToUpdate = instance.vnode.dynamicProps!
|
||||
for (let i = 0; i < propsToUpdate.length; i++) {
|
||||
const key = propsToUpdate[i]
|
||||
// PROPS flag guarantees rawProps to be non-null
|
||||
const value = rawProps![key]
|
||||
if (options) {
|
||||
// attr / props separation was done on init and will be consistent
|
||||
// in this code path, so just check if attrs have it.
|
||||
if (hasOwn(attrs, key)) {
|
||||
attrs[key] = value
|
||||
} else {
|
||||
const camelizedKey = camelize(key)
|
||||
props[camelizedKey] = resolvePropValue(
|
||||
options,
|
||||
rawCurrentProps,
|
||||
camelizedKey,
|
||||
value
|
||||
)
|
||||
}
|
||||
} else {
|
||||
rawValue = rawProps[key]
|
||||
attrs[key] = value
|
||||
}
|
||||
validateProp(key, toRaw(rawValue), opt, !hasOwn(props, key))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if component has no declared props, $attrs === $props
|
||||
attrs = props
|
||||
}
|
||||
|
||||
// in case of dynamic props, check if we need to delete keys from
|
||||
// the props proxy
|
||||
const { patchFlag } = instance.vnode
|
||||
if (
|
||||
propsProxy !== null &&
|
||||
(patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)
|
||||
) {
|
||||
const rawInitialProps = toRaw(propsProxy)
|
||||
for (const key in rawInitialProps) {
|
||||
if (!hasOwn(props, key)) {
|
||||
delete propsProxy[key]
|
||||
// full props update.
|
||||
setFullProps(instance, rawProps, props, attrs)
|
||||
// in case of dynamic props, check if we need to delete keys from
|
||||
// the props object
|
||||
for (const key in rawCurrentProps) {
|
||||
if (!rawProps || !hasOwn(rawProps, key)) {
|
||||
delete props[key]
|
||||
}
|
||||
}
|
||||
for (const key in attrs) {
|
||||
if (!rawProps || !hasOwn(rawProps, key)) {
|
||||
delete attrs[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,25 +192,91 @@ export function resolveProps(
|
||||
// lock readonly
|
||||
lock()
|
||||
|
||||
instance.props = props
|
||||
instance.attrs = options ? attrs || EMPTY_OBJ : props
|
||||
if (__DEV__ && rawOptions && rawProps) {
|
||||
validateProps(props, rawOptions)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizationMap = new WeakMap<
|
||||
ComponentPropsOptions,
|
||||
NormalizedPropsOptions
|
||||
>()
|
||||
function setFullProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null,
|
||||
props: Data,
|
||||
attrs: Data
|
||||
) {
|
||||
const { 0: options, 1: needCastKeys } = normalizePropsOptions(
|
||||
instance.type.props
|
||||
)
|
||||
const emits = instance.type.emits
|
||||
|
||||
function normalizePropsOptions(
|
||||
raw: ComponentPropsOptions | void
|
||||
): NormalizedPropsOptions {
|
||||
if (rawProps) {
|
||||
for (const key in rawProps) {
|
||||
const value = rawProps[key]
|
||||
// key, ref are reserved and never passed down
|
||||
if (isReservedProp(key)) {
|
||||
continue
|
||||
}
|
||||
// prop option names are camelized during normalization, so to support
|
||||
// kebab -> camel conversion here we need to camelize the key.
|
||||
let camelKey
|
||||
if (options && hasOwn(options, (camelKey = camelize(key)))) {
|
||||
props[camelKey] = value
|
||||
} else if (!emits || !isEmitListener(emits, key)) {
|
||||
// Any non-declared (either as a prop or an emitted event) props are put
|
||||
// into a separate `attrs` object for spreading. Make sure to preserve
|
||||
// original key casing
|
||||
attrs[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needCastKeys) {
|
||||
for (let i = 0; i < needCastKeys.length; i++) {
|
||||
const key = needCastKeys[i]
|
||||
props[key] = resolvePropValue(options!, props, key, props[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePropValue(
|
||||
options: NormalizedPropsOptions[0],
|
||||
props: Data,
|
||||
key: string,
|
||||
value: unknown
|
||||
) {
|
||||
let opt = options[key]
|
||||
if (opt == null) {
|
||||
return value
|
||||
}
|
||||
const hasDefault = hasOwn(opt, 'default')
|
||||
// default values
|
||||
if (hasDefault && value === undefined) {
|
||||
const defaultValue = opt.default
|
||||
value = isFunction(defaultValue) ? defaultValue() : defaultValue
|
||||
}
|
||||
// boolean casting
|
||||
if (opt[BooleanFlags.shouldCast]) {
|
||||
if (!hasOwn(props, key) && !hasDefault) {
|
||||
value = false
|
||||
} else if (
|
||||
opt[BooleanFlags.shouldCastTrue] &&
|
||||
(value === '' || value === hyphenate(key))
|
||||
) {
|
||||
value = true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizePropsOptions(
|
||||
raw: ComponentPropsOptions | undefined
|
||||
): NormalizedPropsOptions | [] {
|
||||
if (!raw) {
|
||||
return [] as any
|
||||
return EMPTY_ARR as any
|
||||
}
|
||||
if (normalizationMap.has(raw)) {
|
||||
return normalizationMap.get(raw)!
|
||||
if ((raw as any)._n) {
|
||||
return (raw as any)._n
|
||||
}
|
||||
const options: NormalizedPropsOptions[0] = {}
|
||||
const normalized: NormalizedPropsOptions[0] = {}
|
||||
const needCastKeys: NormalizedPropsOptions[1] = []
|
||||
if (isArray(raw)) {
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
@@ -226,10 +284,8 @@ function normalizePropsOptions(
|
||||
warn(`props must be strings when using array syntax.`, raw[i])
|
||||
}
|
||||
const normalizedKey = camelize(raw[i])
|
||||
if (normalizedKey[0] !== '$') {
|
||||
options[normalizedKey] = EMPTY_OBJ
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`)
|
||||
if (validatePropName(normalizedKey)) {
|
||||
normalized[normalizedKey] = EMPTY_OBJ
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -238,28 +294,27 @@ function normalizePropsOptions(
|
||||
}
|
||||
for (const key in raw) {
|
||||
const normalizedKey = camelize(key)
|
||||
if (normalizedKey[0] !== '$') {
|
||||
if (validatePropName(normalizedKey)) {
|
||||
const opt = raw[key]
|
||||
const prop: NormalizedProp = (options[normalizedKey] =
|
||||
const prop: NormalizedProp = (normalized[normalizedKey] =
|
||||
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
|
||||
if (prop != null) {
|
||||
if (prop) {
|
||||
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
||||
const stringIndex = getTypeIndex(String, prop.type)
|
||||
prop[BooleanFlags.shouldCast] = booleanIndex > -1
|
||||
prop[BooleanFlags.shouldCastTrue] = booleanIndex < stringIndex
|
||||
prop[BooleanFlags.shouldCastTrue] =
|
||||
stringIndex < 0 || booleanIndex < stringIndex
|
||||
// if the prop needs boolean casting or default value
|
||||
if (booleanIndex > -1 || hasOwn(prop, 'default')) {
|
||||
needCastKeys.push(normalizedKey)
|
||||
}
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
const normalized: NormalizedPropsOptions = [options, needCastKeys]
|
||||
normalizationMap.set(raw, normalized)
|
||||
return normalized
|
||||
const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
|
||||
def(raw, '_n', normalizedEntry)
|
||||
return normalizedEntry
|
||||
}
|
||||
|
||||
// use function string name to check type constructors
|
||||
@@ -283,15 +338,29 @@ function getTypeIndex(
|
||||
return i
|
||||
}
|
||||
}
|
||||
} else if (isObject(expectedTypes)) {
|
||||
} else if (isFunction(expectedTypes)) {
|
||||
return isSameType(expectedTypes, type) ? 0 : -1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type AssertionResult = {
|
||||
valid: boolean
|
||||
expectedType: string
|
||||
function validateProps(props: Data, rawOptions: ComponentPropsOptions) {
|
||||
const rawValues = toRaw(props)
|
||||
const options = normalizePropsOptions(rawOptions)[0]
|
||||
for (const key in options) {
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
validateProp(key, rawValues[key], opt, !hasOwn(rawValues, key))
|
||||
}
|
||||
}
|
||||
|
||||
function validatePropName(key: string) {
|
||||
if (key[0] !== '$') {
|
||||
return true
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${key}" is a reserved property.`)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function validateProp(
|
||||
@@ -336,6 +405,11 @@ const isSimpleType = /*#__PURE__*/ makeMap(
|
||||
'String,Number,Boolean,Function,Symbol'
|
||||
)
|
||||
|
||||
type AssertionResult = {
|
||||
valid: boolean
|
||||
expectedType: string
|
||||
}
|
||||
|
||||
function assertType(value: unknown, type: PropConstructor): AssertionResult {
|
||||
let valid
|
||||
const expectedType = getType(type)
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import { ComponentInternalInstance, Data, Emit } from './component'
|
||||
import { ComponentInternalInstance, Data } from './component'
|
||||
import { nextTick, queueJob } from './scheduler'
|
||||
import { instanceWatch } from './apiWatch'
|
||||
import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted } from '@vue/shared'
|
||||
import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared'
|
||||
import { ReactiveEffect, UnwrapRef, toRaw } from '@vue/reactivity'
|
||||
import {
|
||||
ExtractComputedReturns,
|
||||
ComponentOptionsBase,
|
||||
ComputedOptions,
|
||||
MethodOptions
|
||||
} from './apiOptions'
|
||||
import { UnwrapRef, ReactiveEffect } from '@vue/reactivity'
|
||||
import { warn } from './warning'
|
||||
MethodOptions,
|
||||
resolveMergedOptions
|
||||
} from './componentOptions'
|
||||
import { normalizePropsOptions } from './componentProps'
|
||||
import { EmitsOptions, EmitFn } from './componentEmits'
|
||||
import { Slots } from './componentSlots'
|
||||
import {
|
||||
currentRenderingInstance,
|
||||
markAttrsAccessed
|
||||
} from './componentRenderUtils'
|
||||
import { warn } from './warning'
|
||||
|
||||
// public properties exposed on the proxy, which is used as the render context
|
||||
// in templates (as `this` in the render option)
|
||||
export type ComponentPublicInstance<
|
||||
P = {},
|
||||
B = {},
|
||||
D = {},
|
||||
P = {}, // props type extracted from props option
|
||||
B = {}, // raw bindings returned from setup()
|
||||
D = {}, // return from data()
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = {},
|
||||
PublicProps = P
|
||||
> = {
|
||||
$: ComponentInternalInstance
|
||||
$data: D
|
||||
$props: PublicProps
|
||||
$attrs: Data
|
||||
@@ -33,9 +38,9 @@ export type ComponentPublicInstance<
|
||||
$slots: Slots
|
||||
$root: ComponentInternalInstance | null
|
||||
$parent: ComponentInternalInstance | null
|
||||
$emit: Emit
|
||||
$emit: EmitFn<E>
|
||||
$el: any
|
||||
$options: ComponentOptionsBase<P, B, D, C, M>
|
||||
$options: ComponentOptionsBase<P, B, D, C, M, E>
|
||||
$forceUpdate: ReactiveEffect
|
||||
$nextTick: typeof nextTick
|
||||
$watch: typeof instanceWatch
|
||||
@@ -51,42 +56,43 @@ const publicPropertiesMap: Record<
|
||||
> = {
|
||||
$: i => i,
|
||||
$el: i => i.vnode.el,
|
||||
$cache: i => i.renderCache || (i.renderCache = []),
|
||||
$data: i => i.data,
|
||||
$props: i => i.propsProxy,
|
||||
$props: i => i.props,
|
||||
$attrs: i => i.attrs,
|
||||
$slots: i => i.slots,
|
||||
$refs: i => i.refs,
|
||||
$parent: i => i.parent,
|
||||
$root: i => i.root,
|
||||
$parent: i => i.parent && i.parent.proxy,
|
||||
$root: i => i.root && i.root.proxy,
|
||||
$emit: i => i.emit,
|
||||
$options: i => i.type,
|
||||
$options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type),
|
||||
$forceUpdate: i => () => queueJob(i.update),
|
||||
$nextTick: () => nextTick,
|
||||
$watch: i => instanceWatch.bind(i)
|
||||
$watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP
|
||||
}
|
||||
|
||||
const enum AccessTypes {
|
||||
DATA,
|
||||
CONTEXT,
|
||||
PROPS
|
||||
PROPS,
|
||||
OTHER
|
||||
}
|
||||
|
||||
export interface ComponentPublicProxyTarget {
|
||||
[key: string]: any
|
||||
_: ComponentInternalInstance
|
||||
}
|
||||
|
||||
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
get(target: ComponentInternalInstance, key: string) {
|
||||
// fast path for unscopables when using `with` block
|
||||
if (__RUNTIME_COMPILE__ && (key as any) === Symbol.unscopables) {
|
||||
return
|
||||
}
|
||||
get({ _: instance }: ComponentPublicProxyTarget, key: string) {
|
||||
const {
|
||||
renderContext,
|
||||
data,
|
||||
props,
|
||||
propsProxy,
|
||||
accessCache,
|
||||
type,
|
||||
sink
|
||||
} = target
|
||||
sink,
|
||||
appContext
|
||||
} = instance
|
||||
|
||||
// data / props / renderContext
|
||||
// This getter gets called for every property access on the render context
|
||||
@@ -94,41 +100,58 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
// is the multiple hasOwn() calls. It's much faster to do a simple property
|
||||
// access on a plain object, so we use an accessCache object (with null
|
||||
// prototype) to memoize what access type a key corresponds to.
|
||||
const n = accessCache![key]
|
||||
if (n !== undefined) {
|
||||
switch (n) {
|
||||
case AccessTypes.DATA:
|
||||
return data[key]
|
||||
case AccessTypes.CONTEXT:
|
||||
return renderContext[key]
|
||||
case AccessTypes.PROPS:
|
||||
return propsProxy![key]
|
||||
if (key[0] !== '$') {
|
||||
const n = accessCache![key]
|
||||
if (n !== undefined) {
|
||||
switch (n) {
|
||||
case AccessTypes.DATA:
|
||||
return data[key]
|
||||
case AccessTypes.CONTEXT:
|
||||
return renderContext[key]
|
||||
case AccessTypes.PROPS:
|
||||
return props![key]
|
||||
// default: just fallthrough
|
||||
}
|
||||
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
|
||||
accessCache![key] = AccessTypes.DATA
|
||||
return data[key]
|
||||
} else if (renderContext !== EMPTY_OBJ && hasOwn(renderContext, key)) {
|
||||
accessCache![key] = AccessTypes.CONTEXT
|
||||
return renderContext[key]
|
||||
} else if (type.props) {
|
||||
// only cache other properties when instance has declared (thus stable)
|
||||
// props
|
||||
if (hasOwn(normalizePropsOptions(type.props)[0]!, key)) {
|
||||
accessCache![key] = AccessTypes.PROPS
|
||||
// return the value from propsProxy for ref unwrapping and readonly
|
||||
return props![key]
|
||||
} else {
|
||||
accessCache![key] = AccessTypes.OTHER
|
||||
}
|
||||
}
|
||||
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
|
||||
accessCache![key] = AccessTypes.DATA
|
||||
return data[key]
|
||||
} else if (hasOwn(renderContext, key)) {
|
||||
accessCache![key] = AccessTypes.CONTEXT
|
||||
return renderContext[key]
|
||||
} else if (hasOwn(props, key)) {
|
||||
// only cache props access if component has declared (thus stable) props
|
||||
if (type.props != null) {
|
||||
accessCache![key] = AccessTypes.PROPS
|
||||
}
|
||||
// return the value from propsProxy for ref unwrapping and readonly
|
||||
return propsProxy![key]
|
||||
}
|
||||
|
||||
// public $xxx properties & user-attached properties (sink)
|
||||
const publicGetter = publicPropertiesMap[key]
|
||||
if (publicGetter !== undefined) {
|
||||
let cssModule, globalProperties
|
||||
if (publicGetter) {
|
||||
if (__DEV__ && key === '$attrs') {
|
||||
markAttrsAccessed()
|
||||
}
|
||||
return publicGetter(target)
|
||||
return publicGetter(instance)
|
||||
} else if (hasOwn(sink, key)) {
|
||||
return sink[key]
|
||||
} else if (__DEV__ && currentRenderingInstance != null) {
|
||||
} else if (
|
||||
(cssModule = type.__cssModules) &&
|
||||
(cssModule = cssModule[key])
|
||||
) {
|
||||
return cssModule
|
||||
} else if (
|
||||
((globalProperties = appContext.config.globalProperties),
|
||||
hasOwn(globalProperties, key))
|
||||
) {
|
||||
return globalProperties[key]
|
||||
} else if (__DEV__ && currentRenderingInstance) {
|
||||
warn(
|
||||
`Property ${JSON.stringify(key)} was accessed during render ` +
|
||||
`but is not defined on instance.`
|
||||
@@ -136,46 +159,152 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
}
|
||||
},
|
||||
|
||||
has(target: ComponentInternalInstance, key: string) {
|
||||
const { data, accessCache, renderContext, type, sink } = target
|
||||
return (
|
||||
accessCache![key] !== undefined ||
|
||||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
|
||||
hasOwn(renderContext, key) ||
|
||||
(type.props != null && hasOwn(type.props, key)) ||
|
||||
hasOwn(publicPropertiesMap, key) ||
|
||||
hasOwn(sink, key)
|
||||
)
|
||||
},
|
||||
|
||||
set(target: ComponentInternalInstance, key: string, value: any): boolean {
|
||||
const { data, renderContext } = target
|
||||
set(
|
||||
{ _: instance }: ComponentPublicProxyTarget,
|
||||
key: string,
|
||||
value: any
|
||||
): boolean {
|
||||
const { data, renderContext } = instance
|
||||
if (data !== EMPTY_OBJ && hasOwn(data, key)) {
|
||||
data[key] = value
|
||||
} else if (hasOwn(renderContext, key)) {
|
||||
renderContext[key] = value
|
||||
} else if (key[0] === '$' && key.slice(1) in target) {
|
||||
} else if (key[0] === '$' && key.slice(1) in instance) {
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Attempting to mutate public property "${key}". ` +
|
||||
`Properties starting with $ are reserved and readonly.`,
|
||||
target
|
||||
instance
|
||||
)
|
||||
return false
|
||||
} else if (key in target.props) {
|
||||
} else if (key in instance.props) {
|
||||
__DEV__ &&
|
||||
warn(`Attempting to mutate prop "${key}". Props are readonly.`, target)
|
||||
warn(
|
||||
`Attempting to mutate prop "${key}". Props are readonly.`,
|
||||
instance
|
||||
)
|
||||
return false
|
||||
} else {
|
||||
target.sink[key] = value
|
||||
instance.sink[key] = value
|
||||
if (__DEV__) {
|
||||
instance.proxyTarget[key] = value
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
has(
|
||||
{
|
||||
_: { data, accessCache, renderContext, type, sink, appContext }
|
||||
}: ComponentPublicProxyTarget,
|
||||
key: string
|
||||
) {
|
||||
return (
|
||||
accessCache![key] !== undefined ||
|
||||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
|
||||
hasOwn(renderContext, key) ||
|
||||
(type.props && hasOwn(normalizePropsOptions(type.props)[0]!, key)) ||
|
||||
hasOwn(publicPropertiesMap, key) ||
|
||||
hasOwn(sink, key) ||
|
||||
hasOwn(appContext.config.globalProperties, key)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const runtimeCompiledRenderProxyHandlers = {
|
||||
if (__DEV__ && !__TEST__) {
|
||||
PublicInstanceProxyHandlers.ownKeys = (
|
||||
target: ComponentPublicProxyTarget
|
||||
) => {
|
||||
warn(
|
||||
`Avoid app logic that relies on enumerating keys on a component instance. ` +
|
||||
`The keys will be empty in production mode to avoid performance overhead.`
|
||||
)
|
||||
return Reflect.ownKeys(target)
|
||||
}
|
||||
}
|
||||
|
||||
export const RuntimeCompiledPublicInstanceProxyHandlers = {
|
||||
...PublicInstanceProxyHandlers,
|
||||
has(_target: ComponentInternalInstance, key: string) {
|
||||
get(target: ComponentPublicProxyTarget, key: string) {
|
||||
// fast path for unscopables when using `with` block
|
||||
if ((key as any) === Symbol.unscopables) {
|
||||
return
|
||||
}
|
||||
return PublicInstanceProxyHandlers.get!(target, key, target)
|
||||
},
|
||||
has(_: ComponentPublicProxyTarget, key: string) {
|
||||
return key[0] !== '_' && !isGloballyWhitelisted(key)
|
||||
}
|
||||
}
|
||||
|
||||
// In dev mode, the proxy target exposes the same properties as seen on `this`
|
||||
// for easier console inspection. In prod mode it will be an empty object so
|
||||
// these properties definitions can be skipped.
|
||||
export function createDevProxyTarget(instance: ComponentInternalInstance) {
|
||||
const target: Record<string, any> = {}
|
||||
|
||||
// expose internal instance for proxy handlers
|
||||
Object.defineProperty(target, `_`, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: () => instance
|
||||
})
|
||||
|
||||
// expose public properties
|
||||
Object.keys(publicPropertiesMap).forEach(key => {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: () => publicPropertiesMap[key](instance),
|
||||
// intercepted by the proxy so no need for implementation,
|
||||
// but needed to prevent set errors
|
||||
set: NOOP
|
||||
})
|
||||
})
|
||||
|
||||
// expose global properties
|
||||
const { globalProperties } = instance.appContext.config
|
||||
Object.keys(globalProperties).forEach(key => {
|
||||
Object.defineProperty(target, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: () => globalProperties[key],
|
||||
set: NOOP
|
||||
})
|
||||
})
|
||||
|
||||
return target as ComponentPublicProxyTarget
|
||||
}
|
||||
|
||||
export function exposePropsOnDevProxyTarget(
|
||||
instance: ComponentInternalInstance
|
||||
) {
|
||||
const {
|
||||
proxyTarget,
|
||||
type: { props: propsOptions }
|
||||
} = instance
|
||||
if (propsOptions) {
|
||||
Object.keys(normalizePropsOptions(propsOptions)[0]!).forEach(key => {
|
||||
Object.defineProperty(proxyTarget, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: () => instance.props[key],
|
||||
set: NOOP
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function exposeRenderContextOnDevProxyTarget(
|
||||
instance: ComponentInternalInstance
|
||||
) {
|
||||
const { proxyTarget, renderContext } = instance
|
||||
Object.keys(toRaw(renderContext)).forEach(key => {
|
||||
Object.defineProperty(proxyTarget, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: () => renderContext[key],
|
||||
set: NOOP
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,17 +8,25 @@ import {
|
||||
normalizeVNode,
|
||||
createVNode,
|
||||
Comment,
|
||||
cloneVNode
|
||||
cloneVNode,
|
||||
Fragment,
|
||||
VNodeArrayChildren,
|
||||
isVNode
|
||||
} from './vnode'
|
||||
import { ShapeFlags } from './shapeFlags'
|
||||
import { handleError, ErrorCodes } from './errorHandling'
|
||||
import { PatchFlags, EMPTY_OBJ } from '@vue/shared'
|
||||
import { PatchFlags, ShapeFlags, isOn } from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
|
||||
// mark the current rendering instance for asset resolution (e.g.
|
||||
// resolveComponent, resolveDirective) during render
|
||||
export let currentRenderingInstance: ComponentInternalInstance | null = null
|
||||
|
||||
export function setCurrentRenderingInstance(
|
||||
instance: ComponentInternalInstance | null
|
||||
) {
|
||||
currentRenderingInstance = instance
|
||||
}
|
||||
|
||||
// dev only flag to track whether $attrs was used during render.
|
||||
// If $attrs was used during render then the warning for failed attrs
|
||||
// fallthrough can be suppressed.
|
||||
@@ -33,13 +41,15 @@ export function renderComponentRoot(
|
||||
): VNode {
|
||||
const {
|
||||
type: Component,
|
||||
parent,
|
||||
vnode,
|
||||
proxy,
|
||||
withProxy,
|
||||
props,
|
||||
slots,
|
||||
attrs,
|
||||
emit
|
||||
emit,
|
||||
renderCache
|
||||
} = instance
|
||||
|
||||
let result
|
||||
@@ -48,8 +58,15 @@ export function renderComponentRoot(
|
||||
accessedAttrs = false
|
||||
}
|
||||
try {
|
||||
let fallthroughAttrs
|
||||
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
|
||||
result = normalizeVNode(instance.render!.call(withProxy || proxy))
|
||||
// withProxy is a proxy with a different `has` trap only for
|
||||
// runtime-compiled render functions using `with` block.
|
||||
const proxyToUse = withProxy || proxy
|
||||
result = normalizeVNode(
|
||||
instance.render!.call(proxyToUse, proxyToUse!, renderCache)
|
||||
)
|
||||
fallthroughAttrs = attrs
|
||||
} else {
|
||||
// functional
|
||||
const render = Component as FunctionalComponent
|
||||
@@ -62,52 +79,127 @@ export function renderComponentRoot(
|
||||
})
|
||||
: render(props, null as any /* we know it doesn't need it */)
|
||||
)
|
||||
fallthroughAttrs = Component.props ? attrs : getFallthroughAttrs(attrs)
|
||||
}
|
||||
|
||||
// attr merging
|
||||
// in dev mode, comments are preserved, and it's possible for a template
|
||||
// to have comments along side the root element which makes it a fragment
|
||||
let root = result
|
||||
let setRoot: ((root: VNode) => void) | undefined = undefined
|
||||
if (__DEV__) {
|
||||
;[root, setRoot] = getChildRoot(result)
|
||||
}
|
||||
|
||||
if (
|
||||
Component.props != null &&
|
||||
Component.inheritAttrs !== false &&
|
||||
attrs !== EMPTY_OBJ &&
|
||||
Object.keys(attrs).length
|
||||
fallthroughAttrs &&
|
||||
Object.keys(fallthroughAttrs).length
|
||||
) {
|
||||
if (
|
||||
result.shapeFlag & ShapeFlags.ELEMENT ||
|
||||
result.shapeFlag & ShapeFlags.COMPONENT
|
||||
root.shapeFlag & ShapeFlags.ELEMENT ||
|
||||
root.shapeFlag & ShapeFlags.COMPONENT
|
||||
) {
|
||||
result = cloneVNode(result, attrs)
|
||||
} else if (__DEV__ && !accessedAttrs && result.type !== Comment) {
|
||||
root = cloneVNode(root, fallthroughAttrs)
|
||||
// If the child root node is a compiler optimized vnode, make sure it
|
||||
// force update full props to account for the merged attrs.
|
||||
if (root.dynamicChildren) {
|
||||
root.patchFlag |= PatchFlags.FULL_PROPS
|
||||
}
|
||||
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
|
||||
warn(
|
||||
`Extraneous non-props attributes (${Object.keys(attrs).join(',')}) ` +
|
||||
`Extraneous non-props attributes (` +
|
||||
`${Object.keys(attrs).join(', ')}) ` +
|
||||
`were passed to component but could not be automatically inherited ` +
|
||||
`because component renders fragment or text root nodes.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// inherit scopeId
|
||||
const parentScopeId = parent && parent.type.__scopeId
|
||||
if (parentScopeId) {
|
||||
root = cloneVNode(root, { [parentScopeId]: '' })
|
||||
}
|
||||
// inherit directives
|
||||
if (vnode.dirs) {
|
||||
if (__DEV__ && !isElementRoot(root)) {
|
||||
warn(
|
||||
`Runtime directive used on component with non-element root node. ` +
|
||||
`The directives will not function as intended.`
|
||||
)
|
||||
}
|
||||
root.dirs = vnode.dirs
|
||||
}
|
||||
// inherit transition data
|
||||
if (vnode.transition != null) {
|
||||
if (
|
||||
__DEV__ &&
|
||||
!(result.shapeFlag & ShapeFlags.COMPONENT) &&
|
||||
!(result.shapeFlag & ShapeFlags.ELEMENT) &&
|
||||
result.type !== Comment
|
||||
) {
|
||||
if (vnode.transition) {
|
||||
if (__DEV__ && !isElementRoot(root)) {
|
||||
warn(
|
||||
`Component inside <Transition> renders non-element root node ` +
|
||||
`that cannot be animated.`
|
||||
)
|
||||
}
|
||||
result.transition = vnode.transition
|
||||
root.transition = vnode.transition
|
||||
}
|
||||
|
||||
if (__DEV__ && setRoot) {
|
||||
setRoot(root)
|
||||
} else {
|
||||
result = root
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
|
||||
result = createVNode(Comment)
|
||||
}
|
||||
currentRenderingInstance = null
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const getChildRoot = (
|
||||
vnode: VNode
|
||||
): [VNode, ((root: VNode) => void) | undefined] => {
|
||||
if (vnode.type !== Fragment) {
|
||||
return [vnode, undefined]
|
||||
}
|
||||
const rawChildren = vnode.children as VNodeArrayChildren
|
||||
const dynamicChildren = vnode.dynamicChildren as VNodeArrayChildren
|
||||
const children = rawChildren.filter(child => {
|
||||
return !(isVNode(child) && child.type === Comment)
|
||||
})
|
||||
if (children.length !== 1) {
|
||||
return [vnode, undefined]
|
||||
}
|
||||
const childRoot = children[0]
|
||||
const index = rawChildren.indexOf(childRoot)
|
||||
const dynamicIndex = dynamicChildren
|
||||
? dynamicChildren.indexOf(childRoot)
|
||||
: null
|
||||
const setRoot = (updatedRoot: VNode) => {
|
||||
rawChildren[index] = updatedRoot
|
||||
if (dynamicIndex !== null) dynamicChildren[dynamicIndex] = updatedRoot
|
||||
}
|
||||
return [normalizeVNode(childRoot), setRoot]
|
||||
}
|
||||
|
||||
const getFallthroughAttrs = (attrs: Data): Data | undefined => {
|
||||
let res: Data | undefined
|
||||
for (const key in attrs) {
|
||||
if (key === 'class' || key === 'style' || isOn(key)) {
|
||||
;(res || (res = {}))[key] = attrs[key]
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const isElementRoot = (vnode: VNode) => {
|
||||
return (
|
||||
vnode.shapeFlag & ShapeFlags.COMPONENT ||
|
||||
vnode.shapeFlag & ShapeFlags.ELEMENT ||
|
||||
vnode.type === Comment // potential v-if branch switch
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldUpdateComponent(
|
||||
prevVNode: VNode,
|
||||
nextVNode: VNode,
|
||||
@@ -130,6 +222,11 @@ export function shouldUpdateComponent(
|
||||
return true
|
||||
}
|
||||
|
||||
// force child update for runtime directive or transition on component vnode.
|
||||
if (nextVNode.dirs || nextVNode.transition) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (patchFlag > 0) {
|
||||
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
|
||||
// slot content that references values that might have changed,
|
||||
@@ -139,34 +236,43 @@ export function shouldUpdateComponent(
|
||||
if (patchFlag & PatchFlags.FULL_PROPS) {
|
||||
// presence of this flag indicates props are always non-null
|
||||
return hasPropsChanged(prevProps!, nextProps!)
|
||||
} else if (patchFlag & PatchFlags.PROPS) {
|
||||
const dynamicProps = nextVNode.dynamicProps!
|
||||
for (let i = 0; i < dynamicProps.length; i++) {
|
||||
const key = dynamicProps[i]
|
||||
if (nextProps![key] !== prevProps![key]) {
|
||||
return true
|
||||
} else {
|
||||
if (patchFlag & PatchFlags.CLASS) {
|
||||
return prevProps!.class !== nextProps!.class
|
||||
}
|
||||
if (patchFlag & PatchFlags.STYLE) {
|
||||
return hasPropsChanged(prevProps!.style, nextProps!.style)
|
||||
}
|
||||
if (patchFlag & PatchFlags.PROPS) {
|
||||
const dynamicProps = nextVNode.dynamicProps!
|
||||
for (let i = 0; i < dynamicProps.length; i++) {
|
||||
const key = dynamicProps[i]
|
||||
if (nextProps![key] !== prevProps![key]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!optimized) {
|
||||
// this path is only taken by manually written render functions
|
||||
// so presence of any children leads to a forced update
|
||||
if (prevChildren != null || nextChildren != null) {
|
||||
if (nextChildren == null || !(nextChildren as any).$stable) {
|
||||
if (prevChildren || nextChildren) {
|
||||
if (!nextChildren || !(nextChildren as any).$stable) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (prevProps === nextProps) {
|
||||
return false
|
||||
}
|
||||
if (prevProps === null) {
|
||||
return nextProps !== null
|
||||
if (!prevProps) {
|
||||
return !!nextProps
|
||||
}
|
||||
if (nextProps === null) {
|
||||
if (!nextProps) {
|
||||
return true
|
||||
}
|
||||
return hasPropsChanged(prevProps, nextProps)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -186,7 +292,7 @@ function hasPropsChanged(prevProps: Data, nextProps: Data): boolean {
|
||||
|
||||
export function updateHOCHostEl(
|
||||
{ vnode, parent }: ComponentInternalInstance,
|
||||
el: object // HostNode
|
||||
el: typeof vnode.el // HostNode
|
||||
) {
|
||||
while (parent && parent.subTree === vnode) {
|
||||
;(vnode = parent.vnode).el = el
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import { ComponentInternalInstance, currentInstance } from './component'
|
||||
import { VNode, NormalizedChildren, normalizeVNode, VNodeChild } from './vnode'
|
||||
import { isArray, isFunction, EMPTY_OBJ } from '@vue/shared'
|
||||
import { ShapeFlags } from './shapeFlags'
|
||||
import {
|
||||
VNode,
|
||||
VNodeNormalizedChildren,
|
||||
normalizeVNode,
|
||||
VNodeChild,
|
||||
InternalObjectSymbol
|
||||
} from './vnode'
|
||||
import {
|
||||
isArray,
|
||||
isFunction,
|
||||
EMPTY_OBJ,
|
||||
ShapeFlags,
|
||||
PatchFlags,
|
||||
extend,
|
||||
def
|
||||
} from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { isKeepAlive } from './components/KeepAlive'
|
||||
import { withCtx } from './helpers/withRenderContext'
|
||||
|
||||
export type Slot = (...args: any[]) => VNode[]
|
||||
|
||||
export type InternalSlots = {
|
||||
[name: string]: Slot
|
||||
[name: string]: Slot | undefined
|
||||
}
|
||||
|
||||
export type Slots = Readonly<InternalSlots>
|
||||
@@ -17,67 +31,129 @@ export type RawSlots = {
|
||||
[name: string]: unknown
|
||||
// manual render fn hint to skip forced children updates
|
||||
$stable?: boolean
|
||||
// internal, indicates compiler generated slots = can skip normalization
|
||||
_compiled?: boolean
|
||||
// internal, for tracking slot owner instance. This is attached during
|
||||
// normalizeChildren when the component vnode is created.
|
||||
_ctx?: ComponentInternalInstance | null
|
||||
// internal, indicates compiler generated slots
|
||||
_?: 1
|
||||
}
|
||||
|
||||
const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'
|
||||
|
||||
const normalizeSlotValue = (value: unknown): VNode[] =>
|
||||
isArray(value)
|
||||
? value.map(normalizeVNode)
|
||||
: [normalizeVNode(value as VNodeChild)]
|
||||
|
||||
const normalizeSlot = (key: string, rawSlot: Function): Slot => (
|
||||
props: any
|
||||
) => {
|
||||
if (__DEV__ && currentInstance != null) {
|
||||
warn(
|
||||
`Slot "${key}" invoked outside of the render function: ` +
|
||||
`this will not track dependencies used in the slot. ` +
|
||||
`Invoke the slot function inside the render function instead.`
|
||||
)
|
||||
}
|
||||
return normalizeSlotValue(rawSlot(props))
|
||||
}
|
||||
|
||||
export function resolveSlots(
|
||||
instance: ComponentInternalInstance,
|
||||
children: NormalizedChildren
|
||||
) {
|
||||
let slots: InternalSlots | void
|
||||
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
|
||||
const rawSlots = children as RawSlots
|
||||
if (rawSlots._compiled) {
|
||||
// pre-normalized slots object generated by compiler
|
||||
slots = children as Slots
|
||||
} else {
|
||||
slots = {}
|
||||
for (const key in rawSlots) {
|
||||
if (key === '$stable') continue
|
||||
const value = rawSlots[key]
|
||||
if (isFunction(value)) {
|
||||
slots[key] = normalizeSlot(key, value)
|
||||
} else if (value != null) {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`Non-function value encountered for slot "${key}". ` +
|
||||
`Prefer function slots for better performance.`
|
||||
)
|
||||
}
|
||||
const normalized = normalizeSlotValue(value)
|
||||
slots[key] = () => normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (children !== null) {
|
||||
// non slot object children (direct value) passed to a component
|
||||
if (__DEV__ && !isKeepAlive(instance.vnode)) {
|
||||
const normalizeSlot = (
|
||||
key: string,
|
||||
rawSlot: Function,
|
||||
ctx: ComponentInternalInstance | null | undefined
|
||||
): Slot =>
|
||||
withCtx((props: any) => {
|
||||
if (__DEV__ && currentInstance) {
|
||||
warn(
|
||||
`Non-function value encountered for default slot. ` +
|
||||
`Prefer function slots for better performance.`
|
||||
`Slot "${key}" invoked outside of the render function: ` +
|
||||
`this will not track dependencies used in the slot. ` +
|
||||
`Invoke the slot function inside the render function instead.`
|
||||
)
|
||||
}
|
||||
const normalized = normalizeSlotValue(children)
|
||||
slots = { default: () => normalized }
|
||||
return normalizeSlotValue(rawSlot(props))
|
||||
}, ctx)
|
||||
|
||||
const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => {
|
||||
const ctx = rawSlots._ctx
|
||||
for (const key in rawSlots) {
|
||||
if (isInternalKey(key)) continue
|
||||
const value = rawSlots[key]
|
||||
if (isFunction(value)) {
|
||||
slots[key] = normalizeSlot(key, value, ctx)
|
||||
} else if (value != null) {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`Non-function value encountered for slot "${key}". ` +
|
||||
`Prefer function slots for better performance.`
|
||||
)
|
||||
}
|
||||
const normalized = normalizeSlotValue(value)
|
||||
slots[key] = () => normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeVNodeSlots = (
|
||||
instance: ComponentInternalInstance,
|
||||
children: VNodeNormalizedChildren
|
||||
) => {
|
||||
if (__DEV__ && !isKeepAlive(instance.vnode)) {
|
||||
warn(
|
||||
`Non-function value encountered for default slot. ` +
|
||||
`Prefer function slots for better performance.`
|
||||
)
|
||||
}
|
||||
const normalized = normalizeSlotValue(children)
|
||||
instance.slots.default = () => normalized
|
||||
}
|
||||
|
||||
export const initSlots = (
|
||||
instance: ComponentInternalInstance,
|
||||
children: VNodeNormalizedChildren
|
||||
) => {
|
||||
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
|
||||
if ((children as RawSlots)._ === 1) {
|
||||
instance.slots = children as InternalSlots
|
||||
} else {
|
||||
normalizeObjectSlots(children as RawSlots, (instance.slots = {}))
|
||||
}
|
||||
} else {
|
||||
instance.slots = {}
|
||||
if (children) {
|
||||
normalizeVNodeSlots(instance, children)
|
||||
}
|
||||
}
|
||||
def(instance.slots, InternalObjectSymbol, true)
|
||||
}
|
||||
|
||||
export const updateSlots = (
|
||||
instance: ComponentInternalInstance,
|
||||
children: VNodeNormalizedChildren
|
||||
) => {
|
||||
const { vnode, slots } = instance
|
||||
let needDeletionCheck = true
|
||||
let deletionComparisonTarget = EMPTY_OBJ
|
||||
if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
|
||||
if ((children as RawSlots)._ === 1) {
|
||||
// compiled slots.
|
||||
if (
|
||||
// bail on dynamic slots (v-if, v-for, reference of scope variables)
|
||||
!(vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS) &&
|
||||
// bail on HRM updates
|
||||
!(__DEV__ && instance.parent && instance.parent.renderUpdated)
|
||||
) {
|
||||
// compiled AND static.
|
||||
// no need to update, and skip stale slots removal.
|
||||
needDeletionCheck = false
|
||||
} else {
|
||||
// compiled but dynamic - update slots, but skip normalization.
|
||||
extend(slots, children as Slots)
|
||||
}
|
||||
} else {
|
||||
needDeletionCheck = !(children as RawSlots).$stable
|
||||
normalizeObjectSlots(children as RawSlots, slots)
|
||||
}
|
||||
deletionComparisonTarget = children as RawSlots
|
||||
} else if (children) {
|
||||
// non slot object children (direct value) passed to a component
|
||||
normalizeVNodeSlots(instance, children)
|
||||
deletionComparisonTarget = { default: 1 }
|
||||
}
|
||||
|
||||
// delete stale slots
|
||||
if (needDeletionCheck) {
|
||||
for (const key in slots) {
|
||||
if (!isInternalKey(key) && !(key in deletionComparisonTarget)) {
|
||||
delete slots[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
instance.slots = slots || EMPTY_OBJ
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
getCurrentInstance,
|
||||
SetupContext,
|
||||
ComponentOptions,
|
||||
ComponentInternalInstance
|
||||
} from '../component'
|
||||
import {
|
||||
@@ -9,16 +8,17 @@ import {
|
||||
Comment,
|
||||
isSameVNodeType,
|
||||
VNode,
|
||||
VNodeChildren
|
||||
VNodeArrayChildren
|
||||
} from '../vnode'
|
||||
import { warn } from '../warning'
|
||||
import { isKeepAlive } from './KeepAlive'
|
||||
import { toRaw } from '@vue/reactivity'
|
||||
import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
|
||||
import { ShapeFlags } from '../shapeFlags'
|
||||
import { ShapeFlags } from '@vue/shared'
|
||||
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
|
||||
import { RendererElement } from '../renderer'
|
||||
|
||||
export interface BaseTransitionProps {
|
||||
export interface BaseTransitionProps<HostElement = RendererElement> {
|
||||
mode?: 'in-out' | 'out-in' | 'default'
|
||||
appear?: boolean
|
||||
|
||||
@@ -32,25 +32,25 @@ export interface BaseTransitionProps {
|
||||
// Hooks. Using camel case for easier usage in render functions & JSX.
|
||||
// In templates these can be written as @before-enter="xxx" as prop names
|
||||
// are camelized.
|
||||
onBeforeEnter?: (el: any) => void
|
||||
onEnter?: (el: any, done: () => void) => void
|
||||
onAfterEnter?: (el: any) => void
|
||||
onEnterCancelled?: (el: any) => void
|
||||
onBeforeEnter?: (el: HostElement) => void
|
||||
onEnter?: (el: HostElement, done: () => void) => void
|
||||
onAfterEnter?: (el: HostElement) => void
|
||||
onEnterCancelled?: (el: HostElement) => void
|
||||
// leave
|
||||
onBeforeLeave?: (el: any) => void
|
||||
onLeave?: (el: any, done: () => void) => void
|
||||
onAfterLeave?: (el: any) => void
|
||||
onLeaveCancelled?: (el: any) => void // only fired in persisted mode
|
||||
onBeforeLeave?: (el: HostElement) => void
|
||||
onLeave?: (el: HostElement, done: () => void) => void
|
||||
onAfterLeave?: (el: HostElement) => void
|
||||
onLeaveCancelled?: (el: HostElement) => void // only fired in persisted mode
|
||||
}
|
||||
|
||||
export interface TransitionHooks {
|
||||
persisted: boolean
|
||||
beforeEnter(el: object): void
|
||||
enter(el: object): void
|
||||
leave(el: object, remove: () => void): void
|
||||
beforeEnter(el: RendererElement): void
|
||||
enter(el: RendererElement): void
|
||||
leave(el: RendererElement, remove: () => void): void
|
||||
afterLeave?(): void
|
||||
delayLeave?(
|
||||
el: object,
|
||||
el: RendererElement,
|
||||
earlyRemove: () => void,
|
||||
delayedLeave: () => void
|
||||
): void
|
||||
@@ -99,6 +99,23 @@ export function useTransitionState(): TransitionState {
|
||||
|
||||
const BaseTransitionImpl = {
|
||||
name: `BaseTransition`,
|
||||
|
||||
props: {
|
||||
mode: String,
|
||||
appear: Boolean,
|
||||
persisted: Boolean,
|
||||
// enter
|
||||
onBeforeEnter: Function,
|
||||
onEnter: Function,
|
||||
onAfterEnter: Function,
|
||||
onEnterCancelled: Function,
|
||||
// leave
|
||||
onBeforeLeave: Function,
|
||||
onLeave: Function,
|
||||
onAfterLeave: Function,
|
||||
onLeaveCancelled: Function
|
||||
},
|
||||
|
||||
setup(props: BaseTransitionProps, { slots }: SetupContext) {
|
||||
const instance = getCurrentInstance()!
|
||||
const state = useTransitionState()
|
||||
@@ -200,29 +217,11 @@ const BaseTransitionImpl = {
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
;(BaseTransitionImpl as ComponentOptions).props = {
|
||||
mode: String,
|
||||
appear: Boolean,
|
||||
persisted: Boolean,
|
||||
// enter
|
||||
onBeforeEnter: Function,
|
||||
onEnter: Function,
|
||||
onAfterEnter: Function,
|
||||
onEnterCancelled: Function,
|
||||
// leave
|
||||
onBeforeLeave: Function,
|
||||
onLeave: Function,
|
||||
onAfterLeave: Function,
|
||||
onLeaveCancelled: Function
|
||||
}
|
||||
}
|
||||
|
||||
// export the public type for h/tsx inference
|
||||
// also to avoid inline import() in generated d.ts files
|
||||
export const BaseTransition = (BaseTransitionImpl as any) as {
|
||||
new (): {
|
||||
$props: BaseTransitionProps
|
||||
$props: BaseTransitionProps<any>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +253,7 @@ export function resolveTransitionHooks(
|
||||
onLeave,
|
||||
onAfterLeave,
|
||||
onLeaveCancelled
|
||||
}: BaseTransitionProps,
|
||||
}: BaseTransitionProps<any>,
|
||||
state: TransitionState,
|
||||
instance: ComponentInternalInstance
|
||||
): TransitionHooks {
|
||||
@@ -286,10 +285,10 @@ export function resolveTransitionHooks(
|
||||
if (
|
||||
leavingVNode &&
|
||||
isSameVNodeType(vnode, leavingVNode) &&
|
||||
leavingVNode.el._leaveCb
|
||||
leavingVNode.el!._leaveCb
|
||||
) {
|
||||
// force early removal (not cancelled)
|
||||
leavingVNode.el._leaveCb()
|
||||
leavingVNode.el!._leaveCb()
|
||||
}
|
||||
callHook(onBeforeEnter, [el])
|
||||
},
|
||||
@@ -370,7 +369,7 @@ function emptyPlaceholder(vnode: VNode): VNode | undefined {
|
||||
function getKeepAliveChild(vnode: VNode): VNode | undefined {
|
||||
return isKeepAlive(vnode)
|
||||
? vnode.children
|
||||
? ((vnode.children as VNodeChildren)[0] as VNode)
|
||||
? ((vnode.children as VNodeArrayChildren)[0] as VNode)
|
||||
: undefined
|
||||
: vnode
|
||||
}
|
||||
|
||||
@@ -7,18 +7,24 @@ import {
|
||||
LifecycleHooks,
|
||||
currentInstance
|
||||
} from '../component'
|
||||
import { VNode, cloneVNode, isVNode } from '../vnode'
|
||||
import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode'
|
||||
import { warn } from '../warning'
|
||||
import { onBeforeUnmount, injectHook, onUnmounted } from '../apiLifecycle'
|
||||
import { isString, isArray } from '@vue/shared'
|
||||
import {
|
||||
isString,
|
||||
isArray,
|
||||
ShapeFlags,
|
||||
remove,
|
||||
invokeArrayFns
|
||||
} from '@vue/shared'
|
||||
import { watch } from '../apiWatch'
|
||||
import { ShapeFlags } from '../shapeFlags'
|
||||
import { SuspenseBoundary } from './Suspense'
|
||||
import {
|
||||
RendererInternals,
|
||||
queuePostRenderEffect,
|
||||
invokeHooks,
|
||||
MoveType
|
||||
MoveType,
|
||||
RendererElement,
|
||||
RendererNode
|
||||
} from '../renderer'
|
||||
import { setTransitionHooks } from './BaseTransition'
|
||||
|
||||
@@ -37,7 +43,13 @@ type Keys = Set<CacheKey>
|
||||
export interface KeepAliveSink {
|
||||
renderer: RendererInternals
|
||||
parentSuspense: SuspenseBoundary | null
|
||||
activate: (vnode: VNode, container: object, anchor: object | null) => void
|
||||
activate: (
|
||||
vnode: VNode,
|
||||
container: RendererElement,
|
||||
anchor: RendererNode | null,
|
||||
isSVG: boolean,
|
||||
optimized: boolean
|
||||
) => void
|
||||
deactivate: (vnode: VNode) => void
|
||||
}
|
||||
|
||||
@@ -73,21 +85,33 @@ const KeepAliveImpl = {
|
||||
const sink = instance.sink as KeepAliveSink
|
||||
const {
|
||||
renderer: {
|
||||
move,
|
||||
unmount: _unmount,
|
||||
options: { createElement }
|
||||
p: patch,
|
||||
m: move,
|
||||
um: _unmount,
|
||||
o: { createElement }
|
||||
},
|
||||
parentSuspense
|
||||
} = sink
|
||||
const storageContainer = createElement('div')
|
||||
|
||||
sink.activate = (vnode, container, anchor) => {
|
||||
sink.activate = (vnode, container, anchor, isSVG, optimized) => {
|
||||
const child = vnode.component!
|
||||
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
|
||||
// in case props have changed
|
||||
patch(
|
||||
child.vnode,
|
||||
vnode,
|
||||
container,
|
||||
anchor,
|
||||
instance,
|
||||
parentSuspense,
|
||||
isSVG,
|
||||
optimized
|
||||
)
|
||||
queuePostRenderEffect(() => {
|
||||
const component = vnode.component!
|
||||
component.isDeactivated = false
|
||||
if (component.a !== null) {
|
||||
invokeHooks(component.a)
|
||||
child.isDeactivated = false
|
||||
if (child.a) {
|
||||
invokeArrayFns(child.a)
|
||||
}
|
||||
}, parentSuspense)
|
||||
}
|
||||
@@ -96,8 +120,8 @@ const KeepAliveImpl = {
|
||||
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
|
||||
queuePostRenderEffect(() => {
|
||||
const component = vnode.component!
|
||||
if (component.da !== null) {
|
||||
invokeHooks(component.da)
|
||||
if (component.da) {
|
||||
invokeArrayFns(component.da)
|
||||
}
|
||||
component.isDeactivated = true
|
||||
}, parentSuspense)
|
||||
@@ -136,8 +160,7 @@ const KeepAliveImpl = {
|
||||
([include, exclude]) => {
|
||||
include && pruneCache(name => matches(include, name))
|
||||
exclude && pruneCache(name => matches(exclude, name))
|
||||
},
|
||||
{ lazy: true }
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -177,7 +200,7 @@ const KeepAliveImpl = {
|
||||
}
|
||||
|
||||
const key = vnode.key == null ? comp : vnode.key
|
||||
const cached = cache.get(key)
|
||||
const cachedVNode = cache.get(key)
|
||||
|
||||
// clone vnode if it's reused because we are going to mutate it
|
||||
if (vnode.el) {
|
||||
@@ -185,11 +208,10 @@ const KeepAliveImpl = {
|
||||
}
|
||||
cache.set(key, vnode)
|
||||
|
||||
if (cached) {
|
||||
if (cachedVNode) {
|
||||
// copy over mounted state
|
||||
vnode.el = cached.el
|
||||
vnode.anchor = cached.anchor
|
||||
vnode.component = cached.component
|
||||
vnode.el = cachedVNode.el
|
||||
vnode.component = cachedVNode.component
|
||||
if (vnode.transition) {
|
||||
// recursively update transition hooks on subTree
|
||||
setTransitionHooks(vnode, vnode.transition!)
|
||||
@@ -219,7 +241,7 @@ const KeepAliveImpl = {
|
||||
// also to avoid inline import() in generated d.ts files
|
||||
export const KeepAlive = (KeepAliveImpl as any) as {
|
||||
new (): {
|
||||
$props: KeepAliveProps
|
||||
$props: VNodeProps & KeepAliveProps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +305,7 @@ function registerKeepAliveHook(
|
||||
if (target) {
|
||||
let current = target.parent
|
||||
while (current && current.parent) {
|
||||
if (current.parent.type === KeepAliveImpl) {
|
||||
if (isKeepAlive(current.parent.vnode)) {
|
||||
injectToKeepAliveRoot(wrappedHook, type, target, current)
|
||||
}
|
||||
current = current.parent
|
||||
@@ -299,7 +321,6 @@ function injectToKeepAliveRoot(
|
||||
) {
|
||||
injectHook(type, hook, keepAliveRoot, true /* prepend */)
|
||||
onUnmounted(() => {
|
||||
const hooks = keepAliveRoot[type]!
|
||||
hooks.splice(hooks.indexOf(hook), 1)
|
||||
remove(keepAliveRoot[type]!, hook)
|
||||
}, target)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { VNode, normalizeVNode, VNodeChild } from '../vnode'
|
||||
import { ShapeFlags } from '../shapeFlags'
|
||||
import { isFunction, isArray } from '@vue/shared'
|
||||
import { VNode, normalizeVNode, VNodeChild, VNodeProps } from '../vnode'
|
||||
import { isFunction, isArray, ShapeFlags } from '@vue/shared'
|
||||
import { ComponentInternalInstance, handleSetupResult } from '../component'
|
||||
import { Slots } from '../componentSlots'
|
||||
import { RendererInternals, MoveType } from '../renderer'
|
||||
import {
|
||||
RendererInternals,
|
||||
MoveType,
|
||||
SetupRenderEffectFn,
|
||||
RendererNode,
|
||||
RendererElement
|
||||
} from '../renderer'
|
||||
import { queuePostFlushCb, queueJob } from '../scheduler'
|
||||
import { updateHOCHostEl } from '../componentRenderUtils'
|
||||
import { handleError, ErrorCodes } from '../errorHandling'
|
||||
import { pushWarningContext, popWarningContext } from '../warning'
|
||||
import { handleError, ErrorCodes } from '../errorHandling'
|
||||
|
||||
export interface SuspenseProps {
|
||||
onResolve?: () => void
|
||||
onRecede?: () => void
|
||||
}
|
||||
|
||||
export const isSuspense = (type: any): boolean => type.__isSuspense
|
||||
|
||||
// Suspense exposes a component-like API, and is treated like a component
|
||||
// in the compiler, but internally it's a special built-in type that hooks
|
||||
// directly into the renderer.
|
||||
@@ -26,8 +33,8 @@ export const SuspenseImpl = {
|
||||
process(
|
||||
n1: VNode | null,
|
||||
n2: VNode,
|
||||
container: object,
|
||||
anchor: object | null,
|
||||
container: RendererElement,
|
||||
anchor: RendererNode | null,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
isSVG: boolean,
|
||||
@@ -58,7 +65,8 @@ export const SuspenseImpl = {
|
||||
rendererInternals
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
hydrate: hydrateSuspense
|
||||
}
|
||||
|
||||
// Force-casted public typing for h and TSX props inference
|
||||
@@ -66,13 +74,13 @@ export const Suspense = ((__FEATURE_SUSPENSE__
|
||||
? SuspenseImpl
|
||||
: null) as any) as {
|
||||
__isSuspense: true
|
||||
new (): { $props: SuspenseProps }
|
||||
new (): { $props: VNodeProps & SuspenseProps }
|
||||
}
|
||||
|
||||
function mountSuspense(
|
||||
n2: VNode,
|
||||
container: object,
|
||||
anchor: object | null,
|
||||
container: RendererElement,
|
||||
anchor: RendererNode | null,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
isSVG: boolean,
|
||||
@@ -80,8 +88,8 @@ function mountSuspense(
|
||||
rendererInternals: RendererInternals
|
||||
) {
|
||||
const {
|
||||
patch,
|
||||
options: { createElement }
|
||||
p: patch,
|
||||
o: { createElement }
|
||||
} = rendererInternals
|
||||
const hiddenContainer = createElement('div')
|
||||
const suspense = (n2.suspense = createSuspenseBoundary(
|
||||
@@ -96,14 +104,10 @@ function mountSuspense(
|
||||
rendererInternals
|
||||
))
|
||||
|
||||
const { content, fallback } = normalizeSuspenseChildren(n2)
|
||||
suspense.subTree = content
|
||||
suspense.fallbackTree = fallback
|
||||
|
||||
// start mounting the content subtree in an off-dom container
|
||||
patch(
|
||||
null,
|
||||
content,
|
||||
suspense.subTree,
|
||||
hiddenContainer,
|
||||
null,
|
||||
parentComponent,
|
||||
@@ -116,7 +120,7 @@ function mountSuspense(
|
||||
// mount the fallback tree
|
||||
patch(
|
||||
null,
|
||||
fallback,
|
||||
suspense.fallbackTree,
|
||||
container,
|
||||
anchor,
|
||||
parentComponent,
|
||||
@@ -124,7 +128,7 @@ function mountSuspense(
|
||||
isSVG,
|
||||
optimized
|
||||
)
|
||||
n2.el = fallback.el
|
||||
n2.el = suspense.fallbackTree.el
|
||||
} else {
|
||||
// Suspense has no async deps. Just resolve.
|
||||
suspense.resolve()
|
||||
@@ -134,12 +138,12 @@ function mountSuspense(
|
||||
function patchSuspense(
|
||||
n1: VNode,
|
||||
n2: VNode,
|
||||
container: object,
|
||||
anchor: object | null,
|
||||
container: RendererElement,
|
||||
anchor: RendererNode | null,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
isSVG: boolean,
|
||||
optimized: boolean,
|
||||
{ patch }: RendererInternals
|
||||
{ p: patch }: RendererInternals
|
||||
) {
|
||||
const suspense = (n2.suspense = n1.suspense)!
|
||||
suspense.vnode = n2
|
||||
@@ -192,66 +196,64 @@ function patchSuspense(
|
||||
suspense.fallbackTree = fallback
|
||||
}
|
||||
|
||||
export interface SuspenseBoundary<
|
||||
HostNode = any,
|
||||
HostElement = any,
|
||||
HostVNode = VNode<HostNode, HostElement>
|
||||
> {
|
||||
vnode: HostVNode
|
||||
parent: SuspenseBoundary<HostNode, HostElement> | null
|
||||
export interface SuspenseBoundary {
|
||||
vnode: VNode
|
||||
parent: SuspenseBoundary | null
|
||||
parentComponent: ComponentInternalInstance | null
|
||||
isSVG: boolean
|
||||
optimized: boolean
|
||||
container: HostElement
|
||||
hiddenContainer: HostElement
|
||||
anchor: HostNode | null
|
||||
subTree: HostVNode
|
||||
fallbackTree: HostVNode
|
||||
container: RendererElement
|
||||
hiddenContainer: RendererElement
|
||||
anchor: RendererNode | null
|
||||
subTree: VNode
|
||||
fallbackTree: VNode
|
||||
deps: number
|
||||
isHydrating: boolean
|
||||
isResolved: boolean
|
||||
isUnmounted: boolean
|
||||
effects: Function[]
|
||||
resolve(): void
|
||||
recede(): void
|
||||
move(container: HostElement, anchor: HostNode | null, type: MoveType): void
|
||||
next(): HostNode | null
|
||||
move(
|
||||
container: RendererElement,
|
||||
anchor: RendererNode | null,
|
||||
type: MoveType
|
||||
): void
|
||||
next(): RendererNode | null
|
||||
registerDep(
|
||||
instance: ComponentInternalInstance,
|
||||
setupRenderEffect: (
|
||||
instance: ComponentInternalInstance,
|
||||
parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
|
||||
initialVNode: VNode<HostNode, HostElement>,
|
||||
container: HostElement,
|
||||
anchor: HostNode | null,
|
||||
isSVG: boolean
|
||||
) => void
|
||||
): void
|
||||
unmount(
|
||||
parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
|
||||
doRemove?: boolean
|
||||
setupRenderEffect: SetupRenderEffectFn
|
||||
): void
|
||||
unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void
|
||||
}
|
||||
|
||||
function createSuspenseBoundary<HostNode, HostElement>(
|
||||
vnode: VNode<HostNode, HostElement>,
|
||||
parent: SuspenseBoundary<HostNode, HostElement> | null,
|
||||
function createSuspenseBoundary(
|
||||
vnode: VNode,
|
||||
parent: SuspenseBoundary | null,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
container: HostElement,
|
||||
hiddenContainer: HostElement,
|
||||
anchor: HostNode | null,
|
||||
container: RendererElement,
|
||||
hiddenContainer: RendererElement,
|
||||
anchor: RendererNode | null,
|
||||
isSVG: boolean,
|
||||
optimized: boolean,
|
||||
rendererInternals: RendererInternals<HostNode, HostElement>
|
||||
): SuspenseBoundary<HostNode, HostElement> {
|
||||
rendererInternals: RendererInternals,
|
||||
isHydrating = false
|
||||
): SuspenseBoundary {
|
||||
const {
|
||||
patch,
|
||||
move,
|
||||
unmount,
|
||||
next,
|
||||
options: { parentNode }
|
||||
p: patch,
|
||||
m: move,
|
||||
um: unmount,
|
||||
n: next,
|
||||
o: { parentNode }
|
||||
} = rendererInternals
|
||||
|
||||
const suspense: SuspenseBoundary<HostNode, HostElement> = {
|
||||
const getCurrentTree = () =>
|
||||
suspense.isResolved || suspense.isHydrating
|
||||
? suspense.subTree
|
||||
: suspense.fallbackTree
|
||||
|
||||
const { content, fallback } = normalizeSuspenseChildren(vnode)
|
||||
const suspense: SuspenseBoundary = {
|
||||
vnode,
|
||||
parent,
|
||||
parentComponent,
|
||||
@@ -261,8 +263,9 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
hiddenContainer,
|
||||
anchor,
|
||||
deps: 0,
|
||||
subTree: null as any, // will be set immediately after creation
|
||||
fallbackTree: null as any, // will be set immediately after creation
|
||||
subTree: content,
|
||||
fallbackTree: fallback,
|
||||
isHydrating,
|
||||
isResolved: false,
|
||||
isUnmounted: false,
|
||||
effects: [],
|
||||
@@ -289,18 +292,23 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
container
|
||||
} = suspense
|
||||
|
||||
// this is initial anchor on mount
|
||||
let { anchor } = suspense
|
||||
// unmount fallback tree
|
||||
if (fallbackTree.el) {
|
||||
// if the fallback tree was mounted, it may have been moved
|
||||
// as part of a parent suspense. get the latest anchor for insertion
|
||||
anchor = next(fallbackTree)
|
||||
unmount(fallbackTree as VNode, parentComponent, suspense, true)
|
||||
if (suspense.isHydrating) {
|
||||
suspense.isHydrating = false
|
||||
} else {
|
||||
// this is initial anchor on mount
|
||||
let { anchor } = suspense
|
||||
// unmount fallback tree
|
||||
if (fallbackTree.el) {
|
||||
// if the fallback tree was mounted, it may have been moved
|
||||
// as part of a parent suspense. get the latest anchor for insertion
|
||||
anchor = next(fallbackTree)
|
||||
unmount(fallbackTree, parentComponent, suspense, true)
|
||||
}
|
||||
// move content from off-dom container to actual container
|
||||
move(subTree, container, anchor, MoveType.ENTER)
|
||||
}
|
||||
// move content from off-dom container to actual container
|
||||
move(subTree as VNode, container, anchor, MoveType.ENTER)
|
||||
const el = (vnode.el = (subTree as VNode).el!)
|
||||
|
||||
const el = (vnode.el = subTree.el!)
|
||||
// suspense as the root node of a component...
|
||||
if (parentComponent && parentComponent.subTree === vnode) {
|
||||
parentComponent.vnode.el = el
|
||||
@@ -324,6 +332,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
queuePostFlushCb(effects)
|
||||
}
|
||||
suspense.isResolved = true
|
||||
suspense.effects = []
|
||||
// invoke @resolve event
|
||||
const onResolve = vnode.props && vnode.props.onResolve
|
||||
if (isFunction(onResolve)) {
|
||||
@@ -346,7 +355,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
|
||||
// move content tree back to the off-dom container
|
||||
const anchor = next(subTree)
|
||||
move(subTree as VNode, hiddenContainer, null, MoveType.LEAVE)
|
||||
move(subTree, hiddenContainer, null, MoveType.LEAVE)
|
||||
// remount the fallback tree
|
||||
patch(
|
||||
null,
|
||||
@@ -358,7 +367,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
isSVG,
|
||||
optimized
|
||||
)
|
||||
const el = (vnode.el = (fallbackTree as VNode).el!)
|
||||
const el = (vnode.el = fallbackTree.el!)
|
||||
// suspense as the root node of a component...
|
||||
if (parentComponent && parentComponent.subTree === vnode) {
|
||||
parentComponent.vnode.el = el
|
||||
@@ -373,19 +382,12 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
},
|
||||
|
||||
move(container, anchor, type) {
|
||||
move(
|
||||
suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
|
||||
container,
|
||||
anchor,
|
||||
type
|
||||
)
|
||||
move(getCurrentTree(), container, anchor, type)
|
||||
suspense.container = container
|
||||
},
|
||||
|
||||
next() {
|
||||
return next(
|
||||
suspense.isResolved ? suspense.subTree : suspense.fallbackTree
|
||||
)
|
||||
return next(getCurrentTree())
|
||||
},
|
||||
|
||||
registerDep(instance, setupRenderEffect) {
|
||||
@@ -398,6 +400,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
})
|
||||
}
|
||||
|
||||
const hydratedEl = instance.vnode.el
|
||||
suspense.deps++
|
||||
instance
|
||||
.asyncDep!.catch(err => {
|
||||
@@ -416,15 +419,27 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
if (__DEV__) {
|
||||
pushWarningContext(vnode)
|
||||
}
|
||||
handleSetupResult(instance, asyncSetupResult, suspense)
|
||||
handleSetupResult(instance, asyncSetupResult, false)
|
||||
if (hydratedEl) {
|
||||
// vnode may have been replaced if an update happened before the
|
||||
// async dep is reoslved.
|
||||
vnode.el = hydratedEl
|
||||
}
|
||||
setupRenderEffect(
|
||||
instance,
|
||||
suspense,
|
||||
vnode,
|
||||
// component may have been moved before resolve
|
||||
parentNode(instance.subTree.el)!,
|
||||
next(instance.subTree),
|
||||
isSVG
|
||||
// component may have been moved before resolve.
|
||||
// if this is not a hydration, instance.subTree will be the comment
|
||||
// placeholder.
|
||||
hydratedEl
|
||||
? parentNode(hydratedEl)!
|
||||
: parentNode(instance.subTree.el!)!,
|
||||
// anchor will not be used if this is hydration, so only need to
|
||||
// consider the comment placeholder case.
|
||||
hydratedEl ? null : next(instance.subTree),
|
||||
suspense,
|
||||
isSVG,
|
||||
optimized
|
||||
)
|
||||
updateHOCHostEl(instance, vnode.el)
|
||||
if (__DEV__) {
|
||||
@@ -453,7 +468,54 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
||||
return suspense
|
||||
}
|
||||
|
||||
function normalizeSuspenseChildren(
|
||||
function hydrateSuspense(
|
||||
node: Node,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
isSVG: boolean,
|
||||
optimized: boolean,
|
||||
rendererInternals: RendererInternals,
|
||||
hydrateNode: (
|
||||
node: Node,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
optimized: boolean
|
||||
) => Node | null
|
||||
): Node | null {
|
||||
const suspense = (vnode.suspense = createSuspenseBoundary(
|
||||
vnode,
|
||||
parentSuspense,
|
||||
parentComponent,
|
||||
node.parentNode!,
|
||||
document.createElement('div'),
|
||||
null,
|
||||
isSVG,
|
||||
optimized,
|
||||
rendererInternals,
|
||||
true /* hydrating */
|
||||
))
|
||||
// there are two possible scenarios for server-rendered suspense:
|
||||
// - success: ssr content should be fully resolved
|
||||
// - failure: ssr content should be the fallback branch.
|
||||
// however, on the client we don't really know if it has failed or not
|
||||
// attempt to hydrate the DOM assuming it has succeeded, but we still
|
||||
// need to construct a suspense boundary first
|
||||
const result = hydrateNode(
|
||||
node,
|
||||
suspense.subTree,
|
||||
parentComponent,
|
||||
suspense,
|
||||
optimized
|
||||
)
|
||||
if (suspense.deps === 0) {
|
||||
suspense.resolve()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function normalizeSuspenseChildren(
|
||||
vnode: VNode
|
||||
): {
|
||||
content: VNode
|
||||
@@ -478,7 +540,7 @@ export function queueEffectWithSuspense(
|
||||
fn: Function | Function[],
|
||||
suspense: SuspenseBoundary | null
|
||||
): void {
|
||||
if (suspense !== null && !suspense.isResolved) {
|
||||
if (suspense && !suspense.isResolved) {
|
||||
if (isArray(fn)) {
|
||||
suspense.effects.push(...fn)
|
||||
} else {
|
||||
|
||||
329
packages/runtime-core/src/components/Teleport.ts
Normal file
329
packages/runtime-core/src/components/Teleport.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { ComponentInternalInstance } from '../component'
|
||||
import { SuspenseBoundary } from './Suspense'
|
||||
import {
|
||||
RendererInternals,
|
||||
MoveType,
|
||||
RendererElement,
|
||||
RendererNode,
|
||||
RendererOptions
|
||||
} from '../renderer'
|
||||
import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
|
||||
import { isString, ShapeFlags } from '@vue/shared'
|
||||
import { warn } from '../warning'
|
||||
|
||||
export interface TeleportProps {
|
||||
to: string | RendererElement
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const isTeleport = (type: any): boolean => type.__isTeleport
|
||||
|
||||
const isTeleportDisabled = (props: VNode['props']): boolean =>
|
||||
props && (props.disabled || props.disabled === '')
|
||||
|
||||
const resolveTarget = <T = RendererElement>(
|
||||
props: TeleportProps | null,
|
||||
select: RendererOptions['querySelector']
|
||||
): T | null => {
|
||||
const targetSelector = props && props.to
|
||||
if (isString(targetSelector)) {
|
||||
if (!select) {
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Current renderer does not support string target for Teleports. ` +
|
||||
`(missing querySelector renderer option)`
|
||||
)
|
||||
return null
|
||||
} else {
|
||||
const target = select(targetSelector)
|
||||
if (!target) {
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Failed to locate Teleport target with selector "${targetSelector}".`
|
||||
)
|
||||
}
|
||||
return target as any
|
||||
}
|
||||
} else {
|
||||
if (__DEV__ && !targetSelector) {
|
||||
warn(`Invalid Teleport target: ${targetSelector}`)
|
||||
}
|
||||
return targetSelector as any
|
||||
}
|
||||
}
|
||||
|
||||
export const TeleportImpl = {
|
||||
__isTeleport: true,
|
||||
process(
|
||||
n1: VNode | null,
|
||||
n2: VNode,
|
||||
container: RendererElement,
|
||||
anchor: RendererNode | null,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
isSVG: boolean,
|
||||
optimized: boolean,
|
||||
internals: RendererInternals
|
||||
) {
|
||||
const {
|
||||
mc: mountChildren,
|
||||
pc: patchChildren,
|
||||
pbc: patchBlockChildren,
|
||||
o: { insert, querySelector, createText, createComment }
|
||||
} = internals
|
||||
|
||||
const disabled = isTeleportDisabled(n2.props)
|
||||
const { shapeFlag, children } = n2
|
||||
if (n1 == null) {
|
||||
// insert anchors in the main view
|
||||
const placeholder = (n2.el = __DEV__
|
||||
? createComment('teleport start')
|
||||
: createText(''))
|
||||
const mainAnchor = (n2.anchor = __DEV__
|
||||
? createComment('teleport end')
|
||||
: createText(''))
|
||||
insert(placeholder, container, anchor)
|
||||
insert(mainAnchor, container, anchor)
|
||||
|
||||
const target = (n2.target = resolveTarget(
|
||||
n2.props as TeleportProps,
|
||||
querySelector
|
||||
))
|
||||
const targetAnchor = (n2.targetAnchor = createText(''))
|
||||
if (target) {
|
||||
insert(targetAnchor, target)
|
||||
} else if (__DEV__) {
|
||||
warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
|
||||
}
|
||||
|
||||
const mount = (container: RendererElement, anchor: RendererNode) => {
|
||||
// Teleport *always* has Array children. This is enforced in both the
|
||||
// compiler and vnode children normalization.
|
||||
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||
mountChildren(
|
||||
children as VNodeArrayChildren,
|
||||
container,
|
||||
anchor,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVG,
|
||||
optimized
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
mount(container, mainAnchor)
|
||||
} else if (target) {
|
||||
mount(target, targetAnchor)
|
||||
}
|
||||
} else {
|
||||
// update content
|
||||
n2.el = n1.el
|
||||
const mainAnchor = (n2.anchor = n1.anchor)!
|
||||
const target = (n2.target = n1.target)!
|
||||
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
|
||||
const wasDisabled = isTeleportDisabled(n1.props)
|
||||
const currentContainer = wasDisabled ? container : target
|
||||
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
|
||||
|
||||
if (n2.dynamicChildren) {
|
||||
// fast path when the teleport happens to be a block root
|
||||
patchBlockChildren(
|
||||
n1.dynamicChildren!,
|
||||
n2.dynamicChildren,
|
||||
currentContainer,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVG
|
||||
)
|
||||
} else if (!optimized) {
|
||||
patchChildren(
|
||||
n1,
|
||||
n2,
|
||||
currentContainer,
|
||||
currentAnchor,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVG
|
||||
)
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
if (!wasDisabled) {
|
||||
// enabled -> disabled
|
||||
// move into main container
|
||||
moveTeleport(
|
||||
n2,
|
||||
container,
|
||||
mainAnchor,
|
||||
internals,
|
||||
TeleportMoveTypes.TOGGLE
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// target changed
|
||||
if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
|
||||
const nextTarget = (n2.target = resolveTarget(
|
||||
n2.props as TeleportProps,
|
||||
querySelector
|
||||
))
|
||||
if (nextTarget) {
|
||||
moveTeleport(
|
||||
n2,
|
||||
nextTarget,
|
||||
null,
|
||||
internals,
|
||||
TeleportMoveTypes.TARGET_CHANGE
|
||||
)
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
'Invalid Teleport target on update:',
|
||||
target,
|
||||
`(${typeof target})`
|
||||
)
|
||||
}
|
||||
} else if (wasDisabled) {
|
||||
// disabled -> enabled
|
||||
// move into teleport target
|
||||
moveTeleport(
|
||||
n2,
|
||||
target,
|
||||
targetAnchor,
|
||||
internals,
|
||||
TeleportMoveTypes.TOGGLE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
remove(
|
||||
vnode: VNode,
|
||||
{ r: remove, o: { remove: hostRemove } }: RendererInternals
|
||||
) {
|
||||
const { shapeFlag, children, anchor } = vnode
|
||||
hostRemove(anchor!)
|
||||
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||
for (let i = 0; i < (children as VNode[]).length; i++) {
|
||||
remove((children as VNode[])[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
move: moveTeleport,
|
||||
hydrate: hydrateTeleport
|
||||
}
|
||||
|
||||
export const enum TeleportMoveTypes {
|
||||
TARGET_CHANGE,
|
||||
TOGGLE, // enable / disable
|
||||
REORDER // moved in the main view
|
||||
}
|
||||
|
||||
function moveTeleport(
|
||||
vnode: VNode,
|
||||
container: RendererElement,
|
||||
parentAnchor: RendererNode | null,
|
||||
{ o: { insert }, m: move }: RendererInternals,
|
||||
moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER
|
||||
) {
|
||||
// move target anchor if this is a target change.
|
||||
if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
|
||||
insert(vnode.targetAnchor!, container, parentAnchor)
|
||||
}
|
||||
const { el, anchor, shapeFlag, children, props } = vnode
|
||||
const isReorder = moveType === TeleportMoveTypes.REORDER
|
||||
// move main view anchor if this is a re-order.
|
||||
if (isReorder) {
|
||||
insert(el!, container, parentAnchor)
|
||||
}
|
||||
// if this is a re-order and teleport is enabled (content is in target)
|
||||
// do not move children. So the opposite is: only move children if this
|
||||
// is not a reorder, or the teleport is disabled
|
||||
if (!isReorder || isTeleportDisabled(props)) {
|
||||
// Teleport has either Array children or no children.
|
||||
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||
for (let i = 0; i < (children as VNode[]).length; i++) {
|
||||
move(
|
||||
(children as VNode[])[i],
|
||||
container,
|
||||
parentAnchor,
|
||||
MoveType.REORDER
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// move main view anchor if this is a re-order.
|
||||
if (isReorder) {
|
||||
insert(anchor!, container, parentAnchor)
|
||||
}
|
||||
}
|
||||
|
||||
interface TeleportTargetElement extends Element {
|
||||
// last teleport target
|
||||
_lpa?: Node | null
|
||||
}
|
||||
|
||||
function hydrateTeleport(
|
||||
node: Node,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
optimized: boolean,
|
||||
{
|
||||
o: { nextSibling, parentNode, querySelector }
|
||||
}: RendererInternals<Node, Element>,
|
||||
hydrateChildren: (
|
||||
node: Node | null,
|
||||
vnode: VNode,
|
||||
container: Element,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
optimized: boolean
|
||||
) => Node | null
|
||||
): Node | null {
|
||||
const target = (vnode.target = resolveTarget<Element>(
|
||||
vnode.props as TeleportProps,
|
||||
querySelector
|
||||
))
|
||||
if (target) {
|
||||
// if multiple teleports rendered to the same target element, we need to
|
||||
// pick up from where the last teleport finished instead of the first node
|
||||
const targetNode =
|
||||
(target as TeleportTargetElement)._lpa || target.firstChild
|
||||
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||
if (isTeleportDisabled(vnode.props)) {
|
||||
vnode.anchor = hydrateChildren(
|
||||
nextSibling(node),
|
||||
vnode,
|
||||
parentNode(node)!,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized
|
||||
)
|
||||
vnode.targetAnchor = targetNode
|
||||
} else {
|
||||
vnode.anchor = nextSibling(node)
|
||||
vnode.targetAnchor = hydrateChildren(
|
||||
targetNode,
|
||||
vnode,
|
||||
target,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized
|
||||
)
|
||||
}
|
||||
;(target as TeleportTargetElement)._lpa = nextSibling(
|
||||
vnode.targetAnchor as Node
|
||||
)
|
||||
}
|
||||
}
|
||||
return vnode.anchor && nextSibling(vnode.anchor as Node)
|
||||
}
|
||||
|
||||
// Force-casted public typing for h and TSX props inference
|
||||
export const Teleport = (TeleportImpl as any) as {
|
||||
__isTeleport: true
|
||||
new (): { $props: VNodeProps & TeleportProps }
|
||||
}
|
||||
@@ -12,9 +12,9 @@ return withDirectives(h(comp), [
|
||||
*/
|
||||
|
||||
import { VNode } from './vnode'
|
||||
import { isFunction, EMPTY_OBJ, makeMap, EMPTY_ARR } from '@vue/shared'
|
||||
import { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { ComponentInternalInstance } from './component'
|
||||
import { ComponentInternalInstance, Data } from './component'
|
||||
import { currentRenderingInstance } from './componentRenderUtils'
|
||||
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
@@ -28,20 +28,26 @@ export interface DirectiveBinding {
|
||||
dir: ObjectDirective
|
||||
}
|
||||
|
||||
export type DirectiveHook<T = any> = (
|
||||
export type DirectiveHook<T = any, Prev = VNode<any, T> | null> = (
|
||||
el: T,
|
||||
binding: DirectiveBinding,
|
||||
vnode: VNode<any, T>,
|
||||
prevVNode: VNode<any, T> | null
|
||||
prevVNode: Prev
|
||||
) => void
|
||||
|
||||
export type SSRDirectiveHook = (
|
||||
binding: DirectiveBinding,
|
||||
vnode: VNode
|
||||
) => Data | undefined
|
||||
|
||||
export interface ObjectDirective<T = any> {
|
||||
beforeMount?: DirectiveHook<T>
|
||||
mounted?: DirectiveHook<T>
|
||||
beforeUpdate?: DirectiveHook<T>
|
||||
updated?: DirectiveHook<T>
|
||||
beforeUnmount?: DirectiveHook<T>
|
||||
unmounted?: DirectiveHook<T>
|
||||
beforeMount?: DirectiveHook<T, null>
|
||||
mounted?: DirectiveHook<T, null>
|
||||
beforeUpdate?: DirectiveHook<T, VNode<any, T>>
|
||||
updated?: DirectiveHook<T, VNode<any, T>>
|
||||
beforeUnmount?: DirectiveHook<T, null>
|
||||
unmounted?: DirectiveHook<T, null>
|
||||
getSSRProps?: SSRDirectiveHook
|
||||
}
|
||||
|
||||
export type FunctionDirective<T = any> = DirectiveHook<T>
|
||||
@@ -66,36 +72,6 @@ export function validateDirectiveName(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const directiveToVnodeHooksMap = /*#__PURE__*/ [
|
||||
'beforeMount',
|
||||
'mounted',
|
||||
'beforeUpdate',
|
||||
'updated',
|
||||
'beforeUnmount',
|
||||
'unmounted'
|
||||
].reduce(
|
||||
(map, key: keyof ObjectDirective) => {
|
||||
const vnodeKey = `onVnode` + key[0].toUpperCase() + key.slice(1)
|
||||
const vnodeHook = (vnode: VNode, prevVnode: VNode | null) => {
|
||||
const bindings = vnode.dirs!
|
||||
const prevBindings = prevVnode ? prevVnode.dirs! : EMPTY_ARR
|
||||
for (let i = 0; i < bindings.length; i++) {
|
||||
const binding = bindings[i]
|
||||
const hook = binding.dir[key]
|
||||
if (hook != null) {
|
||||
if (prevVnode != null) {
|
||||
binding.oldValue = prevBindings[i].value
|
||||
}
|
||||
hook(vnode.el, binding, vnode, prevVnode)
|
||||
}
|
||||
}
|
||||
}
|
||||
map[key] = [vnodeKey, vnodeHook]
|
||||
return map
|
||||
},
|
||||
{} as Record<string, [string, Function]>
|
||||
)
|
||||
|
||||
// Directive, value, argument, modifiers
|
||||
export type DirectiveArguments = Array<
|
||||
| [Directive]
|
||||
@@ -114,9 +90,7 @@ export function withDirectives<T extends VNode>(
|
||||
return vnode
|
||||
}
|
||||
const instance = internalInstance.proxy
|
||||
const props = vnode.props || (vnode.props = {})
|
||||
const bindings = vnode.dirs || (vnode.dirs = new Array(directives.length))
|
||||
const injected: Record<string, true> = {}
|
||||
const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
|
||||
for (let i = 0; i < directives.length; i++) {
|
||||
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
|
||||
if (isFunction(dir)) {
|
||||
@@ -125,35 +99,39 @@ export function withDirectives<T extends VNode>(
|
||||
updated: dir
|
||||
} as ObjectDirective
|
||||
}
|
||||
bindings[i] = {
|
||||
bindings.push({
|
||||
dir,
|
||||
instance,
|
||||
value,
|
||||
oldValue: void 0,
|
||||
arg,
|
||||
modifiers
|
||||
}
|
||||
// inject onVnodeXXX hooks
|
||||
for (const key in dir) {
|
||||
if (!injected[key]) {
|
||||
const { 0: hookName, 1: hook } = directiveToVnodeHooksMap[key]
|
||||
const existing = props[hookName]
|
||||
props[hookName] = existing ? [].concat(existing, hook as any) : hook
|
||||
injected[key] = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return vnode
|
||||
}
|
||||
|
||||
export function invokeDirectiveHook(
|
||||
hook: ((...args: any[]) => any) | ((...args: any[]) => any)[],
|
||||
instance: ComponentInternalInstance | null,
|
||||
vnode: VNode,
|
||||
prevVNode: VNode | null = null
|
||||
prevVNode: VNode | null,
|
||||
instance: ComponentInternalInstance | null,
|
||||
name: keyof ObjectDirective
|
||||
) {
|
||||
callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
|
||||
vnode,
|
||||
prevVNode
|
||||
])
|
||||
const bindings = vnode.dirs!
|
||||
const oldBindings = prevVNode && prevVNode.dirs!
|
||||
for (let i = 0; i < bindings.length; i++) {
|
||||
const binding = bindings[i]
|
||||
if (oldBindings) {
|
||||
binding.oldValue = oldBindings[i].value
|
||||
}
|
||||
const hook = binding.dir[name] as DirectiveHook | undefined
|
||||
if (hook) {
|
||||
callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
|
||||
vnode.el,
|
||||
binding,
|
||||
vnode,
|
||||
prevVNode
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ export const enum ErrorCodes {
|
||||
WATCH_CLEANUP,
|
||||
NATIVE_EVENT_HANDLER,
|
||||
COMPONENT_EVENT_HANDLER,
|
||||
VNODE_HOOK,
|
||||
DIRECTIVE_HOOK,
|
||||
TRANSITION_HOOK,
|
||||
APP_ERROR_HANDLER,
|
||||
APP_WARN_HANDLER,
|
||||
FUNCTION_REF,
|
||||
ASYNC_COMPONENT_LOADER,
|
||||
SCHEDULER
|
||||
}
|
||||
|
||||
@@ -42,14 +44,16 @@ export const ErrorTypeStrings: Record<number | string, string> = {
|
||||
[ErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
|
||||
[ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler',
|
||||
[ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler',
|
||||
[ErrorCodes.VNODE_HOOK]: 'vnode hook',
|
||||
[ErrorCodes.DIRECTIVE_HOOK]: 'directive hook',
|
||||
[ErrorCodes.TRANSITION_HOOK]: 'transition hook',
|
||||
[ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
|
||||
[ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
|
||||
[ErrorCodes.FUNCTION_REF]: 'ref function',
|
||||
[ErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader',
|
||||
[ErrorCodes.SCHEDULER]:
|
||||
'scheduler flush. This is likely a Vue internals bug. ' +
|
||||
'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue'
|
||||
'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next'
|
||||
}
|
||||
|
||||
export type ErrorTypes = LifecycleHooks | ErrorCodes
|
||||
@@ -74,24 +78,26 @@ export function callWithAsyncErrorHandling(
|
||||
instance: ComponentInternalInstance | null,
|
||||
type: ErrorTypes,
|
||||
args?: unknown[]
|
||||
) {
|
||||
): any[] {
|
||||
if (isFunction(fn)) {
|
||||
const res = callWithErrorHandling(fn, instance, type, args)
|
||||
if (res != null && !res._isVue && isPromise(res)) {
|
||||
res.catch((err: Error) => {
|
||||
if (res && !res._isVue && isPromise(res)) {
|
||||
res.catch(err => {
|
||||
handleError(err, instance, type)
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const values = []
|
||||
for (let i = 0; i < fn.length; i++) {
|
||||
callWithAsyncErrorHandling(fn[i], instance, type, args)
|
||||
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
export function handleError(
|
||||
err: Error,
|
||||
err: unknown,
|
||||
instance: ComponentInternalInstance | null,
|
||||
type: ErrorTypes
|
||||
) {
|
||||
@@ -104,7 +110,7 @@ export function handleError(
|
||||
const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
|
||||
while (cur) {
|
||||
const errorCapturedHooks = cur.ec
|
||||
if (errorCapturedHooks !== null) {
|
||||
if (errorCapturedHooks) {
|
||||
for (let i = 0; i < errorCapturedHooks.length; i++) {
|
||||
if (errorCapturedHooks[i](err, exposedInstance, errorInfo)) {
|
||||
return
|
||||
@@ -134,7 +140,7 @@ export function setErrorRecovery(value: boolean) {
|
||||
forceRecover = value
|
||||
}
|
||||
|
||||
function logError(err: Error, type: ErrorTypes, contextVNode: VNode | null) {
|
||||
function logError(err: unknown, type: ErrorTypes, contextVNode: VNode | null) {
|
||||
// default behavior is crash in prod & test, recover in dev.
|
||||
if (__DEV__ && (forceRecover || !__TEST__)) {
|
||||
const info = ErrorTypeStrings[type]
|
||||
|
||||
@@ -2,22 +2,16 @@ import {
|
||||
VNode,
|
||||
VNodeProps,
|
||||
createVNode,
|
||||
VNodeChildren,
|
||||
VNodeArrayChildren,
|
||||
Fragment,
|
||||
Portal,
|
||||
isVNode
|
||||
} from './vnode'
|
||||
import { Teleport, TeleportProps } from './components/Teleport'
|
||||
import { Suspense, SuspenseProps } from './components/Suspense'
|
||||
import { isObject, isArray } from '@vue/shared'
|
||||
import { RawSlots } from './componentSlots'
|
||||
import { FunctionalComponent } from './component'
|
||||
import {
|
||||
ComponentOptionsWithoutProps,
|
||||
ComponentOptionsWithArrayProps,
|
||||
ComponentOptionsWithObjectProps,
|
||||
ComponentOptions
|
||||
} from './apiOptions'
|
||||
import { ExtractPropTypes } from './componentProps'
|
||||
import { FunctionalComponent, Component } from './component'
|
||||
import { ComponentOptions } from './componentOptions'
|
||||
|
||||
// `h` is a more user-friendly version of `createVNode` that allows omitting the
|
||||
// props when possible. It is intended for manually written render functions.
|
||||
@@ -62,13 +56,13 @@ type RawChildren =
|
||||
| number
|
||||
| boolean
|
||||
| VNode
|
||||
| VNodeChildren
|
||||
| VNodeArrayChildren
|
||||
| (() => any)
|
||||
|
||||
// fake constructor type returned from `createComponent`
|
||||
// fake constructor type returned from `defineComponent`
|
||||
interface Constructor<P = any> {
|
||||
__isFragment?: never
|
||||
__isPortal?: never
|
||||
__isTeleport?: never
|
||||
__isSuspense?: never
|
||||
new (): { $props: P }
|
||||
}
|
||||
@@ -85,17 +79,17 @@ export function h(
|
||||
): VNode
|
||||
|
||||
// fragment
|
||||
export function h(type: typeof Fragment, children?: VNodeChildren): VNode
|
||||
export function h(type: typeof Fragment, children?: VNodeArrayChildren): VNode
|
||||
export function h(
|
||||
type: typeof Fragment,
|
||||
props?: RawProps | null,
|
||||
children?: VNodeChildren
|
||||
children?: VNodeArrayChildren
|
||||
): VNode
|
||||
|
||||
// portal (target prop is required)
|
||||
// teleport (target prop is required)
|
||||
export function h(
|
||||
type: typeof Portal,
|
||||
props: RawProps & { target: any },
|
||||
type: typeof Teleport,
|
||||
props: RawProps & TeleportProps,
|
||||
children: RawChildren
|
||||
): VNode
|
||||
|
||||
@@ -108,29 +102,21 @@ export function h(
|
||||
): VNode
|
||||
|
||||
// functional component
|
||||
export function h(type: FunctionalComponent, children?: RawChildren): VNode
|
||||
export function h<P>(
|
||||
type: FunctionalComponent<P>,
|
||||
props?: (RawProps & P) | ({} extends P ? null : never),
|
||||
children?: RawChildren | RawSlots
|
||||
): VNode
|
||||
|
||||
// stateful component
|
||||
export function h(type: ComponentOptions, children?: RawChildren): VNode
|
||||
// catch-all for generic component types
|
||||
export function h(type: Component, children?: RawChildren): VNode
|
||||
export function h(
|
||||
type: ComponentOptionsWithoutProps | ComponentOptionsWithArrayProps,
|
||||
type: ComponentOptions | FunctionalComponent<{}>,
|
||||
props?: RawProps | null,
|
||||
children?: RawChildren | RawSlots
|
||||
): VNode
|
||||
export function h<O>(
|
||||
type: ComponentOptionsWithObjectProps<O>,
|
||||
props?:
|
||||
| (RawProps & ExtractPropTypes<O>)
|
||||
| ({} extends ExtractPropTypes<O> ? null : never),
|
||||
children?: RawChildren | RawSlots
|
||||
): VNode
|
||||
|
||||
// fake constructor type returned by `createComponent`
|
||||
// fake constructor type returned by `defineComponent` or class component
|
||||
export function h(type: Constructor, children?: RawChildren): VNode
|
||||
export function h<P>(
|
||||
type: Constructor<P>,
|
||||
|
||||
@@ -8,7 +8,10 @@ interface CompiledSlotDescriptor {
|
||||
|
||||
export function createSlots(
|
||||
slots: Record<string, Slot>,
|
||||
dynamicSlots: (CompiledSlotDescriptor | CompiledSlotDescriptor[])[]
|
||||
dynamicSlots: (
|
||||
| CompiledSlotDescriptor
|
||||
| CompiledSlotDescriptor[]
|
||||
| undefined)[]
|
||||
): Record<string, Slot> {
|
||||
for (let i = 0; i < dynamicSlots.length; i++) {
|
||||
const slot = dynamicSlots[i]
|
||||
@@ -17,7 +20,7 @@ export function createSlots(
|
||||
for (let j = 0; j < slot.length; j++) {
|
||||
slots[slot[j].name] = slot[j].fn
|
||||
}
|
||||
} else {
|
||||
} else if (slot) {
|
||||
// conditional single slot generated by <template v-if="..." #foo>
|
||||
slots[slot.name] = slot.fn
|
||||
}
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
import { VNodeChild } from '../vnode'
|
||||
import { isArray, isString, isObject } from '@vue/shared'
|
||||
|
||||
// v-for string
|
||||
export function renderList(
|
||||
source: unknown,
|
||||
renderItem: (
|
||||
value: unknown,
|
||||
key: string | number,
|
||||
index?: number
|
||||
source: string,
|
||||
renderItem: (value: string, index: number) => VNodeChild
|
||||
): VNodeChild[]
|
||||
|
||||
// v-for number
|
||||
export function renderList(
|
||||
source: number,
|
||||
renderItem: (value: number, index: number) => VNodeChild
|
||||
): VNodeChild[]
|
||||
|
||||
// v-for array
|
||||
export function renderList<T>(
|
||||
source: T[],
|
||||
renderItem: (value: T, index: number) => VNodeChild
|
||||
): VNodeChild[]
|
||||
|
||||
// v-for iterable
|
||||
export function renderList<T>(
|
||||
source: Iterable<T>,
|
||||
renderItem: (value: T, index: number) => VNodeChild
|
||||
): VNodeChild[]
|
||||
|
||||
// v-for object
|
||||
export function renderList<T>(
|
||||
source: T,
|
||||
renderItem: <K extends keyof T>(
|
||||
value: T[K],
|
||||
key: K,
|
||||
index: number
|
||||
) => VNodeChild
|
||||
): VNodeChild[]
|
||||
|
||||
// actual implementation
|
||||
export function renderList(
|
||||
source: any,
|
||||
renderItem: (...args: any[]) => VNodeChild
|
||||
): VNodeChild[] {
|
||||
let ret: VNodeChild[]
|
||||
if (isArray(source) || isString(source)) {
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
import { Data } from '../component'
|
||||
import { Slot } from '../componentSlots'
|
||||
import { Slots } from '../componentSlots'
|
||||
import {
|
||||
VNodeChildren,
|
||||
VNodeArrayChildren,
|
||||
openBlock,
|
||||
createBlock,
|
||||
Fragment,
|
||||
VNode
|
||||
} from '../vnode'
|
||||
import { PatchFlags } from '@vue/shared'
|
||||
import { warn } from '../warning'
|
||||
|
||||
export function renderSlot(
|
||||
slots: Record<string, Slot>,
|
||||
slots: Slots,
|
||||
name: string,
|
||||
props: Data = {},
|
||||
// this is not a user-facing function, so the fallback is always generated by
|
||||
// the compiler and guaranteed to be an array
|
||||
fallback?: VNodeChildren
|
||||
fallback?: VNodeArrayChildren
|
||||
): VNode {
|
||||
const slot = slots[name]
|
||||
let slot = slots[name]
|
||||
|
||||
if (__DEV__ && slot && slot.length > 1) {
|
||||
warn(
|
||||
`SSR-optimized slot function detected in a non-SSR-optimized render ` +
|
||||
`function. You need to mark this component with $dynamic-slots in the ` +
|
||||
`parent template.`
|
||||
)
|
||||
slot = () => []
|
||||
}
|
||||
|
||||
return (
|
||||
openBlock(),
|
||||
createBlock(
|
||||
Fragment,
|
||||
{ key: props.key },
|
||||
slot ? slot(props) : fallback || [],
|
||||
slots._compiled ? 0 : PatchFlags.BAIL
|
||||
slots._ ? PatchFlags.STABLE_FRAGMENT : PatchFlags.BAIL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { currentRenderingInstance } from '../componentRenderUtils'
|
||||
import {
|
||||
currentInstance,
|
||||
Component,
|
||||
ComponentInternalInstance,
|
||||
FunctionalComponent
|
||||
} from '../component'
|
||||
import { currentInstance, Component, FunctionalComponent } from '../component'
|
||||
import { Directive } from '../directives'
|
||||
import {
|
||||
camelize,
|
||||
@@ -18,21 +13,16 @@ import { warn } from '../warning'
|
||||
const COMPONENTS = 'components'
|
||||
const DIRECTIVES = 'directives'
|
||||
|
||||
export function resolveComponent(name: string): Component | undefined {
|
||||
return resolveAsset(COMPONENTS, name)
|
||||
export function resolveComponent(name: string): Component | string | undefined {
|
||||
return resolveAsset(COMPONENTS, name) || name
|
||||
}
|
||||
|
||||
export function resolveDynamicComponent(
|
||||
component: unknown,
|
||||
// Dynamic component resolution has to be called inline due to potential
|
||||
// access to scope variables. When called inside slots it will be inside
|
||||
// a different component's render cycle, so the owner instance must be passed
|
||||
// in explicitly.
|
||||
instance: ComponentInternalInstance
|
||||
): Component | undefined {
|
||||
component: unknown
|
||||
): Component | string | undefined {
|
||||
if (!component) return
|
||||
if (isString(component)) {
|
||||
return resolveAsset(COMPONENTS, component, instance)
|
||||
return resolveAsset(COMPONENTS, component, false) || component
|
||||
} else if (isFunction(component) || isObject(component)) {
|
||||
return component
|
||||
}
|
||||
@@ -46,21 +36,20 @@ export function resolveDirective(name: string): Directive | undefined {
|
||||
function resolveAsset(
|
||||
type: typeof COMPONENTS,
|
||||
name: string,
|
||||
instance?: ComponentInternalInstance
|
||||
warnMissing?: boolean
|
||||
): Component | undefined
|
||||
// overload 2: directives
|
||||
function resolveAsset(
|
||||
type: typeof DIRECTIVES,
|
||||
name: string,
|
||||
instance?: ComponentInternalInstance
|
||||
name: string
|
||||
): Directive | undefined
|
||||
|
||||
function resolveAsset(
|
||||
type: typeof COMPONENTS | typeof DIRECTIVES,
|
||||
name: string,
|
||||
instance: ComponentInternalInstance | null = currentRenderingInstance ||
|
||||
currentInstance
|
||||
warnMissing = true
|
||||
) {
|
||||
const instance = currentRenderingInstance || currentInstance
|
||||
if (instance) {
|
||||
let camelized, capitalized
|
||||
const registry = instance[type]
|
||||
@@ -80,7 +69,7 @@ function resolveAsset(
|
||||
res = self
|
||||
}
|
||||
}
|
||||
if (__DEV__ && !res) {
|
||||
if (__DEV__ && warnMissing && !res) {
|
||||
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
|
||||
}
|
||||
return res
|
||||
|
||||
27
packages/runtime-core/src/helpers/scopeId.ts
Normal file
27
packages/runtime-core/src/helpers/scopeId.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// SFC scoped style ID management.
|
||||
// These are only used in esm-bundler builds, but since exports cannot be
|
||||
// conditional, we can only drop inner implementations in non-bundler builds.
|
||||
|
||||
import { withCtx } from './withRenderContext'
|
||||
|
||||
export let currentScopeId: string | null = null
|
||||
const scopeIdStack: string[] = []
|
||||
|
||||
export function pushScopeId(id: string) {
|
||||
scopeIdStack.push((currentScopeId = id))
|
||||
}
|
||||
|
||||
export function popScopeId() {
|
||||
scopeIdStack.pop()
|
||||
currentScopeId = scopeIdStack[scopeIdStack.length - 1] || null
|
||||
}
|
||||
|
||||
export function withScopeId(id: string): <T extends Function>(fn: T) => T {
|
||||
return ((fn: Function) =>
|
||||
withCtx(function(this: any) {
|
||||
pushScopeId(id)
|
||||
const res = fn.apply(this, arguments)
|
||||
popScopeId()
|
||||
return res
|
||||
})) as any
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { isArray, isPlainObject, objectToString } from '@vue/shared'
|
||||
|
||||
// for converting {{ interpolation }} values to displayed strings.
|
||||
export function toString(val: unknown): string {
|
||||
return val == null
|
||||
? ''
|
||||
: isArray(val) || (isPlainObject(val) && val.toString === objectToString)
|
||||
? JSON.stringify(val, null, 2)
|
||||
: String(val)
|
||||
}
|
||||
30
packages/runtime-core/src/helpers/useCssModule.ts
Normal file
30
packages/runtime-core/src/helpers/useCssModule.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { getCurrentInstance } from '../component'
|
||||
import { EMPTY_OBJ } from '@vue/shared'
|
||||
import { warn } from '../warning'
|
||||
|
||||
export const useCSSModule = (name = '$style'): Record<string, string> => {
|
||||
if (!__GLOBAL__) {
|
||||
const instance = getCurrentInstance()!
|
||||
if (!instance) {
|
||||
__DEV__ && warn(`useCSSModule must be called inside setup()`)
|
||||
return EMPTY_OBJ
|
||||
}
|
||||
const modules = instance.type.__cssModules
|
||||
if (!modules) {
|
||||
__DEV__ && warn(`Current instance does not have CSS modules injected.`)
|
||||
return EMPTY_OBJ
|
||||
}
|
||||
const mod = modules[name]
|
||||
if (!mod) {
|
||||
__DEV__ &&
|
||||
warn(`Current instance does not have CSS module named "${name}".`)
|
||||
return EMPTY_OBJ
|
||||
}
|
||||
return mod as Record<string, string>
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
warn(`useCSSModule() is not supported in the global build.`)
|
||||
}
|
||||
return EMPTY_OBJ
|
||||
}
|
||||
}
|
||||
19
packages/runtime-core/src/helpers/useSsrContext.ts
Normal file
19
packages/runtime-core/src/helpers/useSsrContext.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { inject } from '../apiInject'
|
||||
import { warn } from '../warning'
|
||||
|
||||
export const ssrContextKey = Symbol(__DEV__ ? `ssrContext` : ``)
|
||||
|
||||
export const useSSRContext = <T = Record<string, any>>() => {
|
||||
if (!__GLOBAL__) {
|
||||
const ctx = inject<T>(ssrContextKey)
|
||||
if (!ctx) {
|
||||
warn(
|
||||
`Server rendering context not provided. Make sure to only call ` +
|
||||
`useSsrContext() conditionally in the server build.`
|
||||
)
|
||||
}
|
||||
return ctx
|
||||
} else if (__DEV__) {
|
||||
warn(`useSsrContext() is not supported in the global build.`)
|
||||
}
|
||||
}
|
||||
20
packages/runtime-core/src/helpers/withRenderContext.ts
Normal file
20
packages/runtime-core/src/helpers/withRenderContext.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Slot } from '../componentSlots'
|
||||
import {
|
||||
setCurrentRenderingInstance,
|
||||
currentRenderingInstance
|
||||
} from '../componentRenderUtils'
|
||||
import { ComponentInternalInstance } from '../component'
|
||||
|
||||
export function withCtx(
|
||||
fn: Slot,
|
||||
ctx: ComponentInternalInstance | null = currentRenderingInstance
|
||||
) {
|
||||
if (!ctx) return fn
|
||||
return function renderFnWithContext() {
|
||||
const owner = currentRenderingInstance
|
||||
setCurrentRenderingInstance(ctx)
|
||||
const res = fn.apply(null, arguments as any)
|
||||
setCurrentRenderingInstance(owner)
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,12 @@ import {
|
||||
} from './component'
|
||||
import { queueJob, queuePostFlushCb } from './scheduler'
|
||||
|
||||
export interface HMRRuntime {
|
||||
createRecord: typeof createRecord
|
||||
rerender: typeof rerender
|
||||
reload: typeof reload
|
||||
}
|
||||
|
||||
// Expose the HMR runtime on the global object
|
||||
// This makes it entirely tree-shakable without polluting the exports and makes
|
||||
// it easier to be used in toolings like vue-loader
|
||||
@@ -24,7 +30,7 @@ if (__BUNDLER__ && __DEV__) {
|
||||
createRecord: tryWrap(createRecord),
|
||||
rerender: tryWrap(rerender),
|
||||
reload: tryWrap(reload)
|
||||
}
|
||||
} as HMRRuntime
|
||||
}
|
||||
|
||||
interface HMRRecord {
|
||||
@@ -53,9 +59,13 @@ function createRecord(id: string, comp: ComponentOptions): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
function rerender(id: string, newRender: RenderFunction) {
|
||||
map.get(id)!.instances.forEach(instance => {
|
||||
instance.render = newRender
|
||||
function rerender(id: string, newRender?: RenderFunction) {
|
||||
// Array.from creates a snapshot which avoids the set being mutated during
|
||||
// updates
|
||||
Array.from(map.get(id)!.instances).forEach(instance => {
|
||||
if (newRender) {
|
||||
instance.render = newRender
|
||||
}
|
||||
instance.renderCache = []
|
||||
// this flag forces child components with slot content to update
|
||||
instance.renderUpdated = true
|
||||
@@ -77,13 +87,19 @@ function reload(id: string, newComp: ComponentOptions) {
|
||||
// 2. Mark component dirty. This forces the renderer to replace the component
|
||||
// on patch.
|
||||
comp.__hmrUpdated = true
|
||||
record.instances.forEach(instance => {
|
||||
// Array.from creates a snapshot which avoids the set being mutated during
|
||||
// updates
|
||||
Array.from(record.instances).forEach(instance => {
|
||||
if (instance.parent) {
|
||||
// 3. Force the parent instance to re-render. This will cause all updated
|
||||
// components to be unmounted and re-mounted. Queue the update so that we
|
||||
// don't end up forcing the same parent to re-render multiple times.
|
||||
queueJob(instance.parent.update)
|
||||
} else if (instance.appContext.reload) {
|
||||
// root instance mounted via createApp() has a reload method
|
||||
instance.appContext.reload()
|
||||
} else if (typeof window !== 'undefined') {
|
||||
// root instance inside tree created via raw render(). Force reload.
|
||||
window.location.reload()
|
||||
} else {
|
||||
console.warn(
|
||||
|
||||
441
packages/runtime-core/src/hydration.ts
Normal file
441
packages/runtime-core/src/hydration.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
import {
|
||||
VNode,
|
||||
normalizeVNode,
|
||||
Text,
|
||||
Comment,
|
||||
Static,
|
||||
Fragment,
|
||||
VNodeHook
|
||||
} from './vnode'
|
||||
import { flushPostFlushCbs } from './scheduler'
|
||||
import { ComponentOptions, ComponentInternalInstance } from './component'
|
||||
import { invokeDirectiveHook } from './directives'
|
||||
import { warn } from './warning'
|
||||
import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
|
||||
import { RendererInternals, invokeVNodeHook } from './renderer'
|
||||
import {
|
||||
SuspenseImpl,
|
||||
SuspenseBoundary,
|
||||
queueEffectWithSuspense
|
||||
} from './components/Suspense'
|
||||
import { TeleportImpl } from './components/Teleport'
|
||||
|
||||
export type RootHydrateFunction = (
|
||||
vnode: VNode<Node, Element>,
|
||||
container: Element
|
||||
) => void
|
||||
|
||||
const enum DOMNodeTypes {
|
||||
ELEMENT = 1,
|
||||
TEXT = 3,
|
||||
COMMENT = 8
|
||||
}
|
||||
|
||||
let hasMismatch = false
|
||||
|
||||
const isSVGContainer = (container: Element) =>
|
||||
/svg/.test(container.namespaceURI!) && container.tagName !== 'foreignObject'
|
||||
|
||||
const isComment = (node: Node): node is Comment =>
|
||||
node.nodeType === DOMNodeTypes.COMMENT
|
||||
|
||||
// Note: hydration is DOM-specific
|
||||
// But we have to place it in core due to tight coupling with core - splitting
|
||||
// it out creates a ton of unnecessary complexity.
|
||||
// Hydration also depends on some renderer internal logic which needs to be
|
||||
// passed in via arguments.
|
||||
export function createHydrationFunctions(
|
||||
rendererInternals: RendererInternals<Node, Element>
|
||||
) {
|
||||
const {
|
||||
mt: mountComponent,
|
||||
p: patch,
|
||||
o: { patchProp, nextSibling, parentNode, remove, insert, createComment }
|
||||
} = rendererInternals
|
||||
|
||||
const hydrate: RootHydrateFunction = (vnode, container) => {
|
||||
if (__DEV__ && !container.hasChildNodes()) {
|
||||
warn(
|
||||
`Attempting to hydrate existing markup but container is empty. ` +
|
||||
`Performing full mount instead.`
|
||||
)
|
||||
patch(null, vnode, container)
|
||||
return
|
||||
}
|
||||
hasMismatch = false
|
||||
hydrateNode(container.firstChild!, vnode, null, null)
|
||||
flushPostFlushCbs()
|
||||
if (hasMismatch && !__TEST__) {
|
||||
// this error should show up in production
|
||||
console.error(`Hydration completed but contains mismatches.`)
|
||||
}
|
||||
}
|
||||
|
||||
const hydrateNode = (
|
||||
node: Node,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
optimized = false
|
||||
): Node | null => {
|
||||
const isFragmentStart = isComment(node) && node.data === '['
|
||||
const onMismatch = () =>
|
||||
handleMismtach(
|
||||
node,
|
||||
vnode,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isFragmentStart
|
||||
)
|
||||
|
||||
const { type, shapeFlag } = vnode
|
||||
const domType = node.nodeType
|
||||
vnode.el = node
|
||||
|
||||
switch (type) {
|
||||
case Text:
|
||||
if (domType !== DOMNodeTypes.TEXT) {
|
||||
return onMismatch()
|
||||
}
|
||||
if ((node as Text).data !== vnode.children) {
|
||||
hasMismatch = true
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Hydration text mismatch:` +
|
||||
`\n- Client: ${JSON.stringify(vnode.children)}`,
|
||||
`\n- Server: ${JSON.stringify((node as Text).data)}`
|
||||
)
|
||||
;(node as Text).data = vnode.children as string
|
||||
}
|
||||
return nextSibling(node)
|
||||
case Comment:
|
||||
if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
|
||||
return onMismatch()
|
||||
}
|
||||
return nextSibling(node)
|
||||
case Static:
|
||||
if (domType !== DOMNodeTypes.ELEMENT) {
|
||||
return onMismatch()
|
||||
}
|
||||
return nextSibling(node)
|
||||
case Fragment:
|
||||
if (!isFragmentStart) {
|
||||
return onMismatch()
|
||||
}
|
||||
return hydrateFragment(
|
||||
node as Comment,
|
||||
vnode,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized
|
||||
)
|
||||
default:
|
||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||
if (
|
||||
domType !== DOMNodeTypes.ELEMENT ||
|
||||
vnode.type !== (node as Element).tagName.toLowerCase()
|
||||
) {
|
||||
return onMismatch()
|
||||
}
|
||||
return hydrateElement(
|
||||
node as Element,
|
||||
vnode,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized
|
||||
)
|
||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||
// when setting up the render effect, if the initial vnode already
|
||||
// has .el set, the component will perform hydration instead of mount
|
||||
// on its sub-tree.
|
||||
const container = parentNode(node)!
|
||||
const hydrateComponent = () => {
|
||||
mountComponent(
|
||||
vnode,
|
||||
container,
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVGContainer(container),
|
||||
optimized
|
||||
)
|
||||
}
|
||||
// async component
|
||||
const loadAsync = (vnode.type as ComponentOptions).__asyncLoader
|
||||
if (loadAsync) {
|
||||
loadAsync().then(hydrateComponent)
|
||||
} else {
|
||||
hydrateComponent()
|
||||
}
|
||||
// component may be async, so in the case of fragments we cannot rely
|
||||
// on component's rendered output to determine the end of the fragment
|
||||
// instead, we do a lookahead to find the end anchor node.
|
||||
return isFragmentStart
|
||||
? locateClosingAsyncAnchor(node)
|
||||
: nextSibling(node)
|
||||
} else if (shapeFlag & ShapeFlags.TELEPORT) {
|
||||
if (domType !== DOMNodeTypes.COMMENT) {
|
||||
return onMismatch()
|
||||
}
|
||||
return (vnode.type as typeof TeleportImpl).hydrate(
|
||||
node,
|
||||
vnode,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized,
|
||||
rendererInternals,
|
||||
hydrateChildren
|
||||
)
|
||||
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
|
||||
return (vnode.type as typeof SuspenseImpl).hydrate(
|
||||
node,
|
||||
vnode,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVGContainer(parentNode(node)!),
|
||||
optimized,
|
||||
rendererInternals,
|
||||
hydrateNode
|
||||
)
|
||||
} else if (__DEV__) {
|
||||
warn('Invalid HostVNode type:', type, `(${typeof type})`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const hydrateElement = (
|
||||
el: Element,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
optimized: boolean
|
||||
) => {
|
||||
optimized = optimized || !!vnode.dynamicChildren
|
||||
const { props, patchFlag, shapeFlag, dirs } = vnode
|
||||
// skip props & children if this is hoisted static nodes
|
||||
if (patchFlag !== PatchFlags.HOISTED) {
|
||||
// props
|
||||
if (props) {
|
||||
if (
|
||||
!optimized ||
|
||||
(patchFlag & PatchFlags.FULL_PROPS ||
|
||||
patchFlag & PatchFlags.HYDRATE_EVENTS)
|
||||
) {
|
||||
for (const key in props) {
|
||||
if (!isReservedProp(key) && isOn(key)) {
|
||||
patchProp(el, key, null, props[key])
|
||||
}
|
||||
}
|
||||
} else if (props.onClick) {
|
||||
// Fast path for click listeners (which is most often) to avoid
|
||||
// iterating through props.
|
||||
patchProp(el, 'onClick', null, props.onClick)
|
||||
}
|
||||
}
|
||||
// vnode / directive hooks
|
||||
let vnodeHooks: VNodeHook | null | undefined
|
||||
if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
|
||||
invokeVNodeHook(vnodeHooks, parentComponent, vnode)
|
||||
}
|
||||
if (dirs) {
|
||||
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
|
||||
}
|
||||
if ((vnodeHooks = props && props.onVnodeMounted) || dirs) {
|
||||
queueEffectWithSuspense(() => {
|
||||
vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
|
||||
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
|
||||
}, parentSuspense)
|
||||
}
|
||||
// children
|
||||
if (
|
||||
shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
|
||||
// skip if element has innerHTML / textContent
|
||||
!(props && (props.innerHTML || props.textContent))
|
||||
) {
|
||||
let next = hydrateChildren(
|
||||
el.firstChild,
|
||||
vnode,
|
||||
el,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized
|
||||
)
|
||||
let hasWarned = false
|
||||
while (next) {
|
||||
hasMismatch = true
|
||||
if (__DEV__ && !hasWarned) {
|
||||
warn(
|
||||
`Hydration children mismatch in <${vnode.type as string}>: ` +
|
||||
`server rendered element contains more child nodes than client vdom.`
|
||||
)
|
||||
hasWarned = true
|
||||
}
|
||||
// The SSRed DOM contains more nodes than it should. Remove them.
|
||||
const cur = next
|
||||
next = next.nextSibling
|
||||
remove(cur)
|
||||
}
|
||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
if (el.textContent !== vnode.children) {
|
||||
hasMismatch = true
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Hydration text content mismatch in <${vnode.type as string}>:\n` +
|
||||
`- Client: ${el.textContent}\n` +
|
||||
`- Server: ${vnode.children as string}`
|
||||
)
|
||||
el.textContent = vnode.children as string
|
||||
}
|
||||
}
|
||||
}
|
||||
return el.nextSibling
|
||||
}
|
||||
|
||||
const hydrateChildren = (
|
||||
node: Node | null,
|
||||
vnode: VNode,
|
||||
container: Element,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
optimized: boolean
|
||||
): Node | null => {
|
||||
optimized = optimized || !!vnode.dynamicChildren
|
||||
const children = vnode.children as VNode[]
|
||||
const l = children.length
|
||||
let hasWarned = false
|
||||
for (let i = 0; i < l; i++) {
|
||||
const vnode = optimized
|
||||
? children[i]
|
||||
: (children[i] = normalizeVNode(children[i]))
|
||||
if (node) {
|
||||
node = hydrateNode(
|
||||
node,
|
||||
vnode,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized
|
||||
)
|
||||
} else {
|
||||
hasMismatch = true
|
||||
if (__DEV__ && !hasWarned) {
|
||||
warn(
|
||||
`Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
|
||||
`server rendered element contains fewer child nodes than client vdom.`
|
||||
)
|
||||
hasWarned = true
|
||||
}
|
||||
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
|
||||
patch(
|
||||
null,
|
||||
vnode,
|
||||
container,
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVGContainer(container)
|
||||
)
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
const hydrateFragment = (
|
||||
node: Comment,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
optimized: boolean
|
||||
) => {
|
||||
const container = parentNode(node)!
|
||||
const next = hydrateChildren(
|
||||
nextSibling(node)!,
|
||||
vnode,
|
||||
container,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized
|
||||
)
|
||||
if (next && isComment(next) && next.data === ']') {
|
||||
return nextSibling((vnode.anchor = next))
|
||||
} else {
|
||||
// fragment didn't hydrate successfully, since we didn't get a end anchor
|
||||
// back. This should have led to node/children mismatch warnings.
|
||||
hasMismatch = true
|
||||
// since the anchor is missing, we need to create one and insert it
|
||||
insert((vnode.anchor = createComment(`]`)), container, next)
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
const handleMismtach = (
|
||||
node: Node,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
isFragment: boolean
|
||||
) => {
|
||||
hasMismatch = true
|
||||
__DEV__ &&
|
||||
warn(
|
||||
`Hydration node mismatch:\n- Client vnode:`,
|
||||
vnode.type,
|
||||
`\n- Server rendered DOM:`,
|
||||
node,
|
||||
node.nodeType === DOMNodeTypes.TEXT
|
||||
? `(text)`
|
||||
: isComment(node) && node.data === '['
|
||||
? `(start of fragment)`
|
||||
: ``
|
||||
)
|
||||
vnode.el = null
|
||||
|
||||
if (isFragment) {
|
||||
// remove excessive fragment nodes
|
||||
const end = locateClosingAsyncAnchor(node)
|
||||
while (true) {
|
||||
const next = nextSibling(node)
|
||||
if (next && next !== end) {
|
||||
remove(next)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const next = nextSibling(node)
|
||||
const container = parentNode(node)!
|
||||
remove(node)
|
||||
|
||||
patch(
|
||||
null,
|
||||
vnode,
|
||||
container,
|
||||
next,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVGContainer(container)
|
||||
)
|
||||
return next
|
||||
}
|
||||
|
||||
const locateClosingAsyncAnchor = (node: Node | null): Node | null => {
|
||||
let match = 0
|
||||
while (node) {
|
||||
node = nextSibling(node)
|
||||
if (node && isComment(node)) {
|
||||
if (node.data === '[') match++
|
||||
if (node.data === ']') {
|
||||
if (match === 0) {
|
||||
return nextSibling(node)
|
||||
} else {
|
||||
match--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
return [hydrate, hydrateNode] as const
|
||||
}
|
||||
@@ -1,12 +1,41 @@
|
||||
// Public API ------------------------------------------------------------------
|
||||
|
||||
export const version = __VERSION__
|
||||
export * from './apiReactivity'
|
||||
export * from './apiWatch'
|
||||
export * from './apiLifecycle'
|
||||
export * from './apiInject'
|
||||
export {
|
||||
effect,
|
||||
ref,
|
||||
unref,
|
||||
shallowRef,
|
||||
isRef,
|
||||
toRefs,
|
||||
reactive,
|
||||
isReactive,
|
||||
readonly,
|
||||
isReadonly,
|
||||
shallowReactive,
|
||||
toRaw,
|
||||
markReadonly,
|
||||
markNonReactive
|
||||
} from '@vue/reactivity'
|
||||
export { computed } from './apiComputed'
|
||||
export { watch, watchEffect } from './apiWatch'
|
||||
export {
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
onBeforeUpdate,
|
||||
onUpdated,
|
||||
onBeforeUnmount,
|
||||
onUnmounted,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onErrorCaptured
|
||||
} from './apiLifecycle'
|
||||
export { provide, inject } from './apiInject'
|
||||
export { nextTick } from './scheduler'
|
||||
export { createComponent } from './apiCreateComponent'
|
||||
export { defineComponent } from './apiDefineComponent'
|
||||
export { defineAsyncComponent } from './apiAsyncComponent'
|
||||
|
||||
// Advanced API ----------------------------------------------------------------
|
||||
|
||||
@@ -23,54 +52,42 @@ export {
|
||||
openBlock,
|
||||
createBlock
|
||||
} from './vnode'
|
||||
// VNode type symbols
|
||||
export { Text, Comment, Fragment, Portal } from './vnode'
|
||||
// Internal Components
|
||||
export { Text, Comment, Fragment } from './vnode'
|
||||
export { Teleport, TeleportProps } from './components/Teleport'
|
||||
export { Suspense, SuspenseProps } from './components/Suspense'
|
||||
export { KeepAlive, KeepAliveProps } from './components/KeepAlive'
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionProps
|
||||
} from './components/BaseTransition'
|
||||
// VNode flags
|
||||
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
|
||||
import { PublicPatchFlags } from '@vue/shared'
|
||||
export const PatchFlags = PublicPatchFlags as {
|
||||
// export patch flags as plain numbers to avoid d.ts relying on @vue/shared
|
||||
// the enum type is internal anyway.
|
||||
TEXT: number
|
||||
CLASS: number
|
||||
STYLE: number
|
||||
PROPS: number
|
||||
NEED_PATCH: number
|
||||
FULL_PROPS: number
|
||||
STABLE_FRAGMENT: number
|
||||
KEYED_FRAGMENT: number
|
||||
UNKEYED_FRAGMENT: number
|
||||
DYNAMIC_SLOTS: number
|
||||
BAIL: number
|
||||
}
|
||||
|
||||
// SFC CSS Modules
|
||||
export { useCSSModule } from './helpers/useCssModule'
|
||||
|
||||
// SSR context
|
||||
export { useSSRContext, ssrContextKey } from './helpers/useSsrContext'
|
||||
|
||||
// Internal API ----------------------------------------------------------------
|
||||
|
||||
// For custom renderers
|
||||
export { createRenderer, RootRenderFunction } from './renderer'
|
||||
export { createRenderer, createHydrationRenderer } from './renderer'
|
||||
export { warn } from './warning'
|
||||
export {
|
||||
handleError,
|
||||
callWithErrorHandling,
|
||||
callWithAsyncErrorHandling
|
||||
callWithAsyncErrorHandling,
|
||||
ErrorCodes
|
||||
} from './errorHandling'
|
||||
export {
|
||||
useTransitionState,
|
||||
TransitionState,
|
||||
resolveTransitionHooks,
|
||||
setTransitionHooks,
|
||||
TransitionHooks
|
||||
setTransitionHooks
|
||||
} from './components/BaseTransition'
|
||||
|
||||
// Internal API ----------------------------------------------------------------
|
||||
|
||||
// For compiler generated code
|
||||
// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
|
||||
export { withCtx } from './helpers/withRenderContext'
|
||||
export { withDirectives } from './directives'
|
||||
export {
|
||||
resolveComponent,
|
||||
@@ -78,25 +95,85 @@ export {
|
||||
resolveDynamicComponent
|
||||
} from './helpers/resolveAssets'
|
||||
export { renderList } from './helpers/renderList'
|
||||
export { toString } from './helpers/toString'
|
||||
export { toHandlers } from './helpers/toHandlers'
|
||||
export { renderSlot } from './helpers/renderSlot'
|
||||
export { createSlots } from './helpers/createSlots'
|
||||
export { setBlockTracking, createTextVNode, createCommentVNode } from './vnode'
|
||||
// Since @vue/shared is inlined into final builds,
|
||||
// when re-exporting from @vue/shared we need to avoid relying on their original
|
||||
// types so that the bundled d.ts does not attempt to import from it.
|
||||
import { capitalize as _capitalize, camelize as _camelize } from '@vue/shared'
|
||||
export const capitalize = _capitalize as (s: string) => string
|
||||
export const camelize = _camelize as (s: string) => string
|
||||
export { pushScopeId, popScopeId, withScopeId } from './helpers/scopeId'
|
||||
export {
|
||||
setBlockTracking,
|
||||
createTextVNode,
|
||||
createCommentVNode,
|
||||
createStaticVNode
|
||||
} from './vnode'
|
||||
export { toDisplayString, camelize } from '@vue/shared'
|
||||
|
||||
// For integration with runtime compiler
|
||||
export { registerRuntimeCompiler } from './component'
|
||||
|
||||
// For test-utils
|
||||
export { transformVNodeArgs } from './vnode'
|
||||
|
||||
// SSR -------------------------------------------------------------------------
|
||||
|
||||
import { createComponentInstance, setupComponent } from './component'
|
||||
import {
|
||||
renderComponentRoot,
|
||||
setCurrentRenderingInstance
|
||||
} from './componentRenderUtils'
|
||||
import { isVNode, normalizeVNode } from './vnode'
|
||||
import { normalizeSuspenseChildren } from './components/Suspense'
|
||||
|
||||
// SSR utils are only exposed in cjs builds.
|
||||
const _ssrUtils = {
|
||||
createComponentInstance,
|
||||
setupComponent,
|
||||
renderComponentRoot,
|
||||
setCurrentRenderingInstance,
|
||||
isVNode,
|
||||
normalizeVNode,
|
||||
normalizeSuspenseChildren
|
||||
}
|
||||
|
||||
export const ssrUtils = (__NODE_JS__ ? _ssrUtils : null) as typeof _ssrUtils
|
||||
|
||||
// Types -----------------------------------------------------------------------
|
||||
|
||||
export { App, AppConfig, AppContext, Plugin } from './apiApp'
|
||||
export { VNode, VNodeTypes, VNodeProps } from './vnode'
|
||||
export {
|
||||
ReactiveEffect,
|
||||
ReactiveEffectOptions,
|
||||
DebuggerEvent,
|
||||
TrackOpTypes,
|
||||
TriggerOpTypes,
|
||||
Ref,
|
||||
ComputedRef,
|
||||
UnwrapRef,
|
||||
WritableComputedOptions
|
||||
} from '@vue/reactivity'
|
||||
export {
|
||||
// types
|
||||
WatchEffect,
|
||||
BaseWatchOptions,
|
||||
WatchOptions,
|
||||
WatchCallback,
|
||||
WatchSource,
|
||||
StopHandle
|
||||
} from './apiWatch'
|
||||
export { InjectionKey } from './apiInject'
|
||||
export {
|
||||
App,
|
||||
AppConfig,
|
||||
AppContext,
|
||||
Plugin,
|
||||
CreateAppFunction,
|
||||
OptionMergeFunction
|
||||
} from './apiCreateApp'
|
||||
export {
|
||||
VNode,
|
||||
VNodeTypes,
|
||||
VNodeProps,
|
||||
VNodeArrayChildren,
|
||||
VNodeNormalizedChildren
|
||||
} from './vnode'
|
||||
export {
|
||||
Component,
|
||||
FunctionalComponent,
|
||||
@@ -109,10 +186,17 @@ export {
|
||||
ComponentOptionsWithoutProps,
|
||||
ComponentOptionsWithObjectProps as ComponentOptionsWithProps,
|
||||
ComponentOptionsWithArrayProps
|
||||
} from './apiOptions'
|
||||
|
||||
} from './componentOptions'
|
||||
export { ComponentPublicInstance } from './componentProxy'
|
||||
export { RendererOptions } from './renderer'
|
||||
export {
|
||||
Renderer,
|
||||
RendererNode,
|
||||
RendererElement,
|
||||
HydrationRenderer,
|
||||
RendererOptions,
|
||||
RootRenderFunction
|
||||
} from './renderer'
|
||||
export { RootHydrateFunction } from './hydration'
|
||||
export { Slot, Slots } from './componentSlots'
|
||||
export {
|
||||
Prop,
|
||||
@@ -129,3 +213,9 @@ export {
|
||||
DirectiveArguments
|
||||
} from './directives'
|
||||
export { SuspenseBoundary } from './components/Suspense'
|
||||
export { TransitionState, TransitionHooks } from './components/BaseTransition'
|
||||
export {
|
||||
AsyncComponentOptions,
|
||||
AsyncComponentLoader
|
||||
} from './apiAsyncComponent'
|
||||
export { HMRRuntime } from './hmr'
|
||||
|
||||
41
packages/runtime-core/src/profiling.ts
Normal file
41
packages/runtime-core/src/profiling.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ComponentInternalInstance, formatComponentName } from './component'
|
||||
|
||||
let supported: boolean
|
||||
let perf: any
|
||||
|
||||
export function startMeasure(
|
||||
instance: ComponentInternalInstance,
|
||||
type: string
|
||||
) {
|
||||
if (instance.appContext.config.performance && isSupported()) {
|
||||
perf.mark(`vue-${type}-${instance.uid}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function endMeasure(instance: ComponentInternalInstance, type: string) {
|
||||
if (instance.appContext.config.performance && isSupported()) {
|
||||
const startTag = `vue-${type}-${instance.uid}`
|
||||
const endTag = startTag + `:end`
|
||||
perf.mark(endTag)
|
||||
perf.measure(
|
||||
`<${formatComponentName(instance.type)}> ${type}`,
|
||||
startTag,
|
||||
endTag
|
||||
)
|
||||
perf.clearMarks(startTag)
|
||||
perf.clearMarks(endTag)
|
||||
}
|
||||
}
|
||||
|
||||
function isSupported() {
|
||||
if (supported !== undefined) {
|
||||
return supported
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.performance) {
|
||||
supported = true
|
||||
perf = window.performance
|
||||
} else {
|
||||
supported = false
|
||||
}
|
||||
return supported
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
||||
import { isArray } from '@vue/shared'
|
||||
|
||||
const queue: Function[] = []
|
||||
const queue: (Function | null)[] = []
|
||||
const postFlushCbs: Function[] = []
|
||||
const p = Promise.resolve()
|
||||
|
||||
@@ -22,6 +22,13 @@ export function queueJob(job: () => void) {
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidateJob(job: () => void) {
|
||||
const i = queue.indexOf(job)
|
||||
if (i > -1) {
|
||||
queue[i] = null
|
||||
}
|
||||
}
|
||||
|
||||
export function queuePostFlushCb(cb: Function | Function[]) {
|
||||
if (!isArray(cb)) {
|
||||
postFlushCbs.push(cb)
|
||||
@@ -63,7 +70,10 @@ function flushJobs(seen?: CountMap) {
|
||||
if (__DEV__) {
|
||||
seen = seen || new Map()
|
||||
}
|
||||
while ((job = queue.shift())) {
|
||||
while ((job = queue.shift()) !== undefined) {
|
||||
if (job === null) {
|
||||
continue
|
||||
}
|
||||
if (__DEV__) {
|
||||
checkRecursiveUpdates(seen!, job)
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export const enum ShapeFlags {
|
||||
ELEMENT = 1,
|
||||
FUNCTIONAL_COMPONENT = 1 << 1,
|
||||
STATEFUL_COMPONENT = 1 << 2,
|
||||
TEXT_CHILDREN = 1 << 3,
|
||||
ARRAY_CHILDREN = 1 << 4,
|
||||
SLOTS_CHILDREN = 1 << 5,
|
||||
SUSPENSE = 1 << 6,
|
||||
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 7,
|
||||
COMPONENT_KEPT_ALIVE = 1 << 8,
|
||||
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
|
||||
}
|
||||
|
||||
// For runtime consumption
|
||||
export const PublicShapeFlags = {
|
||||
ELEMENT: ShapeFlags.ELEMENT,
|
||||
FUNCTIONAL_COMPONENT: ShapeFlags.FUNCTIONAL_COMPONENT,
|
||||
STATEFUL_COMPONENT: ShapeFlags.STATEFUL_COMPONENT,
|
||||
TEXT_CHILDREN: ShapeFlags.TEXT_CHILDREN,
|
||||
ARRAY_CHILDREN: ShapeFlags.ARRAY_CHILDREN,
|
||||
SLOTS_CHILDREN: ShapeFlags.SLOTS_CHILDREN,
|
||||
SUSPENSE: ShapeFlags.SUSPENSE,
|
||||
COMPONENT_SHOULD_KEEP_ALIVE: ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE,
|
||||
COMPONENT_KEPT_ALIVE: ShapeFlags.COMPONENT_KEPT_ALIVE,
|
||||
COMPONENT: ShapeFlags.COMPONENT
|
||||
}
|
||||
@@ -4,23 +4,33 @@ import {
|
||||
isString,
|
||||
isObject,
|
||||
EMPTY_ARR,
|
||||
extend
|
||||
extend,
|
||||
normalizeClass,
|
||||
normalizeStyle,
|
||||
PatchFlags,
|
||||
ShapeFlags
|
||||
} from '@vue/shared'
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
Data,
|
||||
SetupProxySymbol,
|
||||
Component
|
||||
Component,
|
||||
ClassComponent
|
||||
} from './component'
|
||||
import { RawSlots } from './componentSlots'
|
||||
import { ShapeFlags } from './shapeFlags'
|
||||
import { isReactive, Ref } from '@vue/reactivity'
|
||||
import { AppContext } from './apiApp'
|
||||
import { SuspenseBoundary } from './components/Suspense'
|
||||
import { AppContext } from './apiCreateApp'
|
||||
import {
|
||||
SuspenseImpl,
|
||||
isSuspense,
|
||||
SuspenseBoundary
|
||||
} from './components/Suspense'
|
||||
import { DirectiveBinding } from './directives'
|
||||
import { SuspenseImpl } from './components/Suspense'
|
||||
import { TransitionHooks } from './components/BaseTransition'
|
||||
import { warn } from './warning'
|
||||
import { currentScopeId } from './helpers/scopeId'
|
||||
import { TeleportImpl, isTeleport } from './components/Teleport'
|
||||
import { currentRenderingInstance } from './componentRenderUtils'
|
||||
import { RendererNode, RendererElement } from './renderer'
|
||||
|
||||
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
|
||||
__isFragment: true
|
||||
@@ -28,78 +38,82 @@ export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
|
||||
$props: VNodeProps
|
||||
}
|
||||
}
|
||||
export const Portal = (Symbol(__DEV__ ? 'Portal' : undefined) as any) as {
|
||||
__isPortal: true
|
||||
new (): {
|
||||
$props: VNodeProps & { target: string | object }
|
||||
}
|
||||
}
|
||||
export const Text = Symbol(__DEV__ ? 'Text' : undefined)
|
||||
export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
|
||||
export const Static = Symbol(__DEV__ ? 'Static' : undefined)
|
||||
|
||||
export type VNodeTypes =
|
||||
| string
|
||||
| Component
|
||||
| typeof Fragment
|
||||
| typeof Portal
|
||||
| typeof Text
|
||||
| typeof Static
|
||||
| typeof Comment
|
||||
| typeof Fragment
|
||||
| typeof TeleportImpl
|
||||
| typeof SuspenseImpl
|
||||
|
||||
export type VNodeRef =
|
||||
| string
|
||||
| Ref
|
||||
| ((ref: object | null, refs: Record<string, any>) => void)
|
||||
|
||||
export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef]
|
||||
|
||||
type VNodeMountHook = (vnode: VNode) => void
|
||||
type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void
|
||||
export type VNodeHook =
|
||||
| VNodeMountHook
|
||||
| VNodeUpdateHook
|
||||
| VNodeMountHook[]
|
||||
| VNodeUpdateHook[]
|
||||
|
||||
export interface VNodeProps {
|
||||
[key: string]: any
|
||||
key?: string | number
|
||||
ref?: string | Ref | ((ref: object | null) => void)
|
||||
ref?: VNodeRef
|
||||
|
||||
// vnode hooks
|
||||
onVnodeBeforeMount?: (vnode: VNode) => void
|
||||
onVnodeMounted?: (vnode: VNode) => void
|
||||
onVnodeBeforeUpdate?: (vnode: VNode, oldVNode: VNode) => void
|
||||
onVnodeUpdated?: (vnode: VNode, oldVNode: VNode) => void
|
||||
onVnodeBeforeUnmount?: (vnode: VNode) => void
|
||||
onVnodeUnmounted?: (vnode: VNode) => void
|
||||
onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
|
||||
onVnodeMounted?: VNodeMountHook | VNodeMountHook[]
|
||||
onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[]
|
||||
onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[]
|
||||
onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[]
|
||||
onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[]
|
||||
}
|
||||
|
||||
type VNodeChildAtom<HostNode, HostElement> =
|
||||
| VNode<HostNode, HostElement>
|
||||
type VNodeChildAtom = VNode | string | number | boolean | null | void
|
||||
|
||||
export interface VNodeArrayChildren<
|
||||
HostNode = RendererNode,
|
||||
HostElement = RendererElement
|
||||
> extends Array<VNodeArrayChildren | VNodeChildAtom> {}
|
||||
|
||||
export type VNodeChild = VNodeChildAtom | VNodeArrayChildren
|
||||
|
||||
export type VNodeNormalizedChildren =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| void
|
||||
|
||||
export interface VNodeChildren<HostNode = any, HostElement = any>
|
||||
extends Array<
|
||||
| VNodeChildren<HostNode, HostElement>
|
||||
| VNodeChildAtom<HostNode, HostElement>
|
||||
> {}
|
||||
|
||||
export type VNodeChild<HostNode = any, HostElement = any> =
|
||||
| VNodeChildAtom<HostNode, HostElement>
|
||||
| VNodeChildren<HostNode, HostElement>
|
||||
|
||||
export type NormalizedChildren<HostNode = any, HostElement = any> =
|
||||
| string
|
||||
| VNodeChildren<HostNode, HostElement>
|
||||
| VNodeArrayChildren
|
||||
| RawSlots
|
||||
| null
|
||||
|
||||
export interface VNode<HostNode = any, HostElement = any> {
|
||||
export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
|
||||
_isVNode: true
|
||||
type: VNodeTypes
|
||||
props: VNodeProps | null
|
||||
key: string | number | null
|
||||
ref: string | Ref | ((ref: object | null) => void) | null
|
||||
children: NormalizedChildren<HostNode, HostElement>
|
||||
ref: VNodeNormalizedRef | null
|
||||
scopeId: string | null // SFC only
|
||||
children: VNodeNormalizedChildren
|
||||
component: ComponentInternalInstance | null
|
||||
suspense: SuspenseBoundary<HostNode, HostElement> | null
|
||||
suspense: SuspenseBoundary | null
|
||||
dirs: DirectiveBinding[] | null
|
||||
transition: TransitionHooks | null
|
||||
|
||||
// DOM
|
||||
el: HostNode | null
|
||||
anchor: HostNode | null // fragment anchor
|
||||
target: HostElement | null // portal target
|
||||
target: HostElement | null // teleport target
|
||||
targetAnchor: HostNode | null // teleport target anchor
|
||||
|
||||
// optimization only
|
||||
shapeFlag: number
|
||||
@@ -130,7 +144,7 @@ let currentBlock: VNode[] | null = null
|
||||
//
|
||||
// disableTracking is true when creating a fragment block, since a fragment
|
||||
// always diffs its children.
|
||||
export function openBlock(disableTracking?: boolean) {
|
||||
export function openBlock(disableTracking = false) {
|
||||
blockStack.push((currentBlock = disableTracking ? null : []))
|
||||
}
|
||||
|
||||
@@ -157,7 +171,7 @@ export function setBlockTracking(value: number) {
|
||||
// A block root keeps track of dynamic nodes within the block in the
|
||||
// `dynamicChildren` array.
|
||||
export function createBlock(
|
||||
type: VNodeTypes,
|
||||
type: VNodeTypes | ClassComponent,
|
||||
props?: { [key: string]: any } | null,
|
||||
children?: any,
|
||||
patchFlag?: number,
|
||||
@@ -174,7 +188,7 @@ export function createBlock(
|
||||
currentBlock = blockStack[blockStack.length - 1] || null
|
||||
// a block is always going to be patched, so track it as a child of its
|
||||
// parent block
|
||||
if (currentBlock !== null) {
|
||||
if (currentBlock) {
|
||||
currentBlock.push(vnode)
|
||||
}
|
||||
return vnode
|
||||
@@ -197,29 +211,65 @@ export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
|
||||
return n1.type === n2.type && n1.key === n2.key
|
||||
}
|
||||
|
||||
export function createVNode(
|
||||
type: VNodeTypes,
|
||||
let vnodeArgsTransformer:
|
||||
| ((
|
||||
args: Parameters<typeof _createVNode>,
|
||||
instance: ComponentInternalInstance | null
|
||||
) => Parameters<typeof _createVNode>)
|
||||
| undefined
|
||||
|
||||
// Internal API for registering an arguments transform for createVNode
|
||||
// used for creating stubs in the test-utils
|
||||
export function transformVNodeArgs(transformer?: typeof vnodeArgsTransformer) {
|
||||
vnodeArgsTransformer = transformer
|
||||
}
|
||||
|
||||
const createVNodeWithArgsTransform = (
|
||||
...args: Parameters<typeof _createVNode>
|
||||
): VNode => {
|
||||
return _createVNode(
|
||||
...(vnodeArgsTransformer
|
||||
? vnodeArgsTransformer(args, currentRenderingInstance)
|
||||
: args)
|
||||
)
|
||||
}
|
||||
|
||||
export const InternalObjectSymbol = Symbol()
|
||||
|
||||
export const createVNode = (__DEV__
|
||||
? createVNodeWithArgsTransform
|
||||
: _createVNode) as typeof _createVNode
|
||||
|
||||
function _createVNode(
|
||||
type: VNodeTypes | ClassComponent,
|
||||
props: (Data & VNodeProps) | null = null,
|
||||
children: unknown = null,
|
||||
patchFlag: number = 0,
|
||||
dynamicProps: string[] | null = null
|
||||
): VNode {
|
||||
if (__DEV__ && !type) {
|
||||
warn(`Invalid vnode type when creating vnode: ${type}.`)
|
||||
if (!type) {
|
||||
if (__DEV__) {
|
||||
warn(`Invalid vnode type when creating vnode: ${type}.`)
|
||||
}
|
||||
type = Comment
|
||||
}
|
||||
|
||||
// class component normalization.
|
||||
if (isFunction(type) && '__vccOpts' in type) {
|
||||
type = type.__vccOpts
|
||||
}
|
||||
|
||||
// class & style normalization.
|
||||
if (props !== null) {
|
||||
if (props) {
|
||||
// for reactive or proxy objects, we need to clone it to enable mutation.
|
||||
if (isReactive(props) || SetupProxySymbol in props) {
|
||||
if (isReactive(props) || InternalObjectSymbol in props) {
|
||||
props = extend({}, props)
|
||||
}
|
||||
let { class: klass, style } = props
|
||||
if (klass != null && !isString(klass)) {
|
||||
if (klass && !isString(klass)) {
|
||||
props.class = normalizeClass(klass)
|
||||
}
|
||||
if (style != null) {
|
||||
if (isObject(style)) {
|
||||
// reactive state objects need to be cloned since they are likely to be
|
||||
// mutated
|
||||
if (isReactive(style) && !isArray(style)) {
|
||||
@@ -232,20 +282,26 @@ export function createVNode(
|
||||
// encode the vnode type information into a bitmap
|
||||
const shapeFlag = isString(type)
|
||||
? ShapeFlags.ELEMENT
|
||||
: __FEATURE_SUSPENSE__ && (type as any).__isSuspense === true
|
||||
: __FEATURE_SUSPENSE__ && isSuspense(type)
|
||||
? ShapeFlags.SUSPENSE
|
||||
: isObject(type)
|
||||
? ShapeFlags.STATEFUL_COMPONENT
|
||||
: isFunction(type)
|
||||
? ShapeFlags.FUNCTIONAL_COMPONENT
|
||||
: 0
|
||||
: isTeleport(type)
|
||||
? ShapeFlags.TELEPORT
|
||||
: isObject(type)
|
||||
? ShapeFlags.STATEFUL_COMPONENT
|
||||
: isFunction(type)
|
||||
? ShapeFlags.FUNCTIONAL_COMPONENT
|
||||
: 0
|
||||
|
||||
const vnode: VNode = {
|
||||
_isVNode: true,
|
||||
type,
|
||||
props,
|
||||
key: (props !== null && props.key) || null,
|
||||
ref: (props !== null && props.ref) || null,
|
||||
key: props && props.key !== undefined ? props.key : null,
|
||||
ref:
|
||||
props && props.ref !== undefined
|
||||
? [currentRenderingInstance!, props.ref]
|
||||
: null,
|
||||
scopeId: currentScopeId,
|
||||
children: null,
|
||||
component: null,
|
||||
suspense: null,
|
||||
@@ -254,6 +310,7 @@ export function createVNode(
|
||||
el: null,
|
||||
anchor: null,
|
||||
target: null,
|
||||
targetAnchor: null,
|
||||
shapeFlag,
|
||||
patchFlag,
|
||||
dynamicProps,
|
||||
@@ -269,8 +326,12 @@ export function createVNode(
|
||||
// the next vnode so that it can be properly unmounted later.
|
||||
if (
|
||||
shouldTrack > 0 &&
|
||||
currentBlock !== null &&
|
||||
currentBlock &&
|
||||
// the EVENTS flag is only for hydration and if it is the only flag, the
|
||||
// vnode should not be considered dynamic due to handler caching.
|
||||
patchFlag !== PatchFlags.HYDRATE_EVENTS &&
|
||||
(patchFlag > 0 ||
|
||||
shapeFlag & ShapeFlags.SUSPENSE ||
|
||||
shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
|
||||
shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
|
||||
) {
|
||||
@@ -292,12 +353,14 @@ export function cloneVNode<T, U>(
|
||||
props: extraProps
|
||||
? vnode.props
|
||||
? mergeProps(vnode.props, extraProps)
|
||||
: extraProps
|
||||
: extend({}, extraProps)
|
||||
: vnode.props,
|
||||
key: vnode.key,
|
||||
ref: vnode.ref,
|
||||
scopeId: vnode.scopeId,
|
||||
children: vnode.children,
|
||||
target: vnode.target,
|
||||
targetAnchor: vnode.targetAnchor,
|
||||
shapeFlag: vnode.shapeFlag,
|
||||
patchFlag: vnode.patchFlag,
|
||||
dynamicProps: vnode.dynamicProps,
|
||||
@@ -321,6 +384,10 @@ export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
|
||||
return createVNode(Text, null, text, flag)
|
||||
}
|
||||
|
||||
export function createStaticVNode(content: string): VNode {
|
||||
return createVNode(Static, null, content)
|
||||
}
|
||||
|
||||
export function createCommentVNode(
|
||||
text: string = '',
|
||||
// when used as the v-else branch, the comment node must be created as a
|
||||
@@ -328,12 +395,12 @@ export function createCommentVNode(
|
||||
asBlock: boolean = false
|
||||
): VNode {
|
||||
return asBlock
|
||||
? createBlock(Comment, null, text)
|
||||
? (openBlock(), createBlock(Comment, null, text))
|
||||
: createVNode(Comment, null, text)
|
||||
}
|
||||
|
||||
export function normalizeVNode<T, U>(child: VNodeChild<T, U>): VNode<T, U> {
|
||||
if (child == null) {
|
||||
export function normalizeVNode(child: VNodeChild): VNode {
|
||||
if (child == null || typeof child === 'boolean') {
|
||||
// empty placeholder
|
||||
return createVNode(Comment)
|
||||
} else if (isArray(child)) {
|
||||
@@ -344,67 +411,56 @@ export function normalizeVNode<T, U>(child: VNodeChild<T, U>): VNode<T, U> {
|
||||
// always produce all-vnode children arrays
|
||||
return child.el === null ? child : cloneVNode(child)
|
||||
} else {
|
||||
// primitive types
|
||||
// strings and numbers
|
||||
return createVNode(Text, null, String(child))
|
||||
}
|
||||
}
|
||||
|
||||
// optimized normalization for template-compiled render fns
|
||||
export function cloneIfMounted(child: VNode): VNode {
|
||||
return child.el === null ? child : cloneVNode(child)
|
||||
}
|
||||
|
||||
export function normalizeChildren(vnode: VNode, children: unknown) {
|
||||
let type = 0
|
||||
const { shapeFlag } = vnode
|
||||
if (children == null) {
|
||||
children = null
|
||||
} else if (isArray(children)) {
|
||||
type = ShapeFlags.ARRAY_CHILDREN
|
||||
} else if (typeof children === 'object') {
|
||||
type = ShapeFlags.SLOTS_CHILDREN
|
||||
// Normalize slot to plain children
|
||||
if (
|
||||
(shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) &&
|
||||
(children as any).default
|
||||
) {
|
||||
normalizeChildren(vnode, (children as any).default())
|
||||
return
|
||||
} else {
|
||||
type = ShapeFlags.SLOTS_CHILDREN
|
||||
if (!(children as RawSlots)._ && !(InternalObjectSymbol in children!)) {
|
||||
// if slots are not normalized, attach context instance
|
||||
// (compiled / normalized slots already have context)
|
||||
;(children as RawSlots)._ctx = currentRenderingInstance
|
||||
}
|
||||
}
|
||||
} else if (isFunction(children)) {
|
||||
children = { default: children }
|
||||
children = { default: children, _ctx: currentRenderingInstance }
|
||||
type = ShapeFlags.SLOTS_CHILDREN
|
||||
} else {
|
||||
children = String(children)
|
||||
type = ShapeFlags.TEXT_CHILDREN
|
||||
// force teleport children to array so it can be moved around
|
||||
if (shapeFlag & ShapeFlags.TELEPORT) {
|
||||
type = ShapeFlags.ARRAY_CHILDREN
|
||||
children = [createTextVNode(children as string)]
|
||||
} else {
|
||||
type = ShapeFlags.TEXT_CHILDREN
|
||||
}
|
||||
}
|
||||
vnode.children = children as NormalizedChildren
|
||||
vnode.children = children as VNodeNormalizedChildren
|
||||
vnode.shapeFlag |= type
|
||||
}
|
||||
|
||||
function normalizeStyle(
|
||||
value: unknown
|
||||
): Record<string, string | number> | void {
|
||||
if (isArray(value)) {
|
||||
const res: Record<string, string | number> = {}
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const normalized = normalizeStyle(value[i])
|
||||
if (normalized) {
|
||||
for (const key in normalized) {
|
||||
res[key] = normalized[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
} else if (isObject(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeClass(value: unknown): string {
|
||||
let res = ''
|
||||
if (isString(value)) {
|
||||
res = value
|
||||
} else if (isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
res += normalizeClass(value[i]) + ' '
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
for (const name in value) {
|
||||
if (value[name]) {
|
||||
res += name + ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
return res.trim()
|
||||
}
|
||||
|
||||
const handlersRE = /^on|^vnode/
|
||||
|
||||
export function mergeProps(...args: (Data & VNodeProps)[]) {
|
||||
@@ -414,15 +470,20 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
|
||||
const toMerge = args[i]
|
||||
for (const key in toMerge) {
|
||||
if (key === 'class') {
|
||||
ret.class = normalizeClass([ret.class, toMerge.class])
|
||||
if (ret.class !== toMerge.class) {
|
||||
ret.class = normalizeClass([ret.class, toMerge.class])
|
||||
}
|
||||
} else if (key === 'style') {
|
||||
ret.style = normalizeStyle([ret.style, toMerge.style])
|
||||
} else if (handlersRE.test(key)) {
|
||||
// on*, vnode*
|
||||
const existing = ret[key]
|
||||
ret[key] = existing
|
||||
? [].concat(existing as any, toMerge[key] as any)
|
||||
: toMerge[key]
|
||||
const incoming = toMerge[key]
|
||||
if (existing !== incoming) {
|
||||
ret[key] = existing
|
||||
? [].concat(existing as any, toMerge[key] as any)
|
||||
: incoming
|
||||
}
|
||||
} else {
|
||||
ret[key] = toMerge[key]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { VNode } from './vnode'
|
||||
import { Data, ComponentInternalInstance, Component } from './component'
|
||||
import {
|
||||
Data,
|
||||
ComponentInternalInstance,
|
||||
Component,
|
||||
formatComponentName
|
||||
} from './component'
|
||||
import { isString, isFunction } from '@vue/shared'
|
||||
import { toRaw, isRef, pauseTracking, resumeTracking } from '@vue/reactivity'
|
||||
import { toRaw, isRef, pauseTracking, resetTracking } from '@vue/reactivity'
|
||||
import { callWithErrorHandling, ErrorCodes } from './errorHandling'
|
||||
|
||||
type ComponentVNode = VNode & {
|
||||
@@ -43,7 +48,10 @@ export function warn(msg: string, ...args: any[]) {
|
||||
msg + args.join(''),
|
||||
instance && instance.proxy,
|
||||
trace
|
||||
.map(({ vnode }) => `at <${formatComponentName(vnode)}>`)
|
||||
.map(
|
||||
({ vnode }) =>
|
||||
`at <${formatComponentName(vnode.type as Component)}>`
|
||||
)
|
||||
.join('\n'),
|
||||
trace
|
||||
]
|
||||
@@ -60,7 +68,7 @@ export function warn(msg: string, ...args: any[]) {
|
||||
console.warn(...warnArgs)
|
||||
}
|
||||
|
||||
resumeTracking()
|
||||
resetTracking()
|
||||
}
|
||||
|
||||
function getComponentTrace(): ComponentTraceStack {
|
||||
@@ -111,24 +119,6 @@ function formatTraceEntry({ vnode, recurseCount }: TraceEntry): any[] {
|
||||
: [open + close, rootLabel]
|
||||
}
|
||||
|
||||
const classifyRE = /(?:^|[-_])(\w)/g
|
||||
const classify = (str: string): string =>
|
||||
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
|
||||
|
||||
function formatComponentName(vnode: ComponentVNode, file?: string): string {
|
||||
const Component = vnode.type as Component
|
||||
let name = isFunction(Component)
|
||||
? Component.displayName || Component.name
|
||||
: Component.name
|
||||
if (!name && file) {
|
||||
const match = file.match(/([^/\\]+)\.vue$/)
|
||||
if (match) {
|
||||
name = match[1]
|
||||
}
|
||||
}
|
||||
return name ? classify(name) : 'Anonymous'
|
||||
}
|
||||
|
||||
function formatProps(props: Data): any[] {
|
||||
const res: any[] = []
|
||||
const keys = Object.keys(props)
|
||||
|
||||
Reference in New Issue
Block a user