diff --git a/packages/runtime-core/__tests__/apiSetupContext.spec.ts b/packages/runtime-core/__tests__/apiSetupContext.spec.ts
index 08f0235b9..3933c6f11 100644
--- a/packages/runtime-core/__tests__/apiSetupContext.spec.ts
+++ b/packages/runtime-core/__tests__/apiSetupContext.spec.ts
@@ -120,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)
}
diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts
new file mode 100644
index 000000000..4ee39db8d
--- /dev/null
+++ b/packages/runtime-core/__tests__/componentProps.spec.ts
@@ -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: `
{{ foo }}
`
+ })
+
+ const foo = ref(1)
+ const id = ref('a')
+
+ const Comp = defineComponent({
+ setup() {
+ return {
+ foo,
+ id
+ }
+ },
+ components: { Child },
+ template: ``
+ })
+
+ // 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('1
')
+
+ foo.value++
+ await nextTick()
+ expect(root.innerHTML).toBe('2
')
+
+ id.value = 'b'
+ await nextTick()
+ expect(root.innerHTML).toBe('2
')
+ })
+
+ 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()
+ })
+})
diff --git a/packages/runtime-core/__tests__/componentProxy.spec.ts b/packages/runtime-core/__tests__/componentProxy.spec.ts
index c2e549905..cbe18c2ca 100644
--- a/packages/runtime-core/__tests__/componentProxy.spec.ts
+++ b/packages/runtime-core/__tests__/componentProxy.spec.ts
@@ -57,31 +57,6 @@ describe('component: proxy', () => {
expect(instance!.renderContext.foo).toBe(2)
})
- test('propsProxy', () => {
- let instance: ComponentInternalInstance
- let instanceProxy: any
- const Comp = {
- props: {
- foo: {
- type: Number,
- default: 1
- }
- },
- setup() {
- return () => null
- },
- mounted() {
- instance = getCurrentInstance()!
- instanceProxy = this
- }
- }
- render(h(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()
- })
-
test('should not expose non-declared props', () => {
let instanceProxy: any
const Comp = {
@@ -110,7 +85,7 @@ describe('component: proxy', () => {
}
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)
diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts
index 20c3a48ff..bc18e542f 100644
--- a/packages/runtime-core/src/apiAsyncComponent.ts
+++ b/packages/runtime-core/src/apiAsyncComponent.ts
@@ -5,7 +5,7 @@ import {
ComponentInternalInstance,
isInSSRComponentSetup
} from './component'
-import { isFunction, isObject, EMPTY_OBJ, NO } from '@vue/shared'
+import { isFunction, isObject, NO } from '@vue/shared'
import { ComponentPublicInstance } from './componentProxy'
import { createVNode } from './vnode'
import { defineComponent } from './apiDefineComponent'
@@ -181,11 +181,7 @@ export function defineAsyncComponent<
function createInnerComp(
comp: Component,
- { props, slots }: ComponentInternalInstance
+ { vnode: { props, children } }: ComponentInternalInstance
) {
- return createVNode(
- comp,
- props === EMPTY_OBJ ? null : props,
- slots === EMPTY_OBJ ? null : slots
- )
+ return createVNode(comp, props, children)
}
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 43e5d5476..96fe68848 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -2,7 +2,6 @@ import { VNode, VNodeChild, isVNode } from './vnode'
import {
reactive,
ReactiveEffect,
- shallowReadonly,
pauseTracking,
resetTracking
} from '@vue/reactivity'
@@ -15,7 +14,7 @@ import {
exposePropsOnDevProxyTarget,
exposeRenderContextOnDevProxyTarget
} from './componentProxy'
-import { ComponentPropsOptions, resolveProps } from './componentProps'
+import { ComponentPropsOptions, initProps } from './componentProps'
import { Slots, resolveSlots } from './componentSlots'
import { warn } from './warning'
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
@@ -147,7 +146,6 @@ export interface ComponentInternalInstance {
// alternative proxy used only for runtime-compiled render functions using
// `with` block
withProxy: ComponentPublicInstance | null
- propsProxy: Data | null
setupContext: SetupContext | null
refs: Data
emit: EmitFn
@@ -208,7 +206,6 @@ export function createComponentInstance(
proxy: null,
proxyTarget: null!, // to be immediately set
withProxy: null,
- propsProxy: null,
setupContext: null,
effects: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
@@ -292,26 +289,24 @@ export let isInSSRComponentSetup = false
export function setupComponent(
instance: ComponentInternalInstance,
- parentSuspense: SuspenseBoundary | null,
isSSR = false
) {
isInSSRComponentSetup = isSSR
+
const { props, children, shapeFlag } = instance.vnode
- resolveProps(instance, props)
+ const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
+ initProps(instance, props, isStateful, isSSR)
resolveSlots(instance, children)
- // setup stateful logic
- let setupResult
- if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
- setupResult = setupStatefulComponent(instance, parentSuspense, isSSR)
- }
+ const setupResult = isStateful
+ ? setupStatefulComponent(instance, isSSR)
+ : undefined
isInSSRComponentSetup = false
return setupResult
}
function setupStatefulComponent(
instance: ComponentInternalInstance,
- parentSuspense: SuspenseBoundary | null,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
@@ -340,13 +335,7 @@ function setupStatefulComponent(
if (__DEV__) {
exposePropsOnDevProxyTarget(instance)
}
- // 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 = isSSR
- ? instance.props
- : shallowReadonly(instance.props))
- // 3. call setup()
+ // 2. call setup()
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
@@ -358,7 +347,7 @@ function setupStatefulComponent(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
- [propsProxy, setupContext]
+ [instance.props, setupContext]
)
resetTracking()
currentInstance = null
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index f34bfafd4..a4dcf1cb1 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -1,4 +1,4 @@
-import { toRaw, lock, unlock } from '@vue/reactivity'
+import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity'
import {
EMPTY_OBJ,
camelize,
@@ -13,8 +13,7 @@ import {
PatchFlags,
makeMap,
isReservedProp,
- EMPTY_ARR,
- ShapeFlags
+ EMPTY_ARR
} from '@vue/shared'
import { warn } from './warning'
import { Data, ComponentInternalInstance } from './component'
@@ -95,45 +94,117 @@ type NormalizedProp =
// and an array of prop keys that need value casting (booleans and defaults)
type NormalizedPropsOptions = [Record, string[]]
-// resolve raw VNode data.
-// - filter out reserved keys (key, ref)
-// - 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
+ rawProps: Data | null,
+ isStateful: number, // result of bitwise flag comparison
+ isSSR = false
) {
- const _options = instance.type.props
- const hasDeclaredProps = !!_options
- if (!rawProps && !hasDeclaredProps) {
- instance.props = instance.attrs = EMPTY_OBJ
- return
+ const props: Data = {}
+ const attrs: Data = {}
+ 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 emits = instance.type.emits
- const props: Data = {}
- let attrs: Data | undefined = undefined
-
- // 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()
+ 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 {
+ attrs[key] = value
+ }
+ }
+ }
+ } else {
+ // 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]
+ }
+ }
+ }
+
+ // lock readonly
+ lock()
+
+ if (__DEV__ && rawOptions && rawProps) {
+ validateProps(props, rawOptions)
+ }
+}
+
+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
+
if (rawProps) {
for (const key in rawProps) {
const value = rawProps[key]
@@ -144,95 +215,58 @@ export function resolveProps(
// prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key.
let camelKey
- if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) {
- setProp(camelKey, value)
+ 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 || (attrs = {}))[key] = value
+ attrs[key] = value
}
}
}
- if (hasDeclaredProps) {
- // set default values & cast booleans
+ if (needCastKeys) {
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
- let opt = options[key]
- if (opt == null) continue
- 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 (!hasOwn(props, key) && !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
- validateProp(key, props[key], opt, !hasOwn(props, key))
- }
+ props[key] = resolvePropValue(options!, props, key, props[key])
}
}
-
- // in case of dynamic props, check if we need to delete keys from
- // the props proxy
- const { patchFlag } = instance.vnode
- if (
- hasDeclaredProps &&
- propsProxy &&
- (patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)
- ) {
- const rawInitialProps = toRaw(propsProxy)
- for (const key in rawInitialProps) {
- if (!hasOwn(props, key)) {
- delete propsProxy[key]
- }
- }
- }
-
- // lock readonly
- lock()
-
- if (
- instance.vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT &&
- !hasDeclaredProps
- ) {
- // functional component with optional props: use attrs as props
- instance.props = attrs || EMPTY_OBJ
- } else {
- instance.props = props
- }
- instance.attrs = attrs || EMPTY_OBJ
}
-function validatePropName(key: string) {
- if (key[0] !== '$') {
- return true
- } else if (__DEV__) {
- warn(`Invalid prop name: "${key}" is a reserved property.`)
+function resolvePropValue(
+ options: NormalizedPropsOptions[0],
+ props: Data,
+ key: string,
+ value: unknown
+) {
+ let opt = options[key]
+ if (opt == null) {
+ return value
}
- return false
+ 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 | void
-): NormalizedPropsOptions {
+ raw: ComponentPropsOptions | undefined
+): NormalizedPropsOptions | [] {
if (!raw) {
return EMPTY_ARR as any
}
@@ -307,9 +341,23 @@ function getTypeIndex(
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(
@@ -354,6 +402,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)
diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts
index 9aabfc1ad..f30c300a3 100644
--- a/packages/runtime-core/src/componentProxy.ts
+++ b/packages/runtime-core/src/componentProxy.ts
@@ -57,7 +57,7 @@ const publicPropertiesMap: Record<
$: i => i,
$el: i => i.vnode.el,
$data: i => i.data,
- $props: i => i.propsProxy,
+ $props: i => i.props,
$attrs: i => i.attrs,
$slots: i => i.slots,
$refs: i => i.refs,
@@ -87,7 +87,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = {
const {
renderContext,
data,
- propsProxy,
+ props,
accessCache,
type,
sink,
@@ -109,7 +109,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = {
case AccessTypes.CONTEXT:
return renderContext[key]
case AccessTypes.PROPS:
- return propsProxy![key]
+ return props![key]
// default: just fallthrough
}
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
@@ -121,10 +121,10 @@ export const PublicInstanceProxyHandlers: ProxyHandler = {
} else if (type.props) {
// only cache other properties when instance has declared (thus stable)
// props
- if (hasOwn(normalizePropsOptions(type.props)[0], key)) {
+ if (hasOwn(normalizePropsOptions(type.props)[0]!, key)) {
accessCache![key] = AccessTypes.PROPS
// return the value from propsProxy for ref unwrapping and readonly
- return propsProxy![key]
+ return props![key]
} else {
accessCache![key] = AccessTypes.OTHER
}
@@ -203,7 +203,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = {
accessCache![key] !== undefined ||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
hasOwn(renderContext, key) ||
- (type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) ||
+ (type.props && hasOwn(normalizePropsOptions(type.props)[0]!, key)) ||
hasOwn(publicPropertiesMap, key) ||
hasOwn(sink, key) ||
hasOwn(appContext.config.globalProperties, key)
@@ -284,7 +284,7 @@ export function exposePropsOnDevProxyTarget(
type: { props: propsOptions }
} = instance
if (propsOptions) {
- Object.keys(normalizePropsOptions(propsOptions)[0]).forEach(key => {
+ Object.keys(normalizePropsOptions(propsOptions)[0]!).forEach(key => {
Object.defineProperty(proxyTarget, key, {
enumerable: true,
configurable: true,
diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts
index 8fbcf9c32..6df75f02c 100644
--- a/packages/runtime-core/src/componentRenderUtils.ts
+++ b/packages/runtime-core/src/componentRenderUtils.ts
@@ -14,7 +14,7 @@ import {
isVNode
} from './vnode'
import { handleError, ErrorCodes } from './errorHandling'
-import { PatchFlags, ShapeFlags, EMPTY_OBJ, isOn } from '@vue/shared'
+import { PatchFlags, ShapeFlags, isOn } from '@vue/shared'
import { warn } from './warning'
// mark the current rendering instance for asset resolution (e.g.
@@ -94,7 +94,7 @@ export function renderComponentRoot(
if (
Component.inheritAttrs !== false &&
fallthroughAttrs &&
- fallthroughAttrs !== EMPTY_OBJ
+ Object.keys(fallthroughAttrs).length
) {
if (
root.shapeFlag & ShapeFlags.ELEMENT ||
diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts
index f5934fa0b..0cd79eb55 100644
--- a/packages/runtime-core/src/components/Suspense.ts
+++ b/packages/runtime-core/src/components/Suspense.ts
@@ -438,7 +438,8 @@ function createSuspenseBoundary(
// consider the comment placeholder case.
hydratedEl ? null : next(instance.subTree),
suspense,
- isSVG
+ isSVG,
+ optimized
)
updateHOCHostEl(instance, vnode.el)
if (__DEV__) {
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index 03dc33bb8..311ba344f 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -156,7 +156,8 @@ export function createHydrationFunctions(
null,
parentComponent,
parentSuspense,
- isSVGContainer(container)
+ isSVGContainer(container),
+ optimized
)
}
// async component
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 11e2c76be..14fab2869 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -42,7 +42,7 @@ import {
invalidateJob
} from './scheduler'
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
-import { resolveProps } from './componentProps'
+import { updateProps } from './componentProps'
import { resolveSlots } from './componentSlots'
import { pushWarningContext, popWarningContext, warn } from './warning'
import { ComponentPublicInstance } from './componentProxy'
@@ -226,7 +226,8 @@ export type MountComponentFn = (
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean
+ isSVG: boolean,
+ optimized: boolean
) => void
type ProcessTextOrCommentFn = (
@@ -242,7 +243,8 @@ export type SetupRenderEffectFn = (
container: RendererElement,
anchor: RendererNode | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean
+ isSVG: boolean,
+ optimized: boolean
) => void
export const enum MoveType {
@@ -961,7 +963,8 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
- isSVG
+ isSVG,
+ optimized
)
}
} else {
@@ -978,7 +981,7 @@ function baseCreateRenderer(
if (__DEV__) {
pushWarningContext(n2)
}
- updateComponentPreRender(instance, n2)
+ updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
@@ -1006,7 +1009,8 @@ function baseCreateRenderer(
anchor,
parentComponent,
parentSuspense,
- isSVG
+ isSVG,
+ optimized
) => {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
@@ -1034,7 +1038,7 @@ function baseCreateRenderer(
if (__DEV__) {
startMeasure(instance, `init`)
}
- setupComponent(instance, parentSuspense)
+ setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
@@ -1063,7 +1067,8 @@ function baseCreateRenderer(
container,
anchor,
parentSuspense,
- isSVG
+ isSVG,
+ optimized
)
if (__DEV__) {
@@ -1078,7 +1083,8 @@ function baseCreateRenderer(
container,
anchor,
parentSuspense,
- isSVG
+ isSVG,
+ optimized
) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
@@ -1162,7 +1168,7 @@ function baseCreateRenderer(
}
if (next) {
- updateComponentPreRender(instance, next)
+ updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
@@ -1232,12 +1238,13 @@ function baseCreateRenderer(
const updateComponentPreRender = (
instance: ComponentInternalInstance,
- nextVNode: VNode
+ nextVNode: VNode,
+ optimized: boolean
) => {
nextVNode.component = instance
instance.vnode = nextVNode
instance.next = null
- resolveProps(instance, nextVNode.props)
+ updateProps(instance, nextVNode.props, optimized)
resolveSlots(instance, nextVNode.children)
}
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index a05d9d22b..a43ab687e 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -352,7 +352,7 @@ export function cloneVNode(
props: extraProps
? vnode.props
? mergeProps(vnode.props, extraProps)
- : extraProps
+ : extend({}, extraProps)
: vnode.props,
key: vnode.key,
ref: vnode.ref,
diff --git a/packages/runtime-dom/__tests__/modules/class.spec.ts b/packages/runtime-dom/__tests__/modules/class.spec.ts
index 2874cbea5..2cb40951f 100644
--- a/packages/runtime-dom/__tests__/modules/class.spec.ts
+++ b/packages/runtime-dom/__tests__/modules/class.spec.ts
@@ -70,13 +70,11 @@ describe('class', () => {
const childClass: ClassItem = { value: 'd' }
const child = {
- props: {},
render: () => h('div', { class: ['c', childClass.value] })
}
const parentClass: ClassItem = { value: 'b' }
const parent = {
- props: {},
render: () => h(child, { class: ['a', parentClass.value] })
}
@@ -101,21 +99,18 @@ describe('class', () => {
test('class merge between multiple nested components sharing same element', () => {
const component1 = defineComponent({
- props: {},
render() {
return this.$slots.default!()[0]
}
})
const component2 = defineComponent({
- props: {},
render() {
return this.$slots.default!()[0]
}
})
const component3 = defineComponent({
- props: {},
render() {
return h(
'div',
diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts
index 7f384dd8d..26a5e706c 100644
--- a/packages/server-renderer/src/renderToString.ts
+++ b/packages/server-renderer/src/renderToString.ts
@@ -145,11 +145,7 @@ function renderComponentVNode(
parentComponent: ComponentInternalInstance | null = null
): ResolvedSSRBuffer | Promise {
const instance = createComponentInstance(vnode, parentComponent, null)
- const res = setupComponent(
- instance,
- null /* parentSuspense (no need to track for SSR) */,
- true /* isSSR */
- )
+ const res = setupComponent(instance, true /* isSSR */)
if (isPromise(res)) {
return res
.catch(err => {