组件基础 #

一、组件概述 #

组件是Vue最强大的功能之一。组件可以扩展HTML元素,封装可重用的代码。

1.1 组件的优点 #

  • 复用性:一次编写,多处使用
  • 维护性:逻辑独立,便于维护
  • 可组合:小组件组合成大组件
  • 封装性:隐藏内部实现细节

1.2 组件树结构 #

text
App (根组件)
├── Header
│   ├── Logo
│   └── Navigation
├── Main
│   ├── Sidebar
│   └── Content
│       ├── Article
│       └── Comments
└── Footer

二、定义组件 #

2.1 单文件组件(SFC) #

vue
<!-- MyComponent.vue -->
<template>
  <div class="my-component">
    <h2>{{ title }}</h2>
    <p>{{ content }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: '组件标题',
      content: '组件内容'
    }
  }
}
</script>

<style scoped>
.my-component {
  padding: 20px;
  border: 1px solid #ddd;
}
</style>

2.2 使用setup语法糖 #

vue
<!-- MyComponent.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <button @click="handleClick">点击</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const title = ref('组件标题')

function handleClick() {
  console.log('点击了')
}
</script>

2.3 defineComponent函数 #

vue
<script>
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'MyComponent',
  setup() {
    const count = ref(0)
    
    function increment() {
      count.value++
    }
    
    return {
      count,
      increment
    }
  },
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <button @click="increment">+1</button>
    </div>
  `
})
</script>

2.4 字符串模板组件 #

javascript
import { createApp } from 'vue'

const app = createApp({})

// 使用字符串模板定义组件
app.component('MyButton', {
  template: '<button class="btn"><slot/></button>'
})

三、注册组件 #

3.1 全局注册 #

javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'

const app = createApp(App)

// 全局注册
app.component('MyComponent', MyComponent)
app.component('MyButton', {
  template: '<button><slot/></button>'
})

app.mount('#app')
vue
<!-- 任何组件都可以直接使用 -->
<template>
  <MyComponent />
  <MyButton>按钮</MyButton>
</template>

3.2 局部注册 #

vue
<template>
  <MyComponent />
  <MyButton>按钮</MyButton>
</template>

<script>
import MyComponent from './MyComponent.vue'
import MyButton from './MyButton.vue'

export default {
  components: {
    MyComponent,
    MyButton
  }
}
</script>

3.3 setup语法糖中的局部注册 #

vue
<template>
  <MyComponent />
  <MyButton>按钮</MyButton>
</template>

<script setup>
import MyComponent from './MyComponent.vue'
import MyButton from './MyButton.vue'

// 导入的组件直接可用,无需显式注册
</script>

3.4 全局vs局部注册 #

特性 全局注册 局部注册
使用范围 所有组件 当前组件
Tree-shaking 不支持 支持
构建体积 较大 较小
适用场景 基础组件 业务组件

四、组件命名 #

4.1 命名规范 #

javascript
// 推荐:多词名称,避免与HTML元素冲突
app.component('TodoItem', { /* ... */ })
app.component('TodoList', { /* ... */ })

// 不推荐:单字名称
// app.component('Item', { /* ... */ })

4.2 在模板中使用 #

vue
<template>
  <!-- 推荐使用kebab-case -->
  <todo-item />
  <todo-list />
  
  <!-- 也可以使用PascalCase -->
  <TodoItem />
  <TodoList />
</template>

4.3 name选项 #

vue
<script>
export default {
  name: 'TodoItem',
  // ...
}
</script>

<script setup>
// Vue 3.3+ 使用defineOptions
defineOptions({
  name: 'TodoItem'
})
</script>

五、组件Props #

5.1 基本使用 #

vue
<!-- 子组件 Child.vue -->
<template>
  <div>
    <h3>{{ title }}</h3>
    <p>{{ content }}</p>
  </div>
</template>

<script setup>
defineProps({
  title: String,
  content: String
})
</script>
vue
<!-- 父组件 -->
<template>
  <Child title="标题" content="内容" />
</template>

5.2 Props定义方式 #

vue
<script setup>
// 简单声明
defineProps(['title', 'content'])

// 对象声明(带类型)
defineProps({
  title: String,
  count: Number,
  isActive: Boolean
})

// 完整声明(带验证)
defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  },
  items: {
    type: Array,
    default: () => []
  },
  user: {
    type: Object,
    default: () => ({})
  },
  validator: {
    validator(value) {
      return ['success', 'warning', 'danger'].includes(value)
    }
  }
})
</script>

5.3 Props类型 #

vue
<script setup>
defineProps({
  // 基本类型
  stringProp: String,
  numberProp: Number,
  booleanProp: Boolean,
  arrayProp: Array,
  objectProp: Object,
  functionProp: Function,
  symbolProp: Symbol,
  
  // 多种类型
  multipleType: [String, Number],
  
  // 自定义类
  dateProp: Date,
  
  // 自定义构造函数
  customProp: CustomClass
})
</script>

5.4 传递Props #

vue
<template>
  <!-- 静态传递 -->
  <Child title="静态标题" />
  
  <!-- 动态传递 -->
  <Child :title="dynamicTitle" />
  
  <!-- 传递数字 -->
  <Child :count="100" />
  
  <!-- 传递布尔值 -->
  <Child :is-active="true" />
  
  <!-- 传递数组 -->
  <Child :items="['a', 'b', 'c']" />
  
  <!-- 传递对象 -->
  <Child :user="{ name: '张三', age: 25 }" />
  
  <!-- 传递对象的所有属性 -->
  <Child v-bind="userObject" />
</template>

5.5 单向数据流 #

vue
<script setup>
const props = defineProps({
  count: {
    type: Number,
    default: 0
  }
})

// ❌ 不要直接修改props
// props.count++

// ✅ 使用本地数据
import { ref, computed } from 'vue'

const localCount = ref(props.count)

// ✅ 使用计算属性
const doubledCount = computed(() => props.count * 2)
</script>

六、组件事件 #

6.1 定义和触发事件 #

vue
<!-- 子组件 Child.vue -->
<template>
  <button @click="handleClick">点击</button>
</template>

<script setup>
const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', { id: 1, name: 'updated' })
}
</script>
vue
<!-- 父组件 -->
<template>
  <Child 
    @update="handleUpdate" 
    @delete="handleDelete" 
  />
</template>

<script setup>
function handleUpdate(data) {
  console.log('更新:', data)
}

function handleDelete(id) {
  console.log('删除:', id)
}
</script>

6.2 事件验证 #

vue
<script setup>
const emit = defineEmits({
  submit: ({ email, password }) => {
    if (!email || !password) {
      console.warn('邮箱和密码不能为空')
      return false
    }
    return true
  },
  
  // 无验证
  click: null
})

function handleSubmit() {
  emit('submit', { email: 'test@example.com', password: '123456' })
}
</script>

6.3 v-model实现 #

vue
<!-- 子组件 CustomInput.vue -->
<template>
  <input 
    :value="modelValue" 
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
vue
<!-- 父组件 -->
<template>
  <CustomInput v-model="text" />
</template>

6.4 多个v-model #

vue
<!-- 子组件 UserForm.vue -->
<template>
  <input :value="firstName" @input="$emit('update:firstName', $event.target.value)">
  <input :value="lastName" @input="$emit('update:lastName', $event.target.value)">
</template>

<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
vue
<!-- 父组件 -->
<template>
  <UserForm 
    v-model:first-name="first" 
    v-model:last-name="last" 
  />
</template>

七、插槽 #

7.1 基本插槽 #

vue
<!-- 子组件 Card.vue -->
<template>
  <div class="card">
    <slot>默认内容</slot>
  </div>
</template>
vue
<!-- 父组件 -->
<template>
  <Card>自定义内容</Card>
  <Card />  <!-- 显示默认内容 -->
</template>

7.2 具名插槽 #

vue
<!-- 子组件 Layout.vue -->
<template>
  <div class="container">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
vue
<!-- 父组件 -->
<template>
  <Layout>
    <template #header>
      <h1>页面标题</h1>
    </template>
    
    <template #default>
      <p>主要内容</p>
    </template>
    
    <template #footer>
      <p>页脚信息</p>
    </template>
  </Layout>
</template>

7.3 作用域插槽 #

vue
<!-- 子组件 List.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="index">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script setup>
defineProps({
  items: Array
})
</script>
vue
<!-- 父组件 -->
<template>
  <List :items="users">
    <template #default="{ item, index }">
      <span>{{ index + 1 }}. {{ item.name }} - {{ item.email }}</span>
    </template>
  </List>
</template>

7.4 动态插槽名 #

vue
<template>
  <Layout>
    <template #[dynamicSlotName]>
      动态插槽内容
    </template>
  </Layout>
</template>

<script setup>
import { ref } from 'vue'
const dynamicSlotName = ref('header')
</script>

八、组件实例 #

8.1 访问组件实例 #

vue
<template>
  <Child ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

function callChildMethod() {
  // 访问子组件暴露的方法
  childRef.value.someMethod()
}
</script>
vue
<!-- Child.vue -->
<template>
  <div>子组件</div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}

function someMethod() {
  console.log('子组件方法被调用')
}

// 暴露给父组件
defineExpose({
  count,
  increment,
  someMethod
})
</script>

8.2 模板引用 #

vue
<template>
  <!-- 访问DOM元素 -->
  <input ref="inputRef" />
  <button @click="focusInput">聚焦</button>
  
  <!-- v-for中的ref -->
  <div v-for="item in items" :ref="setItemRef" :key="item.id">
    {{ item.name }}
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const inputRef = ref(null)
const itemRefs = ref([])

function setItemRef(el) {
  if (el) {
    itemRefs.value.push(el)
  }
}

function focusInput() {
  inputRef.value.focus()
}

onMounted(() => {
  inputRef.value.focus()
})
</script>

九、动态组件 #

9.1 基本使用 #

vue
<template>
  <button @click="current = 'Home'">首页</button>
  <button @click="current = 'About'">关于</button>
  
  <component :is="currentComponent" />
</template>

<script setup>
import { ref, computed } from 'vue'
import Home from './Home.vue'
import About from './About.vue'

const current = ref('Home')

const currentComponent = computed(() => {
  return current.value === 'Home' ? Home : About
})
</script>

9.2 keep-alive缓存 #

vue
<template>
  <button @click="current = 'Home'">首页</button>
  <button @click="current = 'About'">关于</button>
  
  <!-- 使用keep-alive缓存组件状态 -->
  <keep-alive>
    <component :is="currentComponent" />
  </keep-alive>
</template>

9.3 keep-alive配置 #

vue
<template>
  <!-- include - 包含的组件 -->
  <keep-alive include="Home,About">
    <component :is="currentComponent" />
  </keep-alive>
  
  <!-- exclude - 排除的组件 -->
  <keep-alive exclude="Settings">
    <component :is="currentComponent" />
  </keep-alive>
  
  <!-- max - 最大缓存数 -->
  <keep-alive :max="10">
    <component :is="currentComponent" />
  </keep-alive>
</template>

十、异步组件 #

10.1 基本使用 #

vue
<template>
  <AsyncComponent />
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)
</script>

10.2 加载状态 #

vue
<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})
</script>

10.3 配合Suspense #

vue
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

十一、总结 #

概念 说明
单文件组件 .vue文件定义组件
全局注册 app.component()
局部注册 components选项
Props 父传子数据
Emits 子传父事件
插槽 内容分发
动态组件 component :is
异步组件 defineAsyncComponent

组件基础要点:

  • 优先使用单文件组件
  • 优先使用局部注册
  • Props是单向数据流
  • 使用事件进行子传父通信
  • 插槽实现内容分发
  • 动态组件实现组件切换
最后更新:2026-03-26