阿里云主机折上折
  • 微信号
Current Site:Index > Compiler macros (defineProps/defineEmits, etc.)

Compiler macros (defineProps/defineEmits, etc.)

Author:Chuan Chen 阅读数:50009人阅读 分类: Vue.js

The Core Role of Compiler Macros in Vue 3

Vue 3's <script setup> syntactic sugar introduces a series of compiler macros, which are specially processed during the compilation phase. defineProps and defineEmits are the two most commonly used macros, providing type-safe props and emits declarations for components. These macros are only available in the <script setup> context and do not require explicit imports.

Basic Usage of defineProps

defineProps is used to declare the props a component receives, supporting both runtime declarations and type declarations:

<script setup>
// Runtime declaration
const props = defineProps({
  title: String,
  likes: Number
})

// Type-based declaration
const props = defineProps<{
  title?: string
  likes: number
}>()
</script>

The type declaration method requires TypeScript support. When using type declarations, default values must be set via the withDefaults compiler macro:

interface Props {
  title?: string
  likes: number
}

const props = withDefaults(defineProps<Props>(), {
  title: 'Default Title',
  likes: 0
})

Detailed Usage of defineEmits

defineEmits is used to declare events that a component can emit, also supporting two declaration methods:

<script setup>
// Runtime declaration
const emit = defineEmits(['change', 'update'])

// Type-based declaration
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

The type declaration method provides stricter parameter type checking. Events can be emitted as follows:

function handleClick() {
  emit('change', 1)  // Correct
  emit('update', 'new value')  // Correct
  emit('change', 'string')  // Type error
}

Compile-Time Characteristics of Macros

These compiler macros are completely removed during the compilation phase and do not appear in the final generated code. For example:

const props = defineProps<{ title: string }>()

Is compiled into:

const props = __props[0]

This design ensures these API calls incur no runtime overhead. Additionally, IDEs can leverage these macros to provide better type hints and code completion.

Comparison with Options API

Compared to the props and emits options in the Options API, compiler macros offer a more concise syntax:

// Options API
export default {
  props: {
    title: String
  },
  emits: ['change'],
  setup(props, { emit }) {
    // ...
  }
}

// Composition API with <script setup>
<script setup>
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ (e: 'change'): void }>()
</script>

Advanced Type Usage

For complex scenarios, advanced TypeScript features can be combined:

<script setup lang="ts">
interface User {
  id: number
  name: string
}

const props = defineProps<{
  userList: User[]
  callback: (user: User) => boolean
}>()

const emit = defineEmits<{
  loaded: [users: User[]]
  error: [message: string, code?: number]
}>()
</script>

Integration with Other Macros

defineProps and defineEmits can be used alongside other compiler macros:

<script setup>
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ (e: 'update:modelValue', value: string): void }>()

// Used with defineExpose
defineExpose({
  reset: () => { /*...*/ }
})
</script>

Practical Application Example

A complete form component example:

<script setup lang="ts">
interface FormItem {
  field: string
  value: any
  required?: boolean
}

const props = defineProps<{
  items: FormItem[]
  submitText?: string
}>()

const emit = defineEmits<{
  (e: 'submit', formData: Record<string, any>): void
  (e: 'cancel'): void
}>()

const formData = computed(() => 
  props.items.reduce((obj, item) => {
    obj[item.field] = item.value
    return obj
  }, {} as Record<string, any>)
)

function handleSubmit() {
  if (validateForm()) {
    emit('submit', formData.value)
  }
}
</script>

Type Inference and IDE Support

When using type declarations, the Volar plugin provides complete type inference. For example:

const props = defineProps<{
  user: {
    name: string
    age: number
  }
}>()

// In the template, the IDE can auto-suggest user.name and user.age

Limitations and Considerations

  1. These macros can only be used in <script setup>
  2. Type declarations require TypeScript support
  3. Props or emits cannot be dynamically defined
  4. Macro arguments must be literals, not variables
// Incorrect example
const propDefinition = { title: String }
const props = defineProps(propDefinition)  // Compilation error

