vue3.x 编译宏(Macros)

0 阅读9分钟

Vue 3 的编译宏是专为 <script setup> 设计的编译时函数。它们由 Vue 编译器在构建阶段识别、处理,并最终转换为标准、高效的运行时代码。宏本身不会出现在最终的产物中,因此无需手动导入,旨在简化组件编写并提升性能。

宏提升机制:Vue 3 的 <script setup> 中的宏(如 defineProps)会被提升到模块作用域,因此无法访问 setup 作用域中声明的变量

defineModel

defineModel 是 Vue 3.4 版本引入的一个编译宏,专门用于在 <script setup> 中简化 v-model 双向绑定的实现。它让自定义组件上的 v-model 变得像普通表单输入一样直观。

注意事项

  1. 模型名称不能重复。
  2. 多次调用无参数的 defineModel报错。
  3. 如果手动写了 props: ['modelValue'] 或 emits: ['update:modelValue'],再使用 defineModel() 也会冲突。

【示例】默认模型名称,不带参数

const input = defineModel()
console.log(input)
 <input v-model="input" type="text" />

defineModel 返回一个可读写的 ref,直接绑定到模板中的表单元素或其他组件。

image.png

编译后,它会变成类似这样的运行时代码:

const input = _useModel(__props, "modelValue");

同时,编译器还会在组件选项中生成相应的 props 和 emits 声明。

props: {
    "modelValue": {},
    "modelModifiers": {}
},
emits: ["update:modelValue"],

image.png

export function defineModel<T, M extends PropertyKey = string, G = T, S = T>(
  options: ({ default: any } | { required: true }) &
    // type:属性类型
    // validator:验证函数
    PropOptions<T> &
    // get:自定义 getter 函数
    // set:自定义 setter 函数
    DefineModelOptions<T, G, S>,

  // T 模型值的类型
  // M 模型名称的类型,默认为字符串
  // G getter 返回值的类型,默认为 T
  // S 接受值的类型,默认为 T
): ModelRef<T, M, G, S>

【示例】多次调用无参数的 defineModel报错

<script setup>
const model1 = defineModel() // 模型名: modelValue
const model2 = defineModel() // ❌ 再次使用默认名称 modelValue,冲突
</script>

image.png

defineModel 会为每个模型生成一对 prop(modelName 和 modelNameModifiers)以及对应的 update:modelName 事件。模型名称在组件内必须唯一,不能重复。

示例 指定自定义模型名称

子组件

<template>
  <div>
    <input type="text" name="age" v-model="age" />
    <input type="text" name="dec" v-model="dec" />
  </div>
</template>
<script setup lang="ts">
const age = defineModel("age");
const dec = defineModel("dec");
</script>

image.png

示例 带类型参数

<template>
  <div>
    <input type="text" name="age" v-model="age" />
    <input type="text" name="dec" v-model="dec" />
  </div>
</template>
<script setup lang="ts">
const age = defineModel<number>("age");
const dec = defineModel<string>("dec", {
  required: true,
});
</script>

image.png

示例 带选项的

<template>
  <div>
    <input type="text" name="age" v-model="age" />
    <input type="text" name="dec" v-model="dec" />
  </div>
</template>
<script setup lang="ts">
const age = defineModel<number>("age", {
  default: 20,
});
const dec = defineModel<string>("dec", {
  required: true,
});
</script>

image.png

示例 带选项并处理修饰符

<template>
  <div>
    <input type="text" name="age" v-model.number="age" />
    <input type="text" name="dec" v-model="dec" />
  </div>
</template>
<script setup lang="ts">
const age = defineModel<number>("age", {
  default: 20,
});
const [dec, modifierDec] = defineModel<string, string>("dec", {
  required: true,
  get(value: string): string {
    console.log(value, "get-xxx", modifierDec);
    return modifierDec?.upper ? value.toUpperCase() : value;
  },
  set(value: string) {
    console.log(value, "xxx");

    return modifierDec?.upper ? value.toUpperCase() : value;
  },
});

console.log(dec, "xxx", modifierDec);

const reset = () => {
  age.value = 20;
  dec.value = "DESC";
};

defineExpose({
  reset,
});
</script>

image.png

// 带名称参数版本
export function defineModel<T, M extends PropertyKey = string, G = T, S = T>(
  // 模型的名称,用于指定 v-model 的绑定名称
  name: string,
  options: ({ default: any } | { required: true }) &
    PropOptions<T> &
    DefineModelOptions<T, G, S>,
): ModelRef<T, M, G, S>
// ModelRef 是一个交叉类型(&)
export type ModelRef<T, M extends PropertyKey = string, G = T, S = T> = Ref<
  G, // G 表示 getter 返回值的类型
  S // 表示 setter 接受值的类型
> &
  // 一个元组类型,包含两个元素
  [ModelRef<T, M, G, S>, Record<M, true | undefined>]

源码

