解释 Vue 3 源码中如何处理组件的 `props` 校验和默认值设置,以及其内部的类型检查逻辑。

各位靓仔靓女们,大家好! 欢迎来到今天的Vue 3源码解密小课堂。今天咱们就来聊聊Vue 3组件里那些“磨人的小妖精”——props,看看Vue是如何给它们验明正身,又如何给它们安排默认值的。准备好了吗?Let’s dive in!

一、Props:组件的“身份证”和“户口本”

在Vue的世界里,props就像组件的身份证和户口本,它定义了组件可以接收哪些数据,这些数据是什么类型,以及如果调用组件的人没给这些数据,组件该怎么办。

// 一个简单的例子
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>作者: {{ author }}</p>
  </div>
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  props: {
    title: {
      type: String,
      required: true
    },
    author: {
      type: String,
      default: '匿名'
    }
  },
  setup(props) {
    console.log(props.title); // 可以访问 title
    console.log(props.author); // 可以访问 author
    return {};
  }
});
</script>

在这个例子中,title是组件的“身份证”,必须提供,否则Vue会报错;author是组件的“户口本”,就算调用组件的人没给,Vue也会给它一个默认值“匿名”。

二、Vue 3 源码中的 Props 处理流程

Vue 3在组件初始化时,会对props进行一系列处理,主要包括以下几个步骤:

  1. 规范化 Props 定义: 将各种奇奇怪怪的props定义方式统一成标准格式。
  2. 创建 Props 代理:props代理到组件实例上,方便我们在setup函数中访问。
  3. 验证 Props: 检查传入的props是否符合定义,类型是否正确,是否缺少必填的props
  4. 设置默认值: 如果props有默认值,并且调用者没有提供,就使用默认值。

接下来,我们深入源码,看看这些步骤是如何实现的。

三、Props 规范化 (Normalization)

在Vue 3中,props的定义方式有很多种,比如:

  • 字符串数组:props: ['title', 'author']
  • 对象形式:props: { title: String, author: { type: String, default: '匿名' } }

为了方便后续处理,Vue需要将这些定义方式统一成标准的对象形式。这个过程主要由normalizeProps函数完成 (源码位置:packages/runtime-core/src/componentProps.ts)。

// 简化版
function normalizeProps(raw: ComponentOptions, app: AppContext, isRoot: boolean): NormalizedPropsOptions {
  const normalized: NormalizedPropsOptions = {};

  if (!raw) {
    return normalized;
  }

  const rawProps = raw.props;

  if (!rawProps) {
    return normalized;
  }

  if (isArray(rawProps)) {
    for (let i = 0; i < rawProps.length; i++) {
      const name = rawProps[i];
      if (isString(name)) {
        normalized[name] = {}; // 转换为对象形式
      } else {
        // ... 处理错误情况
      }
    }
  } else if (isObject(rawProps)) {
    for (const key in rawProps) {
      const rawProp = rawProps[key];
      const normalizedProp: NormalizedProp = (normalized[key] =
        isObject(rawProp) || isArray(rawProp) ? rawProp : { type: rawProp }); // 转换为对象形式
    }
  } else {
    // ... 处理错误情况
  }

  return normalized;
}

这个函数接收组件的props定义 (raw),并返回一个标准化的props对象 (normalized)。

  • 如果是字符串数组,就将每个字符串作为key,创建一个空对象作为value。
  • 如果是对象,就直接使用该对象,但如果value不是对象或数组,就将其包装成{ type: value }的形式。

四、创建 Props 代理 (Proxy)

为了方便在setup函数中访问props,Vue会将props代理到组件实例上。这个过程主要由useProps函数完成 (源码位置:packages/runtime-core/src/componentProps.ts)。

