refactor(runtime-core): refactor props resolution

Improve performance in optimized mode + tests
This commit is contained in:
Evan You 2020-04-06 17:37:47 -04:00
parent c28a9196b2
commit ec4a4c1e06
14 changed files with 440 additions and 196 deletions

View File

@ -120,7 +120,6 @@ describe('api: setup context', () => {
// puts everything received in attrs // puts everything received in attrs
// disable implicit fallthrough // disable implicit fallthrough
inheritAttrs: false, inheritAttrs: false,
props: {},
setup(props: any, { attrs }: any) { setup(props: any, { attrs }: any) {
return () => h('div', attrs) return () => h('div', attrs)
} }

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

@ -57,31 +57,6 @@ describe('component: proxy', () => {
expect(instance!.renderContext.foo).toBe(2) 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', () => { test('should not expose non-declared props', () => {
let instanceProxy: any let instanceProxy: any
const Comp = { const Comp = {
@ -110,7 +85,7 @@ describe('component: proxy', () => {
} }
render(h(Comp), nodeOps.createElement('div')) render(h(Comp), nodeOps.createElement('div'))
expect(instanceProxy.$data).toBe(instance!.data) 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.$attrs).toBe(instance!.attrs)
expect(instanceProxy.$slots).toBe(instance!.slots) expect(instanceProxy.$slots).toBe(instance!.slots)
expect(instanceProxy.$refs).toBe(instance!.refs) expect(instanceProxy.$refs).toBe(instance!.refs)

View File

@ -5,7 +5,7 @@ import {
ComponentInternalInstance, ComponentInternalInstance,
isInSSRComponentSetup isInSSRComponentSetup
} from './component' } from './component'
import { isFunction, isObject, EMPTY_OBJ, NO } from '@vue/shared' import { isFunction, isObject, NO } from '@vue/shared'
import { ComponentPublicInstance } from './componentProxy' import { ComponentPublicInstance } from './componentProxy'
import { createVNode } from './vnode' import { createVNode } from './vnode'
import { defineComponent } from './apiDefineComponent' import { defineComponent } from './apiDefineComponent'
@ -181,11 +181,7 @@ export function defineAsyncComponent<
function createInnerComp( function createInnerComp(
comp: Component, comp: Component,
{ props, slots }: ComponentInternalInstance { vnode: { props, children } }: ComponentInternalInstance
) { ) {
return createVNode( return createVNode(comp, props, children)
comp,
props === EMPTY_OBJ ? null : props,
slots === EMPTY_OBJ ? null : slots
)
} }

View File

@ -2,7 +2,6 @@ import { VNode, VNodeChild, isVNode } from './vnode'
import { import {
reactive, reactive,
ReactiveEffect, ReactiveEffect,
shallowReadonly,
pauseTracking, pauseTracking,
resetTracking resetTracking
} from '@vue/reactivity' } from '@vue/reactivity'
@ -15,7 +14,7 @@ import {
exposePropsOnDevProxyTarget, exposePropsOnDevProxyTarget,
exposeRenderContextOnDevProxyTarget exposeRenderContextOnDevProxyTarget
} from './componentProxy' } from './componentProxy'
import { ComponentPropsOptions, resolveProps } from './componentProps' import { ComponentPropsOptions, initProps } from './componentProps'
import { Slots, resolveSlots } from './componentSlots' import { Slots, resolveSlots } from './componentSlots'
import { warn } from './warning' import { warn } from './warning'
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { ErrorCodes, callWithErrorHandling } from './errorHandling'
@ -147,7 +146,6 @@ export interface ComponentInternalInstance {
// alternative proxy used only for runtime-compiled render functions using // alternative proxy used only for runtime-compiled render functions using
// `with` block // `with` block
withProxy: ComponentPublicInstance | null withProxy: ComponentPublicInstance | null
propsProxy: Data | null
setupContext: SetupContext | null setupContext: SetupContext | null
refs: Data refs: Data
emit: EmitFn emit: EmitFn
@ -208,7 +206,6 @@ export function createComponentInstance(
proxy: null, proxy: null,
proxyTarget: null!, // to be immediately set proxyTarget: null!, // to be immediately set
withProxy: null, withProxy: null,
propsProxy: null,
setupContext: null, setupContext: null,
effects: null, effects: null,
provides: parent ? parent.provides : Object.create(appContext.provides), provides: parent ? parent.provides : Object.create(appContext.provides),
@ -292,26 +289,24 @@ export let isInSSRComponentSetup = false
export function setupComponent( export function setupComponent(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
parentSuspense: SuspenseBoundary | null,
isSSR = false isSSR = false
) { ) {
isInSSRComponentSetup = isSSR isInSSRComponentSetup = isSSR
const { props, children, shapeFlag } = instance.vnode const { props, children, shapeFlag } = instance.vnode
resolveProps(instance, props) const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
initProps(instance, props, isStateful, isSSR)
resolveSlots(instance, children) resolveSlots(instance, children)
// setup stateful logic const setupResult = isStateful
let setupResult ? setupStatefulComponent(instance, isSSR)
if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { : undefined
setupResult = setupStatefulComponent(instance, parentSuspense, isSSR)
}
isInSSRComponentSetup = false isInSSRComponentSetup = false
return setupResult return setupResult
} }
function setupStatefulComponent( function setupStatefulComponent(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
parentSuspense: SuspenseBoundary | null,
isSSR: boolean isSSR: boolean
) { ) {
const Component = instance.type as ComponentOptions const Component = instance.type as ComponentOptions
@ -340,13 +335,7 @@ function setupStatefulComponent(
if (__DEV__) { if (__DEV__) {
exposePropsOnDevProxyTarget(instance) exposePropsOnDevProxyTarget(instance)
} }
// 2. create props proxy // 2. call setup()
// 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()
const { setup } = Component const { setup } = Component
if (setup) { if (setup) {
const setupContext = (instance.setupContext = const setupContext = (instance.setupContext =
@ -358,7 +347,7 @@ function setupStatefulComponent(
setup, setup,
instance, instance,
ErrorCodes.SETUP_FUNCTION, ErrorCodes.SETUP_FUNCTION,
[propsProxy, setupContext] [instance.props, setupContext]
) )
resetTracking() resetTracking()
currentInstance = null currentInstance = null

View File

@ -1,4 +1,4 @@
import { toRaw, lock, unlock } from '@vue/reactivity' import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity'
import { import {
EMPTY_OBJ, EMPTY_OBJ,
camelize, camelize,
@ -13,8 +13,7 @@ import {
PatchFlags, PatchFlags,
makeMap, makeMap,
isReservedProp, isReservedProp,
EMPTY_ARR, EMPTY_ARR
ShapeFlags
} from '@vue/shared' } from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { Data, ComponentInternalInstance } from './component' import { Data, ComponentInternalInstance } from './component'
@ -95,45 +94,117 @@ type NormalizedProp =
// and an array of prop keys that need value casting (booleans and defaults) // and an array of prop keys that need value casting (booleans and defaults)
type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]] type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
// resolve raw VNode data. export function initProps(
// - 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(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
rawProps: Data | null rawProps: Data | null,
isStateful: number, // result of bitwise flag comparison
isSSR = false
) { ) {
const _options = instance.type.props const props: Data = {}
const hasDeclaredProps = !!_options const attrs: Data = {}
if (!rawProps && !hasDeclaredProps) { setFullProps(instance, rawProps, props, attrs)
instance.props = instance.attrs = EMPTY_OBJ const options = instance.type.props
return // validation
if (__DEV__ && options && rawProps) {
validateProps(props, options)
} }
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)! if (isStateful) {
const emits = instance.type.emits // stateful
const props: Data = {} instance.props = isSSR ? props : shallowReadonly(props)
let attrs: Data | undefined = undefined } else {
if (!options) {
// update the instance propsProxy (passed to setup()) to trigger potential // functional w/ optional props, props === attrs
// changes instance.props = attrs
const propsProxy = instance.propsProxy } else {
const setProp = propsProxy // functional w/ declared props
? (key: string, val: unknown) => { instance.props = props
props[key] = val }
propsProxy[key] = val }
} instance.attrs = attrs
: (key: string, val: unknown) => { }
props[key] = val
}
export function updateProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
optimized: boolean
) {
// allow mutation of propsProxy (which is readonly by default) // allow mutation of propsProxy (which is readonly by default)
unlock() 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) { if (rawProps) {
for (const key in rawProps) { for (const key in rawProps) {
const value = rawProps[key] const value = rawProps[key]
@ -144,95 +215,58 @@ export function resolveProps(
// prop option names are camelized during normalization, so to support // prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key. // kebab -> camel conversion here we need to camelize the key.
let camelKey let camelKey
if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) { if (options && hasOwn(options, (camelKey = camelize(key)))) {
setProp(camelKey, value) props[camelKey] = value
} else if (!emits || !isEmitListener(emits, key)) { } else if (!emits || !isEmitListener(emits, key)) {
// Any non-declared (either as a prop or an emitted event) props are put // 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 // into a separate `attrs` object for spreading. Make sure to preserve
// original key casing // original key casing
;(attrs || (attrs = {}))[key] = value attrs[key] = value
} }
} }
} }
if (hasDeclaredProps) { if (needCastKeys) {
// set default values & cast booleans
for (let i = 0; i < needCastKeys.length; i++) { for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i] const key = needCastKeys[i]
let opt = options[key] props[key] = resolvePropValue(options!, props, key, props[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))
}
} }
} }
// 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) { function resolvePropValue(
if (key[0] !== '$') { options: NormalizedPropsOptions[0],
return true props: Data,
} else if (__DEV__) { key: string,
warn(`Invalid prop name: "${key}" is a reserved property.`) 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( export function normalizePropsOptions(
raw: ComponentPropsOptions | void raw: ComponentPropsOptions | undefined
): NormalizedPropsOptions { ): NormalizedPropsOptions | [] {
if (!raw) { if (!raw) {
return EMPTY_ARR as any return EMPTY_ARR as any
} }
@ -307,9 +341,23 @@ function getTypeIndex(
return -1 return -1
} }
type AssertionResult = { function validateProps(props: Data, rawOptions: ComponentPropsOptions) {
valid: boolean const rawValues = toRaw(props)
expectedType: string 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( function validateProp(
@ -354,6 +402,11 @@ const isSimpleType = /*#__PURE__*/ makeMap(
'String,Number,Boolean,Function,Symbol' 'String,Number,Boolean,Function,Symbol'
) )
type AssertionResult = {
valid: boolean
expectedType: string
}
function assertType(value: unknown, type: PropConstructor): AssertionResult { function assertType(value: unknown, type: PropConstructor): AssertionResult {
let valid let valid
const expectedType = getType(type) const expectedType = getType(type)

View File

@ -57,7 +57,7 @@ const publicPropertiesMap: Record<
$: i => i, $: i => i,
$el: i => i.vnode.el, $el: i => i.vnode.el,
$data: i => i.data, $data: i => i.data,
$props: i => i.propsProxy, $props: i => i.props,
$attrs: i => i.attrs, $attrs: i => i.attrs,
$slots: i => i.slots, $slots: i => i.slots,
$refs: i => i.refs, $refs: i => i.refs,
@ -87,7 +87,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
const { const {
renderContext, renderContext,
data, data,
propsProxy, props,
accessCache, accessCache,
type, type,
sink, sink,
@ -109,7 +109,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
case AccessTypes.CONTEXT: case AccessTypes.CONTEXT:
return renderContext[key] return renderContext[key]
case AccessTypes.PROPS: case AccessTypes.PROPS:
return propsProxy![key] return props![key]
// default: just fallthrough // default: just fallthrough
} }
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) { } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
@ -121,10 +121,10 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
} else if (type.props) { } else if (type.props) {
// only cache other properties when instance has declared (thus stable) // only cache other properties when instance has declared (thus stable)
// props // props
if (hasOwn(normalizePropsOptions(type.props)[0], key)) { if (hasOwn(normalizePropsOptions(type.props)[0]!, key)) {
accessCache![key] = AccessTypes.PROPS accessCache![key] = AccessTypes.PROPS
// return the value from propsProxy for ref unwrapping and readonly // return the value from propsProxy for ref unwrapping and readonly
return propsProxy![key] return props![key]
} else { } else {
accessCache![key] = AccessTypes.OTHER accessCache![key] = AccessTypes.OTHER
} }
@ -203,7 +203,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
accessCache![key] !== undefined || accessCache![key] !== undefined ||
(data !== EMPTY_OBJ && hasOwn(data, key)) || (data !== EMPTY_OBJ && hasOwn(data, key)) ||
hasOwn(renderContext, key) || hasOwn(renderContext, key) ||
(type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) || (type.props && hasOwn(normalizePropsOptions(type.props)[0]!, key)) ||
hasOwn(publicPropertiesMap, key) || hasOwn(publicPropertiesMap, key) ||
hasOwn(sink, key) || hasOwn(sink, key) ||
hasOwn(appContext.config.globalProperties, key) hasOwn(appContext.config.globalProperties, key)
@ -284,7 +284,7 @@ export function exposePropsOnDevProxyTarget(
type: { props: propsOptions } type: { props: propsOptions }
} = instance } = instance
if (propsOptions) { if (propsOptions) {
Object.keys(normalizePropsOptions(propsOptions)[0]).forEach(key => { Object.keys(normalizePropsOptions(propsOptions)[0]!).forEach(key => {
Object.defineProperty(proxyTarget, key, { Object.defineProperty(proxyTarget, key, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,

View File

@ -14,7 +14,7 @@ import {
isVNode isVNode
} from './vnode' } from './vnode'
import { handleError, ErrorCodes } from './errorHandling' 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' import { warn } from './warning'
// mark the current rendering instance for asset resolution (e.g. // mark the current rendering instance for asset resolution (e.g.
@ -94,7 +94,7 @@ export function renderComponentRoot(
if ( if (
Component.inheritAttrs !== false && Component.inheritAttrs !== false &&
fallthroughAttrs && fallthroughAttrs &&
fallthroughAttrs !== EMPTY_OBJ Object.keys(fallthroughAttrs).length
) { ) {
if ( if (
root.shapeFlag & ShapeFlags.ELEMENT || root.shapeFlag & ShapeFlags.ELEMENT ||

View File

@ -438,7 +438,8 @@ function createSuspenseBoundary(
// consider the comment placeholder case. // consider the comment placeholder case.
hydratedEl ? null : next(instance.subTree), hydratedEl ? null : next(instance.subTree),
suspense, suspense,
isSVG isSVG,
optimized
) )
updateHOCHostEl(instance, vnode.el) updateHOCHostEl(instance, vnode.el)
if (__DEV__) { if (__DEV__) {

View File

@ -156,7 +156,8 @@ export function createHydrationFunctions(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVGContainer(container) isSVGContainer(container),
optimized
) )
} }
// async component // async component

View File

@ -42,7 +42,7 @@ import {
invalidateJob invalidateJob
} from './scheduler' } from './scheduler'
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity' import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
import { resolveProps } from './componentProps' import { updateProps } from './componentProps'
import { resolveSlots } from './componentSlots' import { resolveSlots } from './componentSlots'
import { pushWarningContext, popWarningContext, warn } from './warning' import { pushWarningContext, popWarningContext, warn } from './warning'
import { ComponentPublicInstance } from './componentProxy' import { ComponentPublicInstance } from './componentProxy'
@ -226,7 +226,8 @@ export type MountComponentFn = (
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean isSVG: boolean,
optimized: boolean
) => void ) => void
type ProcessTextOrCommentFn = ( type ProcessTextOrCommentFn = (
@ -242,7 +243,8 @@ export type SetupRenderEffectFn = (
container: RendererElement, container: RendererElement,
anchor: RendererNode | null, anchor: RendererNode | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean isSVG: boolean,
optimized: boolean
) => void ) => void
export const enum MoveType { export const enum MoveType {
@ -961,7 +963,8 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG isSVG,
optimized
) )
} }
} else { } else {
@ -978,7 +981,7 @@ function baseCreateRenderer(
if (__DEV__) { if (__DEV__) {
pushWarningContext(n2) pushWarningContext(n2)
} }
updateComponentPreRender(instance, n2) updateComponentPreRender(instance, n2, optimized)
if (__DEV__) { if (__DEV__) {
popWarningContext() popWarningContext()
} }
@ -1006,7 +1009,8 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG isSVG,
optimized
) => { ) => {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode, initialVNode,
@ -1034,7 +1038,7 @@ function baseCreateRenderer(
if (__DEV__) { if (__DEV__) {
startMeasure(instance, `init`) startMeasure(instance, `init`)
} }
setupComponent(instance, parentSuspense) setupComponent(instance)
if (__DEV__) { if (__DEV__) {
endMeasure(instance, `init`) endMeasure(instance, `init`)
} }
@ -1063,7 +1067,8 @@ function baseCreateRenderer(
container, container,
anchor, anchor,
parentSuspense, parentSuspense,
isSVG isSVG,
optimized
) )
if (__DEV__) { if (__DEV__) {
@ -1078,7 +1083,8 @@ function baseCreateRenderer(
container, container,
anchor, anchor,
parentSuspense, parentSuspense,
isSVG isSVG,
optimized
) => { ) => {
// create reactive effect for rendering // create reactive effect for rendering
instance.update = effect(function componentEffect() { instance.update = effect(function componentEffect() {
@ -1162,7 +1168,7 @@ function baseCreateRenderer(
} }
if (next) { if (next) {
updateComponentPreRender(instance, next) updateComponentPreRender(instance, next, optimized)
} else { } else {
next = vnode next = vnode
} }
@ -1232,12 +1238,13 @@ function baseCreateRenderer(
const updateComponentPreRender = ( const updateComponentPreRender = (
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
nextVNode: VNode nextVNode: VNode,
optimized: boolean
) => { ) => {
nextVNode.component = instance nextVNode.component = instance
instance.vnode = nextVNode instance.vnode = nextVNode
instance.next = null instance.next = null
resolveProps(instance, nextVNode.props) updateProps(instance, nextVNode.props, optimized)
resolveSlots(instance, nextVNode.children) resolveSlots(instance, nextVNode.children)
} }

View File

@ -352,7 +352,7 @@ export function cloneVNode<T, U>(
props: extraProps props: extraProps
? vnode.props ? vnode.props
? mergeProps(vnode.props, extraProps) ? mergeProps(vnode.props, extraProps)
: extraProps : extend({}, extraProps)
: vnode.props, : vnode.props,
key: vnode.key, key: vnode.key,
ref: vnode.ref, ref: vnode.ref,

View File

@ -70,13 +70,11 @@ describe('class', () => {
const childClass: ClassItem = { value: 'd' } const childClass: ClassItem = { value: 'd' }
const child = { const child = {
props: {},
render: () => h('div', { class: ['c', childClass.value] }) render: () => h('div', { class: ['c', childClass.value] })
} }
const parentClass: ClassItem = { value: 'b' } const parentClass: ClassItem = { value: 'b' }
const parent = { const parent = {
props: {},
render: () => h(child, { class: ['a', parentClass.value] }) render: () => h(child, { class: ['a', parentClass.value] })
} }
@ -101,21 +99,18 @@ describe('class', () => {
test('class merge between multiple nested components sharing same element', () => { test('class merge between multiple nested components sharing same element', () => {
const component1 = defineComponent({ const component1 = defineComponent({
props: {},
render() { render() {
return this.$slots.default!()[0] return this.$slots.default!()[0]
} }
}) })
const component2 = defineComponent({ const component2 = defineComponent({
props: {},
render() { render() {
return this.$slots.default!()[0] return this.$slots.default!()[0]
} }
}) })
const component3 = defineComponent({ const component3 = defineComponent({
props: {},
render() { render() {
return h( return h(
'div', 'div',

View File

@ -145,11 +145,7 @@ function renderComponentVNode(
parentComponent: ComponentInternalInstance | null = null parentComponent: ComponentInternalInstance | null = null
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> { ): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
const instance = createComponentInstance(vnode, parentComponent, null) const instance = createComponentInstance(vnode, parentComponent, null)
const res = setupComponent( const res = setupComponent(instance, true /* isSSR */)
instance,
null /* parentSuspense (no need to track for SSR) */,
true /* isSSR */
)
if (isPromise(res)) { if (isPromise(res)) {
return res return res
.catch(err => { .catch(err => {