function processDefineModel(
  ctx: ScriptCompileContext,
  node: Node,
  declId?: LVal,
): boolean {
  if (!isCallOf(node, DEFINE_MODEL)) {
    return false
  }

  ctx.hasDefineModelCall = true

  // 提取 defineModel 的类型参数
  const type =
    (node.typeParameters && node.typeParameters.params[0]) || undefined

  let modelName: string
  let options: Node | undefined
  const arg0 = node.arguments[0] && unwrapTSNode(node.arguments[0])
  const hasName = arg0 && arg0.type === 'StringLiteral'
  // 如果第一个参数是字符串字面量,则将其作为模型名称,第二个参数作为选项
  if (hasName) {
    modelName = arg0.value
    options = node.arguments[1]
  } else {
    // 使用默认名称 modelValue,第一个参数作为选项
    modelName = 'modelValue'
    options = arg0
  }

  // 确保模型名称不重复,如果重复则报错
  if (ctx.modelDecls[modelName]) {
    ctx.error(`duplicate model name ${JSON.stringify(modelName)}`, node)
  }

  let optionsString = options && ctx.getString(options)
  let optionsRemoved = !options
  const runtimeOptionNodes: Node[] = []

  // 处理选项对象
  // 如果存在 options 且是一个对象表达式(ObjectExpression),且没有扩展运算符或计算属性
  if (
    options &&
    options.type === 'ObjectExpression' &&
    !options.properties.some(p => p.type === 'SpreadElement' || p.computed)
  ) {
    let removed = 0

    // 遍历选项对象的属性
    for (let i = options.properties.length - 1; i >= 0; i--) {
      const p = options.properties[i]
      const next = options.properties[i + 1]
      const start = p.start!
      const end = next ? next.start! : options.end! - 1

      if (
        (p.type === 'ObjectProperty' || p.type === 'ObjectMethod') &&
        ((p.key.type === 'Identifier' &&
          (p.key.name === 'get' || p.key.name === 'set')) ||
          (p.key.type === 'StringLiteral' &&
            (p.key.value === 'get' || p.key.value === 'set')))
      ) {
        // remove runtime-only options from prop options to avoid duplicates
        // 对于 get/set:保留在 optionsString 中
        optionsString =
          optionsString.slice(0, start - options.start!) +
          optionsString.slice(end - options.start!)
      } else {
        // remove prop options from runtime options
        // 对于其他属性(type、default、required 等):这些属于 prop 验证选项,会通过 ctx.s.remove() 从源码中删除,并记录到 runtimeOptionNodes 数组
        removed++
        ctx.s.remove(ctx.startOffset! + start, ctx.startOffset! + end)
        // record prop options for invalid scope var reference check
        runtimeOptionNodes.push(p)
      }
    }

    // 如果所有属性都被移除,则整个选项对象也会被删除
    if (removed === options.properties.length) {
      optionsRemoved = true
      ctx.s.remove(
        ctx.startOffset! + (hasName ? arg0.end! : options.start!),
        ctx.startOffset! + options.end!,
      )
    }
  }

  // 将模型声明信息存储到编译上下文中
  ctx.modelDecls[modelName] = {
    type,
    options: optionsString,
    runtimeOptionNodes,
    identifier:
      declId && declId.type === 'Identifier' ? declId.name : undefined,
  }
  // register binding type
  ctx.bindingMetadata[modelName] = BindingTypes.PROPS

  // defineModel -> useModel
  // 将 defineModel 标识符替换为 useModel 辅助函数
  ctx.s.overwrite(
    ctx.startOffset! + node.callee.start!,
    ctx.startOffset! + node.callee.end!,
    ctx.helper('useModel'),
  )
  // inject arguments
  // 注入额外参数:__props 和模型名称
  ctx.s.appendLeft(
    ctx.startOffset! +
      (node.arguments.length ? node.arguments[0].start! : node.end! - 1),
    `__props, ` +
      (hasName
        ? ``
        : `${JSON.stringify(modelName)}${optionsRemoved ? `` : `, `}`),
  )

  return true
}

defineExpose 暴露组件内部成员

defineExpose 是 Vue 3 在 <script setup> 中提供的一个编译宏,它的作用是显式暴露组件的公共属性或方法,以便父组件通过模板引用(ref)来访问这些暴露的内容。

为什么需要 defineExpose

  1. 在 <script setup> 中,组件内部的所有变量、函数默认都是私有的,父组件无法通过模板引用直接访问它们。这是为了保持组件的封装性,避免外部随意操作内部状态。
  2. 但在某些场景下,父组件确实需要调用子组件的某些方法(例如重置表单、聚焦输入框)或读取特定属性。defineExpose 正是为此设计的“逃生舱”。

注意事项

  1. defineExpose 在一个组件只能调用一次。

image.png

示例 defineExpose暴露子组件的方法和变量

子组件

<template>
  <div>
    <input type="text" name="age" v-model.number="age" />
    <input type="text" name="dec" v-model="dec" />
  </div>
</template>
<script setup lang="ts">
const age = defineModel<number>("age", {
  default: 20,
});
const dec = defineModel<string, string>("dec", {
  required: true,
});

const reset = () => {
  age.value = 20;
  dec.value = "DESC";
};

defineExpose({
  reset,
});
</script>

image.png

父组件

<template>
  <p>{{ "这里是云平台首页" }}</p>
  <tabTwo ref="tabTwoRef" v-model:age="age" v-model:dec="dec"> </tabTwo>
  <button @click="reset">重置</button>