// 简化版
function useProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  isStateful: boolean,
  isSSR = false
) {
  const resolvedProps = shallowReactive({}); // 创建响应式对象,存储解析后的 props

  const props = instance.props = isStateful
    ? resolvedProps as RawProps // 有状态组件,props 是响应式的
    : resolvedProps as Readonly<RawProps>; // 函数式组件,props 是只读的

  // ... 处理 attrs 和 listeners

  return [props, hasPropsChanged];

  function hasPropsChanged(nextProps: Data, prevProps: Data | null): boolean {
    // ... 比较 props 是否发生变化
    return false; // 简化,始终返回 false
  }
}

这个函数做了几件事:

  1. 创建一个响应式对象resolvedProps,用于存储解析后的props
  2. resolvedProps赋值给组件实例的props属性。
  3. 返回props对象,以便在setup函数中使用。

五、Props 验证 (Validation)

Props验证是Vue确保组件接收到正确类型数据的关键环节。如果传入的props不符合定义,Vue会发出警告,帮助开发者及时发现问题。Props验证的核心逻辑在validateProp函数中 (源码位置:packages/runtime-core/src/componentProps.ts)。

// 简化版
const enum Type {
  String,
  Number,
  Boolean,
  Array,
  Object,
  Date,
  Function,
  Symbol
}

const isFunction = (val: any): val is Function => typeof val === 'function'
const isArray = Array.isArray
const isObject = (val: any): val is Record<any, any> =>
  val !== null && typeof val === 'object'

function validateProp(
  key: string,
  value: any,
  prop: PropOptions,
  instance: ComponentInternalInstance,
  isAbsent: boolean
) {
  const { type, required, validator } = prop
  // required!
  if (required && isAbsent) {
    warn(`Missing required prop: "${key}"`, instance)
    return
  }

  if (value == null && !prop.required) {
    return
  }

  const { valid, expectedType } = isValidType(value, type)

  if (!valid) {
    warn(
      getInvalidTypeMessage(key, value, expectedType),
      instance
    )
    return
  }

  if (validator) {
    if (!validator(value)) {
      warn(
        `Invalid prop: custom validator check failed for prop "${key}".`,
        instance
      )
    }
  }
}

function isValidType(value: any, type: PropType<any>): { valid: boolean; expectedType: string[] } {
  if (type === null) {
    return { valid: true, expectedType: ['any'] }
  }
  if (!isArray(type)) {
    type = [type]
  }
  const expectedTypes: string[] = []
  const valid = type.some(t => {
    if (t === String) {
      expectedTypes.push('String')
      return isString(value)
    } else if (t === Number) {
      expectedTypes.push('Number')
      return isNumber(value)
    } else if (t === Boolean) {
      expectedTypes.push('Boolean')
      return isBoolean(value)
    } else if (t === Array) {
      expectedTypes.push('Array')
      return isArray(value)
    } else if (t === Object) {
      expectedTypes.push('Object')
      return isObject(value)
    } else if (t === Date) {
      expectedTypes.push('Date')
      return value instanceof Date
    } else if (t === Function) {
      expectedTypes.push('Function')
      return isFunction(value)
    } else if (t === Symbol) {
      expectedTypes.push('Symbol')
      return typeof value === 'symbol'
    } else {
      if (value) {
        if (typeof t === 'function') {
          expectedTypes.push(t.name || 'Custom Type')
          return value instanceof t
        } else if (typeof t === 'object' && t !== null && typeof t.then === 'function') {
          // Promise
          expectedTypes.push('Promise')
          return typeof value.then === 'function'
        } else {
          expectedTypes.push(String(t))
          return false
        }
      }
      return false
    }
  })
  return { valid, expectedType: expectedTypes }
}

function isString(val: any): val is string {
  return typeof val === 'string'
}

function isNumber(val: any): val is number {
  return typeof val === 'number'
}

function isBoolean(val: any): val is boolean {
  return typeof val === 'boolean'
}

