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
// disable implicit fallthrough
inheritAttrs: false,
props: {},
setup(props: any, { attrs }: any) {
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)
})
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)

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { toRaw, lock, unlock } from '@vue/reactivity'
import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity'
import {
EMPTY_OBJ,
camelize,
@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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