</template>
<script setup lang="ts">
import tabTwo from "@/pages/cloud/components/tabTwo.vue";
import { ref } from "vue";
const tabTwoRef = ref<typeof tabTwo>();
const age = ref();
const dec = ref("desc");

const reset = () => {
  tabTwoRef.value?.reset();
};
defineOptions({
  name: "CloudIndexView",
});
</script>

使用场景?

当父组件需要直接调用子组件内部的方法或访问其内部状态时。在 Vue 3 的 <script setup> 语法下,组件默认是封闭的,不暴露任何内部成员,因此必须通过 defineExpose 显式“开门”。

  1. 表单组件:暴露验证、重置、获取数据方法。
  2. 包装第三方 UI 库:保留底层实例方法。当封装一个第三方 UI 库组件(如 Element Plus 的弹窗)时,通常需要向上层暴露原组件的常用方法(如 open、close)。
  3. 复杂组件内部的工具方法暴露。
  4. 开发组件库:为使用者提供实例 API。当开发一个公共组件库时,defineExpose 是定义组件实例方法的推荐方式。使用者通过 ref 获得组件实例后,能够调用组件设计的 API,而不需要知道组件内部实现细节。

不建议使用 defineExpose的场景?

  • 数据流能通过 props/events 解决:优先使用单向数据流 + 事件通信,避免直接操作子组件带来的耦合。
  • 跨层级通信:不要通过多层 ref 传递,应使用 provide/inject 或全局状态管理(Pinia)。
  • 简单展示组件:没有外部调用需求的组件,无需暴露任何内容。

源码

image.png

function processDefineExpose(
  ctx: ScriptCompileContext,
  node: Node,
): boolean {
  if (isCallOf(node, DEFINE_EXPOSE)) {
    // 确保组件中只调用一次 defineExpose,如果已经调用过则报错。
    if (ctx.hasDefineExposeCall) {
      ctx.error(`duplicate ${DEFINE_EXPOSE}() call`, node)
    }
    ctx.hasDefineExposeCall = true
    return true
  }
  return false
}

defineOptions

defineOptions 是 Vue 3.3 版本引入的一个编译宏,专门用于在 <script setup> 中直接声明组件选项(如 nameinheritAttrscomponentsdirectives 等),而无需再额外写一个普通的 <script> 块。

注意事项

  1. defineOptions 在组件内只能调用一次。
  2. defineOptions 不能有类型参数。
  3. defineOptions 不能用于声明 props、emits、expose、slots 选项。
  4. defineOptions 不支持返回。
  5. 不推荐利用 defineOptions注册组件,因为在 Vue 3 的 <script setup> 语法中,通常情况下,从其他文件导入的组件无需在 components 选项中注册即可直接在模板中使用。
  6. 配置值必须是静态的常量。

【示例 1 】defineOptions 只接收一个参数,参数为对象。

defineOptions({
  name: "CloudView",
  hello: "hello vue3",
});

image.png

【示例 2 】defineOptions 不支持类型参数

defineOptions<{
  name: string; // ❌ 不能有类型参数
}>({
  name: "CloudView",
});

image.png

【示例 3 】defineOptions 没有返回值,不支持返回。

// ❌ 不支持返回
const options = defineOptions({
  name: "CloudView",
});
[@vue/compiler-sfc] defineOptions() has no returning value, it cannot be assigned.

image.png

【示例 4 】defineProps 、defineEmits、defineProps 参数选项使用非字面量会编译错误

<script setup lang="ts">
import { ref as myRef } from "vue";
const count = myRef(0);
console.log(count.value);

defineOptions({
  name: "CloudView",
  hello: count, // ❌本地引用,报错
});
</script>

image.png

示例 声明组件名称 name

在 Vue 3.3+ 的 <script setup> 语法中,defineOptions 是一个编译器宏,允许你直接在 <script setup> 内部设置组件的选项,其中 name 就是用来指定组件名称的。

在 <script setup> 中,组件默认会从文件名推断名称,但有时需要一个明确的、与文件名不同的名称(例如用于递归组件、DevTools 调试、keep-alive 的 include/exclude 等)。

<script setup>
defineOptions({
  name: "CloudIndexView"  // 显式设置组件名称
})
</script>

image.png

示例 控制属性透传 inheritAttrs

在 Vue 3 中,defineOptions 的 inheritAttrs 选项用于控制未被 props 声明的属性(如 classstyledata-*aria-* 等)是否自动透传到组件的根元素上。

默认行为 inheritAttrs 为 true

  • 父组件传入的任何未在 props 中声明的属性,会自动添加到组件的根元素上。
  • 如果组件有多个根节点,自动透传会失效(因为不知道该放到哪个根上),并发出警告。

【示例】父组件传入的任何未在 props 中声明的属性,会自动添加到组件的根元素

image.png

父组件

<template>
  <p>{{ "这里是云平台首页" }}</p>
  <TabTwo data-id="123" class="parent-class"></TabTwo>
