]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(runtime-core): refactor props resolution
authorEvan You <yyx990803@gmail.com>
Mon, 6 Apr 2020 21:37:47 +0000 (17:37 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 6 Apr 2020 21:37:47 +0000 (17:37 -0400)
Improve performance in optimized mode + tests

14 files changed:
packages/runtime-core/__tests__/apiSetupContext.spec.ts
packages/runtime-core/__tests__/componentProps.spec.ts [new file with mode: 0644]
packages/runtime-core/__tests__/componentProxy.spec.ts
packages/runtime-core/src/apiAsyncComponent.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/components/Suspense.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts
packages/runtime-dom/__tests__/modules/class.spec.ts
packages/server-renderer/src/renderToString.ts

index 08f0235b94b6b3b57e6ef4aec075a4949fac0dba..3933c6f1129f0abfdbba1acd4ef53332b79676d6 100644 (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)
       }
diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts
new file mode 100644 (file)
index 0000000..4ee39db
--- /dev/null
@@ -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()
+  })
+})
index c2e54990521c9e8cf6c705ddeba29abb1a6fedf7..cbe18c2ca211d3451b188d7ab5b6f8a4b234916b 100644 (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)
index 20c3a48ff74e416530b96a5364c99813422e64c6..bc18e542f6cfbc59331bef2791e49bb8109ccf9c 100644 (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)
 }
index 43e5d547690410ebb246ec66229abd7af53827b4..96fe6884881030a7be5e13c7c0722b4703cf40e3 100644 (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
index f34bfafd41adac526945d045bce5fe480c4a8703..a4dcf1cb173e24113536ab3b43f858650b8b542b 100644 (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]
-      }
-    }
+function resolvePropValue(
+  options: NormalizedPropsOptions[0],
+  props: Data,
+  key: string,
+  value: unknown
+) {
+  let opt = options[key]
+  if (opt == null) {
+    return value
   }
-
-  // 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
+  const hasDefault = hasOwn(opt, 'default')
+  // default values
+  if (hasDefault && value === undefined) {
+    const defaultValue = opt.default
+    value = isFunction(defaultValue) ? defaultValue() : defaultValue
   }
-  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.`)
+  // 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 false
+  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)
index 9aabfc1ad07581d5f9e8322b70ca33f616134ebd..f30c300a3d6b6dfdfb8d3e6eb40acf0092d9bf48 100644 (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,
index 8fbcf9c324f2326d99f7707518c7f49865d03679..6df75f02c63f50bc8272a97b9738ef582156fe6d 100644 (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 ||
index f5934fa0b70571d2580bbb880bb84dbde466857f..0cd79eb5586537cde0b841c279570304f884570c 100644 (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__) {
index 03dc33bb8984712f2949428df751b83fa5c66e6f..311ba344f260a824b582ea14ee81117ec92469d1 100644 (file)
@@ -156,7 +156,8 @@ export function createHydrationFunctions(
               null,
               parentComponent,
               parentSuspense,
-              isSVGContainer(container)
+              isSVGContainer(container),
+              optimized
             )
           }
           // async component
index 11e2c76be2543749a706882fadfb1dfac78d7d37..14fab2869940fbb0007c041c14b5394b63639c76 100644 (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)
   }
 
index a05d9d22bc4615ec50ebf30c673cb7726bdbf3f8..a43ab687eb20f4eeadf5560bf43e81a1500d1c9b 100644 (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,
index 2874cbea561913240ade77faf569e1ba1666aa8c..2cb40951f10063f7c70d56018e775d54afcb8faf 100644 (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',
index 7f384dd8dc4b060d66e566cf38162ce9c651251e..26a5e706ca9d3cfd764bb95876300b9a3585a412 100644 (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 => {