组件 props 和 attrs 的初始化

2025-10-29 11:35:33

上一节已经可以挂载组件,并在组件内部完成响应式更新。

但组件还需要接收外部传入的参数:

import { createApp, h } from '../dist/vue.esm.js'

const App = {
  props: {
    msg: String
  },
  setup(props, { attrs }) {
    console.log('props', props)
    console.log('attrs', attrs)
  },
  render() {
    return h('div', 'hello world')
  }
}

createApp(App, { msg: 'hello', count: 1 }).mount('#app')

传入了两个参数:

{
  msg: 'hello',
  count: 1
}

组件只声明了 msg,所以最终会被拆分成:

props: {
  msg: 'hello'
},
attrs: {
  count: 1
}
  • props:组件声明并接收的参数
  • attrs:没有被组件声明的参数

接下来就实现组件 props 和 attrs 的初始化。

处理 props 声明

声明 props 有两种写法:

const App = {
  props: ['msg']
}

或者:

const App = {
  props: {
    msg: String
  }
}

先把数组形式转换成对象形式。

组件 props 相关的逻辑放在 componentProps.ts 中:

// packages/runtime-core/src/componentProps.ts
export function normalizePropsOptions(props = {}) {
  if (isArray(props)) {
    return props.reduce((normalized, key) => {
      normalized[key] = {}
      return normalized
    }, {})
  }

  return props
}

数组形式的声明:

['msg', 'count']

会被转换成:

{
  msg: {},
  count: {}
}

保存 props 配置

在创建组件实例时,保存组件声明的 props 配置:

// packages/runtime-core/src/component.ts
import { normalizePropsOptions } from './componentProps'

export function createComponentInstance(vnode) {
  const { type } = vnode

  const instance = {
    type,
    vnode,
    render: null,
    propsOptions: normalizePropsOptions(type.props),
    setupState: null,
    props: {},
    attrs: {},
    subTree: null,
    isMounted: false
  }

  return instance
}

propsOptions 保存的是组件声明的参数名

区分 props 和 attrs

  • 如果参数名在 propsOptions 中,放入 props
  • 如果参数名不在 propsOptions 中,放入 attrs
import { reactive } from '@vue/reactivity'
// packages/runtime-core/src/componentProps.ts
import { hasOwn, isArray } from '@vue/shared'

function setFullProps(instance, rawProps, props, attrs) {
  const { propsOptions } = instance

  if (!rawProps) {
    return
  }

  for (const key in rawProps) {
    const value = rawProps[key]

    if (hasOwn(propsOptions, key)) {
      props[key] = value
    }
    else {
      attrs[key] = value
    }
  }
}

export function initProps(instance) {
  const rawProps = instance.vnode.props
  const props = {}
  const attrs = {}

  setFullProps(instance, rawProps, props, attrs)

  instance.props = reactive(props)
  instance.attrs = attrs
}

为组件更新时可能需要读取响应式的 props, 所以使用 reactive 包装。

传入 setup

现在需要在组件初始化时调用 initProps,并把 props 和 attrs 传给 setup。

// packages/runtime-core/src/component.ts
import { proxyRefs } from '@vue/reactivity'
import { initProps } from './componentProps'

function createSetupContext(instance) {
  return {
    attrs: instance.attrs
  }
}

export function setupComponent(instance) {
  const { type } = instance

  initProps(instance)

  const setupContext = createSetupContext(instance)

  if (isFunction(type.setup)) {
    const setupResult = type.setup(instance.props, setupContext)
    instance.setupState = proxyRefs(setupResult)
  }

  instance.render = type.render
}

这样,组件中就可以通过第二个参数访问 attrs:

小结

  1. normalizePropsOptions 将数组形式的 props 转换成对象形式
  2. 创建组件实例时保存 propsOptions
  3. initProps 遍历原始参数,将参数拆分成 props 和 attrs
  4. props 使用 reactive 包装,支持响应式读取
  5. setup 接收 props 和 setupContext
  6. setupContext.attrs 保存组件未声明的参数