</template>
<script setup lang="ts">
import TabTwo from "@/pages/cloud/components/tabTwo.vue";
defineOptions({
  name: "CloudIndexView",
});
</script>
import TabTwo from "/src/pages/cloud/components/tabTwo.vue?t=1776923709274";
const _sfc_main = /* @__PURE__ */ _defineComponent({
	...{ name: "CloudIndexView" },
	__name: "index",
	setup(__props, { expose: __expose }) {
		__expose();
		const __returned__ = { TabTwo };
		Object.defineProperty(__returned__, "__isScriptSetup", {
			enumerable: false,
			value: true
		});
		return __returned__;
	}
});

子组件

<template>
  <div class="wrapper">
    <input name="age" placeholder="请输入" data-test="field" />
  </div>
</template>
<script setup lang="ts"></script>

image.png

【示例】子组件如果组件有多个根节点,自动透传会失效透传失败。

image.png

image.png

子组件

<template>
  <div class="wrapper">
    <input name="age" placeholder="请输入" data-test="field" />
  </div>
  <div>xx</div>
</template>
<script setup lang="ts"></script>

【示例】禁止自动透传 inheritAttrs为 false

image.png

子组件

<template>
  <div class="wrapper">
    <input name="age" placeholder="请输入" data-test="field" />
  </div>
</template>
<script setup lang="ts">
defineOptions({
  inheritAttrs: false,
});
</script>
const _sfc_main = /* @__PURE__ */ _defineComponent({
	...{ inheritAttrs: false },
	__name: "tabTwo",
	setup(__props, { expose: __expose }) {
		__expose();
		const __returned__ = {};
		Object.defineProperty(__returned__, "__isScriptSetup", {
			enumerable: false,
			value: true
		});
		return __returned__;
	}
});

示例 声明局部指令 directives

【示例】声明局部指令

<template>
  <p>{{ "这里是云平台首页" }}</p>
  <input v-foucs name="age" type="text" placeholder="please input" />
</template>
<script setup lang="ts">
import vFoucs from "@/directives/vFoucs";
defineOptions({
  name: "CloudIndexView",
  directives: {
    vFoucs,
  },
});
</script>

vue3-vite-cube/src/directives/vFoucs.ts

const vFoucs = {
  mounted: (el: HTMLInputElement) => {
    el.focus();
    console.log("vFouce mounted", el);
  },
};

export default vFoucs;
import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/pages/cloud/index.vue");import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
import vFoucs from "/src/directives/vFoucs.ts";
const _sfc_main = /* @__PURE__ */ _defineComponent({
	...{
		name: "CloudIndexView",
		directives: { vFoucs }
	},
	__name: "index",
	setup(__props, { expose: __expose }) {
		__expose();
		const __returned__ = { get vFoucs() {
			return vFoucs;
		} };
		Object.defineProperty(__returned__, "__isScriptSetup", {
			enumerable: false,
			value: true
		});
		return __returned__;
	}
});

【示例】 以 v 开头的变量会被自动视为指令

<template>
  <p>{{ "这里是云平台首页" }}</p>
  <input v-foucs name="age" type="text" placeholder="please input" />
</template>
<script setup lang="ts">
// 以 'v' 开头的变量会被自动视为指令
const vFoucs = {
  mounted: (el: HTMLInputElement) => {
    el.focus();
    console.log("vFouce mounted", el);
  },
};
defineOptions({
  name: "CloudIndexView",
});
</script>

image.png

const _sfc_main = /* @__PURE__ */ _defineComponent({
	...{ name: "CloudIndexView" },
	__name: "index",
	setup(__props, { expose: __expose }) {
		__expose();
		// 以 'v' 开头的变量会被自动视为指令
		const vFoucs = { mounted: (el) => {
			el.focus();
			console.log("vFouce mounted", el);
		} };
		const __returned__ = { vFoucs };
		Object.defineProperty(__returned__, "__isScriptSetup", {
			enumerable: false,
			value: true
		});
		return __returned__;
	}
});

const _hoisted_1 = {
	name: "age",
	type: "text",
	placeholder: "please input"
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
	return _openBlock(), _createElementBlock(
		_Fragment,
		null,
		[_cache[0] || (_cache[0] = _createElementVNode(
			"p",
			null,
			_toDisplayString("这里是云平台首页"),
			-1
			/* CACHED */
		)), _withDirectives(_createElementVNode(
			"input",
			_hoisted_1,
			null,
			512
			/* NEED_PATCH */
		), [[$setup["vFoucs"]]])],
		64
		/* STABLE_FRAGMENT */
	);
}

源码