function getInvalidTypeMessage(
  key: string,
  value: any,
  expectedTypes: string[]
): string {
  let message =
    `Invalid prop: type check failed for prop "${key}".` +
    ` Expected ${expectedTypes.join(' | ')}, got ${toRawType(value)} `
  return message
}

function toRawType(value: any): string {
  return Object.prototype.toString.call(value).slice(8, -1)
}

这个函数接收以下参数:

  • key: prop 的名称。
  • value: 传入的 prop 值。
  • prop: prop 的定义对象。
  • instance: 组件实例。
  • isAbsent: 是否缺少该 prop。

验证逻辑如下:

  1. 检查是否缺少必填的props 如果requiredtrue,并且isAbsenttrue,则发出警告。
  2. 检查类型: 如果定义了type,则检查传入的value是否是指定的类型。 如果类型不匹配,则发出警告。
  3. 检查自定义验证器: 如果定义了validator,则调用该函数进行验证。 如果验证失败,则发出警告。

isValidType函数用于检查传入的value是否是指定的类型。 它支持以下类型:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

以及自定义构造函数 (例如,new MyClass())。

六、设置默认值 (Default Values)

如果props定义了默认值,并且调用者没有提供该props,Vue会使用默认值。这个过程在resolvePropValue函数中完成 (源码位置:packages/runtime-core/src/componentProps.ts)。

// 简化版
function resolvePropValue(
  options: NormalizedPropsOptions,
  props: Data,
  key: string,
  value: any,
  instance: ComponentInternalInstance,
  isAbsent: boolean
) {
  const opt = options[key]
  if (opt != null) {
    const hasDefault = hasOwn(opt, 'default')
    // default values
    if (hasDefault && value === undefined) {
      const defaultValue = opt.default
      value =
        typeof defaultValue === 'function' && getType(opt.type) !== 'Function'
          ? defaultValue.call(instance)
          : defaultValue
    }
  }
  return value
}

const hasOwn = (val: object, key: string | symbol): key is keyof typeof val =>
  Object.prototype.hasOwnProperty.call(val, key)

function getType(fn: PropType<any> | null): Function {
  const type = fn && (Array.isArray(fn) ? fn[0] : fn)
  return type
}

这个函数接收以下参数:

  • options: 标准化的props定义对象。
  • props: 传入的props对象。
  • key: prop 的名称。
  • value: 传入的 prop 值。
  • instance: 组件实例。
  • isAbsent: 是否缺少该 prop。

如果满足以下条件,则使用默认值:

  1. props定义中存在default属性。
  2. 传入的valueundefined (即,调用者没有提供该props)。

如果default是一个函数,并且type不是Function,则调用该函数,并将组件实例作为this上下文传递给该函数。 这是为了支持动态默认值。

七、类型检查逻辑 (Type Checking Logic)

Vue 3 的类型检查逻辑主要集中在 isValidType 函数中,它使用 typeofinstanceof 来判断传入的值是否符合定义的类型。

Prop Type Type Check
String typeof value === 'string'
Number typeof value === 'number'
Boolean typeof value === 'boolean'
Array Array.isArray(value)
Object typeof value === 'object' && value !== null
Date value instanceof Date
Function typeof value === 'function'
Symbol typeof value === 'symbol'
Class value instanceof Class

八、总结

Vue 3 对props的处理流程可以总结为以下几点:

  1. 规范化: 将各种props定义方式统一成标准的对象形式。
  2. 代理:props代理到组件实例上,方便访问。
  3. 验证: 检查类型,检查是否缺少必填的props,执行自定义验证器。
  4. 默认值: 如果调用者没有提供props,并且定义了默认值,则使用默认值。

通过这些步骤,Vue 3 确保了组件接收到正确类型的数据,并且提供了灵活的默认值设置机制,从而提高了代码的可靠性和可维护性。

好了,今天的Vue 3 props源码解密小课堂就到这里。希望大家有所收获,下次再见! 各位靓仔靓女们,记得点赞关注哦! 溜了溜了~

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注