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:
pikax
2020-04-08 21:21:04 +01:00
339 changed files with 26645 additions and 8965 deletions

View File

@@ -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>"`;

View 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('<!---->')
})
})

View File

@@ -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')
})
})

View File

@@ -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()
})
})

View File

@@ -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() {}
},

View File

@@ -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,

View File

@@ -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])
})
})

View File

@@ -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
},

View File

@@ -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()
})
})

View 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)
})
})

View 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()
})
})

View File

@@ -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()
})
})

View File

@@ -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
})
}

View File

@@ -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`)
})
})
})

View File

@@ -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')
})

View 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('')
})
})

View File

@@ -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)
})
})

View File

@@ -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(() => {})

View File

@@ -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>')
})
})
})

View 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>`
)
})
})

View File

@@ -1,5 +1,5 @@
import { toHandlers } from '../../src/helpers/toHandlers'
import { mockWarn } from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
describe('toHandlers', () => {
mockWarn()

View 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)
})
})

View 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()
})
})
})

View File

@@ -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()
})
})

View File

@@ -6,9 +6,9 @@ import {
NodeTypes,
TestElement,
serialize,
serializeInner,
mockWarn
serializeInner
} from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
mockWarn()

View File

@@ -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>')
})
})

View File

@@ -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

View File

@@ -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()
})
})

View File

@@ -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'])
})
})

View File

@@ -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>')
})
})
})

View 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'
})
)
})
})

View File

@@ -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"
}
}

View 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)
}

View 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
}

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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.`)

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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'
}

View 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)))
)
}

View File

@@ -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]
}
}
}

View File

@@ -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)

View File

@@ -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
})
})
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 {

View 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 }
}

View File

@@ -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
])
}
}
}

View File

@@ -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]

View File

@@ -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>,

View File

@@ -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
}

View File

@@ -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)) {

View File

@@ -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
)
)
}

View File

@@ -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

View 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
}

View File

@@ -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)
}

View 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
}
}

View 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.`)
}
}

View 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
}
}

View File

@@ -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(

View 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
}

View File

@@ -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'

View 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

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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]
}

View File

@@ -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)