function processDefineOptions(
  ctx: ScriptCompileContext,
  node: Node,
): boolean {
  if (!isCallOf(node, DEFINE_OPTIONS)) {
    return false
  }

  // 确保组件中只调用一次 defineOptions,如果已经调用过则报错
  if (ctx.hasDefineOptionsCall) {
    ctx.error(`duplicate ${DEFINE_OPTIONS}() call`, node)
  }

  // defineOptions 不接受类型参数,如果提供了类型参数则报错
  if (node.typeParameters) {
    ctx.error(`${DEFINE_OPTIONS}() cannot accept type arguments`, node)
  }

  // defineOptions 没有参数,则直接返回 true
  if (!node.arguments[0]) return true

  ctx.hasDefineOptionsCall = true // 标记为已调用

  // 将其第一个参数(处理 TypeScript 节点后)存储为运行时声明
  ctx.optionsRuntimeDecl = unwrapTSNode(node.arguments[0])

  let propsOption = undefined
  let emitsOption = undefined
  let exposeOption = undefined
  let slotsOption = undefined
  // 遍历 defineOptions 中的属性,查找 props、emits、expose、slots 选项
  if (ctx.optionsRuntimeDecl.type === 'ObjectExpression') {
    for (const prop of ctx.optionsRuntimeDecl.properties) {
      if (
        (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
        prop.key.type === 'Identifier'
      ) {
        switch (prop.key.name) {
          case 'props':
            propsOption = prop
            break

          case 'emits':
            emitsOption = prop
            break

          case 'expose':
            exposeOption = prop
            break

          case 'slots':
            slotsOption = prop
            break
        }
      }
    }
  }

  // defineOptions 不能用于声明 props、emits、expose、slots 选项
  if (propsOption) {
    ctx.error(
      `${DEFINE_OPTIONS}() cannot be used to declare props. Use ${DEFINE_PROPS}() instead.`,
      propsOption,
    )
  }
  if (emitsOption) {
    ctx.error(
      `${DEFINE_OPTIONS}() cannot be used to declare emits. Use ${DEFINE_EMITS}() instead.`,
      emitsOption,
    )
  }
  if (exposeOption) {
    ctx.error(
      `${DEFINE_OPTIONS}() cannot be used to declare expose. Use ${DEFINE_EXPOSE}() instead.`,
      exposeOption,
    )
  }
  if (slotsOption) {
    ctx.error(
      `${DEFINE_OPTIONS}() cannot be used to declare slots. Use ${DEFINE_SLOTS}() instead.`,
      slotsOption,
    )
  }

  return true
}

defineProp 声明组件接收的props

defineProps 是 Vue 3 在 <script setup> 语法糖中提供的一个编译宏,它的核心作用是声明组件的 props(属性) ,使组件能够从父组件接收数据。

defineProps 的两种声明方式:

  1. 运行时声明 (Runtime Declaration) :直接传递一个对象,在运行时提供类型检查和默认值校验。
  2. 类型声明 (Type-based Declaration) :通过泛型参数传递 TypeScript 类型接口,在编译时进行类型推导,让代码更简洁

注意事项

  1. defineProp 组件内只能调用一次。
  2. defineProp 不能同时使用运行时参数和类型参数。
  3. 使用 withDefaults 不能进行解构

【示例】运行时声明

defineProps({
  len: {
    type: Number,
    default: 0,
  },
  count: {
    type: Number,
    default: 0,
  },
});

【示例】类型声明

<script setup lang="ts">
defineProps<{
  len: number;
  count: number;
}>();
</script>

【示例】解构赋值

<script setup>
const { len = 0, count } = defineProps<{
  len: number;
  count: number;
}>();

console.log(len, count);
</script>

image.png

【示例】使用 withDefaults 不能进行解构

// ❌ 不能进行解构
const { len ,count } = withDefaults(
  defineProps<{
    len: number;
    count: number;
  }>(),
  {
    len: 0,
    count: 0,
  }
);

image.png

示例 数组形式

<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

const props = defineProps(["age", "dec"]);

onMounted(() => {
  console.log(props);
});
</script>

image.png

export function defineProps<PropNames extends string = string>(
  // 一个字符串数组,包含要定义的 prop 名称
  props: PropNames[],
): Prettify<Readonly<{ [key in PropNames]?: any }>>

示例 对象形式

<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

const props = defineProps({
  age: {
    type: Number,
    default: 20,
    validator: (val: number) => val >= 0,
  },
  dec: {
    type: String,
    default: "desc",
    validator: (val: string) => val.length > 0 && val.length < 30,
  },
});

onMounted(() => {
  console.log(props);
});
</script>

image.png

export function defineProps<
  PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
>(props: PP): Prettify<Readonly<ExtractPropTypes<PP>>>

示例 泛型形式

<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

interface Props {
  age: number;
  dec?: string;
}
const props = defineProps<Props>();

onMounted(() => {
  console.log(props);
});
</script>

image.png

export function defineProps<TypeProps>(): DefineProps<
  LooseRequired<TypeProps>,
  BooleanKey<TypeProps>
>

示例 结合 withDefaults 进行默认值设置

<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    len: number;
    count: number;
  }>(),
  {
    len: 0,
    count: 0,
  }
);

console.log(props.len, props.count);
</script>

image.png

源码