Comparison with Other Frameworks

Compared to React's props type checking (PropTypes or TypeScript interfaces), Vue's compiler macros offer tighter template integration:

// React approach
interface Props {
  title: string;
}

function Component({ title }: Props) {
  return <div>{title}</div>
}

// Vue approach
<script setup lang="ts">
defineProps<{ title: string }>()
</script>

<template>
  <div>{{ title }}</div>
</template>

Performance Considerations

Since these macros are processed at compile time, they incur no runtime overhead. Compared to the Options API:

  1. No runtime props option processing is needed
  2. Emits validation only occurs in development mode
  3. The generated code is more concise

Custom Macro Extensions

Although uncommon, custom macros can be added via Vue compiler options. This requires modifying the build configuration:

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      compilerOptions: {
        macros: {
          defineMyMacro: () => ({ /* transformation logic */ })
        }
      }
    })
  ]
}

Differences from Composition API Functions

Key distinctions between compiler macros and regular Composition API functions:

Feature Compiler Macros Regular Composition API
Import Method Automatically available, no import needed Must be imported from 'vue'
Compilation Fully replaced at compile time Retained as runtime function calls
Available Context Only in <script setup> Anywhere
TypeScript Support Deep integration Regular type annotations

Debugging and Error Handling

When types do not match, TypeScript provides detailed error messages:

const emit = defineEmits<{
  (e: 'submit', payload: { id: number }): void
}>()

function handleClick() {
  emit('submit', { id: '123' })  // Error: 'string' is not assignable to 'number'
}

In the browser console, if incorrect props types are passed, Vue will issue a warning:

[Vue warn]: Invalid prop: type check failed for prop "count". Expected Number, got String

Migration Strategy from Vue 2

When migrating from Vue 2, options can be gradually replaced:

// Vue 2
export default {
  props: {
    title: String
  },
  emits: ['change'],
  methods: {
    update() {
      this.$emit('change')
    }
  }
}

// Vue 3
<script setup>
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ (e: 'change'): void }>()

function update() {
  emit('change')
}
</script>

Handling in Unit Tests

When testing components that use compiler macros, note the following:

// Testing a component
import { mount } from '@vue/test-utils'
import MyComponent from './MyComponent.vue'

test('emits change event', () => {
  const wrapper = mount(MyComponent, {
    props: {
      title: 'Test Title'
    }
  })
  
  wrapper.vm.emit('change')  // Test emit call
  expect(wrapper.emitted().change).toBeTruthy()
})

Integration with Other Vue Features

Compiler macros seamlessly integrate with other Vue features:

<script setup>
const props = defineProps<{ initialCount: number }>()
const count = ref(props.initialCount)

// Using watch to observe props changes
watch(() => props.initialCount, (newVal) => {
  count.value = newVal
})

// Providing/injecting context
provide('count', readonly(count))
</script>

Type Export and Reuse

Props interfaces can be exported for use by other components:

// types.ts
export interface PaginationProps {
  current: number
  pageSize: number
  total: number
}

// Usage in a component
<script setup lang="ts">
import type { PaginationProps } from './types'

const props = defineProps<PaginationProps>()
</script>

Reactive Props Destructuring

If props need to be destructured, use toRefs to maintain reactivity:

<script setup>
const props = defineProps<{ title: string; count: number }>()
const { title, count } = toRefs(props)

watch(count, (newVal) => {
  console.log(`count changed to ${newVal}`)
})
</script>

Dynamic Component Scenarios

When used with dynamic components, compiler macros remain effective:

<script setup>
const props = defineProps<{
  component: Component
  props?: Record<string, any>
}>()
</script>

<template>
  <component :is="component" v-bind="props || {}" />
</template>

JSX/TSX Support

Compiler macros also work with JSX/TSX:

// MyComponent.tsx
export default defineComponent({
  props: {
    // Traditional approach
  },
  setup(props) {
    // ...
  }
})

// Or
const MyComponent = defineComponent(
  (props: { title: string }) => {
    // ...
  }
)

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:Effect作用域API

下一篇:Vite构建工具

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.