refactor(runtime-core): refactor props resolution
Improve performance in optimized mode + tests
This commit is contained in:
parent
c28a9196b2
commit
ec4a4c1e06
@ -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)
|
||||
}
|
||||
|
232
packages/runtime-core/__tests__/componentProps.spec.ts
Normal file
232
packages/runtime-core/__tests__/componentProps.spec.ts
Normal file
@ -0,0 +1,232 @@
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
getCurrentInstance,
|
||||
render,
|
||||
h,
|
||||
nodeOps,
|
||||
FunctionalComponent,
|
||||
defineComponent,
|
||||
ref
|
||||
} from '@vue/runtime-test'
|
||||
import { render as domRender, nextTick } from 'vue'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
describe('component props', () => {
|
||||
mockWarn()
|
||||
|
||||
test('stateful', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
let proxy: any
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: ['foo'],
|
||||
render() {
|
||||
props = this.$props
|
||||
attrs = this.$attrs
|
||||
proxy = this
|
||||
}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp, { foo: 1, bar: 2 }), root)
|
||||
expect(proxy.foo).toBe(1)
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
|
||||
expect(proxy.foo).toBe(2)
|
||||
expect(props).toEqual({ foo: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render(h(Comp, { qux: 5 }), root)
|
||||
expect(proxy.foo).toBeUndefined()
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
test('stateful with setup', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: ['foo'],
|
||||
setup(_props, { attrs: _attrs }) {
|
||||
return () => {
|
||||
props = _props
|
||||
attrs = _attrs
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp, { foo: 1, bar: 2 }), root)
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
|
||||
expect(props).toEqual({ foo: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render(h(Comp, { qux: 5 }), root)
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
test('functional with declaration', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
|
||||
const Comp: FunctionalComponent = (_props, { attrs: _attrs }) => {
|
||||
props = _props
|
||||
attrs = _attrs
|
||||
}
|
||||
Comp.props = ['foo']
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp, { foo: 1, bar: 2 }), root)
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
|
||||
expect(props).toEqual({ foo: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render(h(Comp, { qux: 5 }), root)
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
test('functional without declaration', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
const Comp: FunctionalComponent = (_props, { attrs: _attrs }) => {
|
||||
props = _props
|
||||
attrs = _attrs
|
||||
}
|
||||
const root = nodeOps.createElement('div')
|
||||
|
||||
render(h(Comp, { foo: 1 }), root)
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ foo: 1 })
|
||||
expect(props).toBe(attrs)
|
||||
|
||||
render(h(Comp, { bar: 2 }), root)
|
||||
expect(props).toEqual({ bar: 2 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
expect(props).toBe(attrs)
|
||||
})
|
||||
|
||||
test('boolean casting', () => {
|
||||
let proxy: any
|
||||
const Comp = {
|
||||
props: {
|
||||
foo: Boolean,
|
||||
bar: Boolean,
|
||||
baz: Boolean,
|
||||
qux: Boolean
|
||||
},
|
||||
render() {
|
||||
proxy = this
|
||||
}
|
||||
}
|
||||
render(
|
||||
h(Comp, {
|
||||
// absent should cast to false
|
||||
bar: '', // empty string should cast to true
|
||||
baz: 'baz', // same string should cast to true
|
||||
qux: 'ok' // other values should be left in-tact (but raise warning)
|
||||
}),
|
||||
nodeOps.createElement('div')
|
||||
)
|
||||
|
||||
expect(proxy.foo).toBe(false)
|
||||
expect(proxy.bar).toBe(true)
|
||||
expect(proxy.baz).toBe(true)
|
||||
expect(proxy.qux).toBe('ok')
|
||||
expect('type check failed for prop "qux"').toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('default value', () => {
|
||||
let proxy: any
|
||||
const Comp = {
|
||||
props: {
|
||||
foo: {
|
||||
default: 1
|
||||
},
|
||||
bar: {
|
||||
default: () => ({ a: 1 })
|
||||
}
|
||||
},
|
||||
render() {
|
||||
proxy = this
|
||||
}
|
||||
}
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp, { foo: 2 }), root)
|
||||
expect(proxy.foo).toBe(2)
|
||||
expect(proxy.bar).toEqual({ a: 1 })
|
||||
|
||||
render(h(Comp, { foo: undefined, bar: { b: 2 } }), root)
|
||||
expect(proxy.foo).toBe(1)
|
||||
expect(proxy.bar).toEqual({ b: 2 })
|
||||
})
|
||||
|
||||
test('optimized props updates', async () => {
|
||||
const Child = defineComponent({
|
||||
props: ['foo'],
|
||||
template: `<div>{{ foo }}</div>`
|
||||
})
|
||||
|
||||
const foo = ref(1)
|
||||
const id = ref('a')
|
||||
|
||||
const Comp = defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
foo,
|
||||
id
|
||||
}
|
||||
},
|
||||
components: { Child },
|
||||
template: `<Child :foo="foo" :id="id"/>`
|
||||
})
|
||||
|
||||
// Note this one is using the main Vue render so it can compile template
|
||||
// on the fly
|
||||
const root = document.createElement('div')
|
||||
domRender(h(Comp), root)
|
||||
expect(root.innerHTML).toBe('<div id="a">1</div>')
|
||||
|
||||
foo.value++
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<div id="a">2</div>')
|
||||
|
||||
id.value = 'b'
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<div id="b">2</div>')
|
||||
})
|
||||
|
||||
test('warn props mutation', () => {
|
||||
let instance: ComponentInternalInstance
|
||||
let setupProps: any
|
||||
const Comp = {
|
||||
props: ['foo'],
|
||||
setup(props: any) {
|
||||
instance = getCurrentInstance()!
|
||||
setupProps = props
|
||||
return () => null
|
||||
}
|
||||
}
|
||||
render(h(Comp, { foo: 1 }), nodeOps.createElement('div'))
|
||||
expect(setupProps.foo).toBe(1)
|
||||
expect(instance!.props.foo).toBe(1)
|
||||
setupProps.foo = 2
|
||||
expect(`Set operation on key "foo" failed`).toHaveBeenWarned()
|
||||
expect(() => {
|
||||
;(instance!.proxy as any).foo = 2
|
||||
}).toThrow(TypeError)
|
||||
expect(`Attempting to mutate prop "foo"`).toHaveBeenWarned()
|
||||
})
|
||||
})
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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, NormalizedProp>, 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)
|
||||
|
@ -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<any> = {
|
||||
const {
|
||||
renderContext,
|
||||
data,
|
||||
propsProxy,
|
||||
props,
|
||||
accessCache,
|
||||
type,
|
||||
sink,
|
||||
@ -109,7 +109,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
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<any> = {
|
||||
} 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<any> = {
|
||||
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,
|
||||
|
@ -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 ||
|
||||
|
@ -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__) {
|
||||
|
@ -156,7 +156,8 @@ export function createHydrationFunctions(
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isSVGContainer(container)
|
||||
isSVGContainer(container),
|
||||
optimized
|
||||
)
|
||||
}
|
||||
// async component
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -352,7 +352,7 @@ export function cloneVNode<T, U>(
|
||||
props: extraProps
|
||||
? vnode.props
|
||||
? mergeProps(vnode.props, extraProps)
|
||||
: extraProps
|
||||
: extend({}, extraProps)
|
||||
: vnode.props,
|
||||
key: vnode.key,
|
||||
ref: vnode.ref,
|
||||
|
@ -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',
|
||||
|
@ -145,11 +145,7 @@ function renderComponentVNode(
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
||||
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 => {
|
||||
|
Loading…
Reference in New Issue
Block a user