function processDefineProps(
  ctx: ScriptCompileContext,
  node: Node,
  declId?: LVal,
  isWithDefaults = false,
): boolean {
  if (!isCallOf(node, DEFINE_PROPS)) {
    return processWithDefaults(ctx, node, declId)
  }

  // 确保组件中只调用一次 defineProps,如果已经调用过则报错
  if (ctx.hasDefinePropsCall) {
    ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
  }
  ctx.hasDefinePropsCall = true // 标记为已调用

  // 记录 defineProps 的第一个参数作为运行时声明
  ctx.propsRuntimeDecl = node.arguments[0]

  // register bindings
  if (ctx.propsRuntimeDecl) {
    for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
      if (!(key in ctx.bindingMetadata)) {
        // 为每个键注册绑定类型为 PROP
        ctx.bindingMetadata[key] = BindingTypes.PROPS
      }
    }
  }

  // call has type parameters - infer runtime types from it
  if (node.typeParameters) {
    if (ctx.propsRuntimeDecl) {
      // 如果同时提供了运行时参数和类型参数,报错
      ctx.error(
        `${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
          `at the same time. Use one or the other.`,
        node,
      )
    }
    // 记录类型参数作为类型声明
    ctx.propsTypeDecl = node.typeParameters.params[0]
  }

  // handle props destructure
  // 解构赋值
  if (!isWithDefaults && declId && declId.type === 'ObjectPattern') {
    processPropsDestructure(ctx, declId)
  }

  ctx.propsCall = node
  ctx.propsDecl = declId

  return true
}
function processPropsDestructure(
  ctx: ScriptCompileContext,
  declId: ObjectPattern,
): void {
  if (ctx.options.propsDestructure === 'error') {
    // 如果禁止且设置为 'error',抛出错误
    ctx.error(`Props destructure is explicitly prohibited via config.`, declId)
  } else if (ctx.options.propsDestructure === false) {
    return
  }

  // 解构声明
  ctx.propsDestructureDecl = declId

  // 注册绑定
  const registerBinding = (
    key: string, // 键值
    local: string, // 本地名称
    defaultValue?: Expression,
  ) => {
    ctx.propsDestructuredBindings[key] = { local, default: defaultValue }

    // 当解构时使用了重命名(如 { foo: myFoo }),local 与 key 不同
    if (local !== key) {
      // 将本地变量名 myFoo 标记为 PROPS_ALIASED,表示它是一个 prop 的别名
      ctx.bindingMetadata[local] = BindingTypes.PROPS_ALIASED

      /**
       * 为什么需要 __propsAliases 映射?
        1、模板编译优化:当模板中使用 myFoo 时,编译器需要知道它实际是 props.foo,以便正确生成渲染函数代码。
        2、响应式追踪:确保别名变量也能正确追踪 prop 的变化(因为最终访问的是同一个 prop)。
        3、类型推导:在 TypeScript 环境下,维护映射有助于保持类型安全。 
       */
      // 建立反向映射:在 __propsAliases 对象中记录 myFoo → 'foo'
      ;(ctx.bindingMetadata.__propsAliases ||
        (ctx.bindingMetadata.__propsAliases = {}))[local] = key
    }
  }

  // 遍历对象的属性
  for (const prop of declId.properties) {
    // 普通属性
    if (prop.type === 'ObjectProperty') {
      // 获取属性键(支持计算属性)
      const propKey = resolveObjectKey(prop.key, prop.computed)

      // 不支持计算属性键
      if (!propKey) {
        ctx.error(
          `${DEFINE_PROPS}() destructure cannot use computed key.`,
          prop.key,
        )
      }

      // 处理默认值
      if (prop.value.type === 'AssignmentPattern') {
        // default value { foo = 123 }
        const { left, right } = prop.value
        // 不支持嵌套模式
        if (left.type !== 'Identifier') {
          ctx.error(
            `${DEFINE_PROPS}() destructure does not support nested patterns.`,
            left,
          )
        }
        registerBinding(propKey, left.name, right)

        // 处理简单解构
      } else if (prop.value.type === 'Identifier') {
        // simple destructure
        registerBinding(propKey, prop.value.name)
      } else {
        ctx.error(
          `${DEFINE_PROPS}() destructure does not support nested patterns.`,
          prop.value,
        )
      }

      // 剩余参数
    } else {
      // rest spread
      // 获取该标识符的名称字符串(例如 ...reset 那么name便是 reset)
      ctx.propsDestructureRestId = (prop.argument as Identifier).name
      // register binding
      // 绑定响应常量,便于在模板中使用
      ctx.bindingMetadata[ctx.propsDestructureRestId] =
        BindingTypes.SETUP_REACTIVE_CONST
    }
  }
}

withDefaults

withDefaults 是一个辅助编译宏,用于为基于类型声明的 defineProps 提供默认值。它只在使用 TypeScript 泛型声明 props 类型时才有必要,因为 TypeScript 接口仅在编译时存在,无法提供运行时的默认值。

withDefaults 接收两个参数:

  1. defineProps<Props>() 调用(不能省略括号)
  2. 一个默认值对象,键为 prop 名,值为默认值。

注意事项

  1. 必需配合 defineProps一起使用。
  2. 复杂类型必须使用函数返回默认值。对于数组、对象等引用类型,必须使用函数返回默认值,否则多个组件实例会共享同一个引用,导致状态污染。
  3. 默认值的类型必须与 props 类型声明匹配,TypeScript 会在编译时检查类型是否一致。

示例 结合 defineProps 提供默认值

<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

interface Props {
  age: number;
  dec?: string;
}
const props = withDefaults(defineProps<Props>(), {
  dec: "des",
  age: 18,
});

onMounted(() => {
  console.log(props);
  emit("change", { type: "测试" });
  emit("send", { age: 18 });
});

const emit = defineEmits<
  ((e: "change", data: { type: string }) => void) &
    ((e: "send", data: { age: number }) => void)
>();
</script>

image.png

示例 函数返回默认值

<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

interface Props {
  age: number;
  dec?: string;
}
const props = withDefaults(defineProps<Props>(), {
  dec: "des",
  age: () => 18,
});

onMounted(() => {
  console.log(props);
  emit("change", { type: "测试" });
  emit("send", { age: 18 });
});

const emit = defineEmits<
  ((e: "change", data: { type: string }) => void) &
    ((e: "send", data: { age: number }) => void)
>();
</script>

image.png

源码

vue3-core/packages/compiler-sfc/src/script/defineProps.ts

function processWithDefaults(
  ctx: ScriptCompileContext,
  node: Node,
  declId?: LVal,
): boolean {
  if (!isCallOf(node, WITH_DEFAULTS)) {
    return false
  }
  if (
    !processDefineProps(
      ctx,
      node.arguments[0],
      declId,
      true /* isWithDefaults */,
    )
  ) {
    // 验证第一个参数
    // 如果不是 defineProps 调用,则报错
    ctx.error(
      `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
      node.arguments[0] || node,
    )
  }

  // withDefaults 只能与基于类型的 defineProps 声明一起使用,不能与运行时声明一起使用
  if (ctx.propsRuntimeDecl) {
    ctx.error(
      `${WITH_DEFAULTS} can only be used with type-based ` +
        `${DEFINE_PROPS} declaration.`,
      node,
    )
  }

  // 警告内容说明 withDefaults 与解构一起使用是不必要的,并且会禁用响应式解构
  if (declId && declId.type === 'ObjectPattern') {
    ctx.warn(
      `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
        `Reactive destructure will be disabled when using withDefaults().\n` +
        `Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...). `,
      node.callee,
    )
  }

  // 记录 withDefaults 的第二个参数作为运行时默认值
  ctx.propsRuntimeDefaults = node.arguments[1]

  // 检查第二个参数是否存在,如果不存在则报错。
  if (!ctx.propsRuntimeDefaults) {
    ctx.error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
  }
  ctx.propsCall = node

  return true
}

defineEmits 声明组件触发的事件

在 Vue 3 的 <script setup> 语法中,defineEmits 是一个编译器宏,用于声明组件可以触发的自定义事件。它不需要手动导入,直接在 <script setup> 中使用即可。

允许子组件声明自己可以触发的自定义事件,让父组件能够监听并响应这些事件,从而实现清晰、可控的组件通信。

注意事项

1、组件只能调用一次 defineEmits。

image.png

2、调用签名和属性语法不能混用

image.png

声明方式

1、类型声明

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'submit'): void
}>()
</script>

2、运行时声明

<script setup lang="ts">
const emit = defineEmits(['update', 'submit'])
</script>

示例 字符串数组形式

适用场景:简单事件声明,不需要参数验证或类型检查。

// 父组件
<template>
  <p>{{ "这里是云平台首页" }}</p>
  <tabTwo :age="age" :dec="dec" @change="changeAge"> </tabTwo>
</template>



const changeAge = (data: any) => {
  console.log(data);
};
def
// 子组件
<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

interface Props {
  age: number;
  dec?: string;
}
const props = defineProps<Props>();

onMounted(() => {
  console.log(props);

  emit("change", { type: "测试" });
});

const emit = defineEmits(["change"]);
</script>

image.png

示例 泛型类型形式

适用场景:TypeScript 项目,需要严格的类型安全。

【示例】直接对象类型写法(最常用)

语法:使用对象类型,键为事件名,值为事件参数的元组类型。

  • 直观易读,最推荐的写法
  • 支持命名元组,提高参数可读性
  • 支持 kebab-case 事件名(如 update:modelValue
<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

interface Props {
  age: number;
  dec?: string;
}
const props = defineProps<Props>();

onMounted(() => {
  console.log(props);
  emit("change", { type: "测试" });
});

const emit = defineEmits<{
  // 带参数事件
  change: [data: { type: string }];
}>();
</script>
import { onMounted } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
const _sfc_main = /* @__PURE__ */ _defineComponent({
	__name: "tabTwo",
	props: {
		age: {
			type: Number,
			required: true
		},
		dec: {
			type: String,
			required: false
		}
	},
	emits: ["change"],
	setup(__props, { expose: __expose, emit: __emit }) {
		__expose();
		const props = __props;
		onMounted(() => {
			console.log(props);
			emit("change", { type: "测试" });
		});
		const emit = __emit;
		const __returned__ = {
			props,
			emit
		};
		Object.defineProperty(__returned__, "__isScriptSetup", {
			enumerable: false,
			value: true
		});
		return __returned__;
	}
});

【示例】 函数重载类型

<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

interface Props {
  age: number;
  dec?: string;
}
const props = defineProps<Props>();

onMounted(() => {
  console.log(props);
  emit("change", { type: "测试" });
  emit("send", { age: props.age });
});

const emit = defineEmits<{
  (e: "change", data: { type: string }): void;
  (e: "send", data: { age: number }): void;
}>();
</script>

image.png

【示例】 函数类型交叉写法

  • 更灵活,可定义复杂的函数重载
  • 适合需要精确控制函数签名的场景
  • 语法相对冗长,一般不推荐
<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

interface Props {
  age: number;
  dec?: string;
}
const props = defineProps<Props>();

onMounted(() => {
  console.log(props);
  emit("change", { type: "测试" });
  emit("send", { age: 18 });
});

const emit = defineEmits<
  ((e: "change", data: { type: string }) => void) &
    ((e: "send", data: { age: number }) => void)
>();
</script>

image.png

示例 对象形式

  • 支持运行时参数验证
  • 类型检查有限(基于验证函数的参数类型)
<template>
  <div>
    <p>age: {{ age }}</p>
    <p>dec: {{ dec }}</p>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";

interface Props {
  age: number;
  dec?: string;
}
const props = defineProps<Props>();

onMounted(() => {
  console.log(props);
  emit("change", { type: "测试" });
  emit("send", { age: 18 });
});

const emit = defineEmits({
  change: null,
  // 验证 send 事件的参数是否符合要求
  send: (data: { age: number }) => {
    // 验证 age 是否为数字
    if (typeof data.age !== "number") {
      return false;
    }
    return true;
  },
});
</script>

image.png

源码

function processDefineEmits(
  ctx: ScriptCompileContext,
  node: Node,
  declId?: LVal,
): boolean {
  // 检查传入的 AST 节点是否为 defineEmits 函数调用
  if (!isCallOf(node, DEFINE_EMITS)) {
    return false
  }
  // 确保组件中只调用一次 defineEmits
  if (ctx.hasDefineEmitCall) {
    ctx.error(`duplicate ${DEFINE_EMITS}() call`, node)
  }
  ctx.hasDefineEmitCall = true // 标记已调用

  // 记录 defineEmits 的第一个参数作为运行时声明
  ctx.emitsRuntimeDecl = node.arguments[0]

  if (node.typeParameters) {
    if (ctx.emitsRuntimeDecl) {
      // 如果同时提供了运行时参数和类型参数,报错
      ctx.error(
        `${DEFINE_EMITS}() cannot accept both type and non-type arguments ` +
          `at the same time. Use one or the other.`,
        node,
      )
    }
    // 记录类型参数作为类型声明
    ctx.emitsTypeDecl = node.typeParameters.params[0]
  }

  // 声明标识符
  ctx.emitDecl = declId

  return true
}

defineSlots

在 Vue 3.3 版本中引入的 defineSlots,是一个专为 TypeScript 项目设计的编译时宏,主要作用是为组件的插槽(Slots)提供完整的类型声明和检查。其核心价值在于为作用域插槽(Scoped Slots) 提供精确的类型提示。

注意事项

  1. defineSlots 在组件内只能调用一次。
  2. defineSlots 不接收参数。

defineSlots 的使用方法非常直接,它的核心语法是在 <script setup> 中,通过泛型参数来声明插槽的类型。

示例 useSlots 使用

<template>
  <div>
    <p>这里是 tabTwo 标题</p>
    <template v-for="(_, name) in slots" :key="name">
      <slot :name="name" :data="info"></slot>
    </template>
  </div>
</template>
<script setup lang="ts">
import { useSlots } from "vue";

defineProps<{
  info: {
    buttonName: string;
  };
}>();

const slots = useSlots();
</script>

image.png

示例 defineSlots 返回

<template>
  <p>{{ "这里是云平台首页" }}</p>
  <tabTwo :info="info">
    <template #default>
      <p>这里是 插槽 default 部分</p>
    </template>
    <template #body>
      <p>这里是 插槽 body 部分</p>
    </template>
  </tabTwo>
</template>
<script setup lang="ts">
import tabTwo from "@/pages/cloud/components/tabTwo.vue";
import { reactive } from "vue";

const info = reactive({
  buttonName: "提交",
});
defineOptions({
  name: "CloudIndexView",
});

const slots = defineSlots<{
  default(): void;
  body(): void;
  // footer(data: { buttonName: string }): void;
}>();

console.log("xxx", slots);
</script>

image.png

源码

function processDefineSlots(
  ctx: ScriptCompileContext,
  node: Node,
  declId?: LVal,
): boolean {
  if (!isCallOf(node, DEFINE_SLOTS)) {
    return false
  }
  // 确保组件中只调用一次 defineSlots,如果已经调用过则报错
  if (ctx.hasDefineSlotsCall) {
    ctx.error(`duplicate ${DEFINE_SLOTS}() call`, node)
  }
  ctx.hasDefineSlotsCall = true // 标记为已调用

  // 确保 defineSlots 不接受任何参数,如果提供了参数则报错
  if (node.arguments.length > 0) {
    ctx.error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
  }

  // 转换为 useSlots 辅助函数
  if (declId) {
    // 将 defineSlots() 调用转换为 useSlots() 调用
    ctx.s.overwrite(
      ctx.startOffset! + node.start!,
      ctx.startOffset! + node.end!,
      `${ctx.helper('useSlots')}()`,
    )
  }

  return true
}

最后