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