vuejs-development

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vue.js Development Skill

Vue.js开发技能

This skill provides comprehensive guidance for building modern Vue.js applications using the Composition API, reactivity system, single-file components, directives, and lifecycle hooks based on official Vue.js documentation.
本技能基于Vue.js官方文档,提供使用Composition API、响应式系统、单文件组件、指令和生命周期钩子构建现代Vue.js应用的全面指导。

When to Use This Skill

何时使用此技能

Use this skill when:
  • Building single-page applications (SPAs) with Vue.js
  • Creating progressive web applications (PWAs) with Vue
  • Developing interactive user interfaces with reactive data
  • Building component-based architectures
  • Implementing forms, data fetching, and state management
  • Creating reusable UI components and libraries
  • Migrating from Options API to Composition API
  • Optimizing Vue application performance
  • Building accessible and maintainable web applications
  • Integrating with TypeScript for type-safe development
在以下场景使用本技能:
  • 使用Vue.js构建单页应用(SPA)
  • 使用Vue创建渐进式Web应用(PWA)
  • 开发带有响应式数据的交互式用户界面
  • 构建基于组件的架构
  • 实现表单、数据获取和状态管理
  • 创建可复用的UI组件和库
  • 从Options API迁移到Composition API
  • 优化Vue应用性能
  • 构建可访问且可维护的Web应用
  • 与TypeScript集成以实现类型安全开发

Core Concepts

核心概念

Reactivity System

响应式系统

Vue's reactivity system is the core mechanism that tracks dependencies and automatically updates the DOM when data changes.
Reactive State with ref():
javascript
import { ref } from 'vue'

// ref creates a reactive reference to a value
const count = ref(0)

// Access value with .value
console.log(count.value) // 0

// Modify value
count.value++
console.log(count.value) // 1

// In templates, .value is automatically unwrapped
// <template>{{ count }}</template>
Reactive Objects with reactive():
javascript
import { reactive } from 'vue'

// reactive creates a reactive proxy of an object
const state = reactive({
  name: 'Vue',
  version: 3,
  features: ['Composition API', 'Teleport', 'Suspense']
})

// Access and modify properties directly
console.log(state.name) // 'Vue'
state.name = 'Vue.js'

// Nested objects are also reactive
state.features.push('Fragments')
When to Use ref() vs reactive():
javascript
// Use ref() for:
// - Primitive values (string, number, boolean)
// - Single values that need reactivity
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)

// Use reactive() for:
// - Objects with multiple properties
// - Complex data structures
const user = reactive({
  id: 1,
  name: 'John',
  email: 'john@example.com',
  preferences: {
    theme: 'dark',
    notifications: true
  }
})
Computed Properties:
javascript
import { ref, computed } from 'vue'

const count = ref(0)

// Computed property automatically tracks dependencies
const doubled = computed(() => count.value * 2)

console.log(doubled.value) // 0
count.value = 5
console.log(doubled.value) // 10

// Writable computed
const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

fullName.value = 'Jane Smith'
console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'
Watchers:
javascript
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

// Watch a single ref
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`)
})

// Watch multiple sources
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
  console.log(`Count: ${newCount}, Message: ${newMessage}`)
})

// Watch reactive object property
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (newValue, oldValue) => {
    console.log(`State count changed from ${oldValue} to ${newValue}`)
  }
)

// watchEffect automatically tracks dependencies
watchEffect(() => {
  console.log(`Count is ${count.value}`)
  // Automatically re-runs when count changes
})

// Immediate execution
watch(count, (newValue) => {
  console.log(`Count is now ${newValue}`)
}, { immediate: true })

// Deep watching
const user = reactive({ profile: { name: 'John' } })
watch(user, (newValue) => {
  console.log('User changed:', newValue)
}, { deep: true })
Vue的响应式系统是跟踪依赖并在数据变化时自动更新DOM的核心机制。
使用ref()创建响应式状态:
javascript
import { ref } from 'vue'

// ref创建一个值的响应式引用
const count = ref(0)

// 通过.value访问值
console.log(count.value) // 0

// 修改值
count.value++
console.log(count.value) // 1

// 在模板中,.value会自动解包
// <template>{{ count }}</template>
使用reactive()创建响应式对象:
javascript
import { reactive } from 'vue'

// reactive创建一个对象的响应式代理
const state = reactive({
  name: 'Vue',
  version: 3,
  features: ['Composition API', 'Teleport', 'Suspense']
})

// 直接访问和修改属性
console.log(state.name) // 'Vue'
state.name = 'Vue.js'

// 嵌套对象同样具有响应式
state.features.push('Fragments')
何时使用ref() vs reactive():
javascript
// 使用ref()的场景:
// - 原始值(字符串、数字、布尔值)
// - 需要响应式的单个值
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)

// 使用reactive()的场景:
// - 包含多个属性的对象
// - 复杂数据结构
const user = reactive({
  id: 1,
  name: 'John',
  email: 'john@example.com',
  preferences: {
    theme: 'dark',
    notifications: true
  }
})
计算属性:
javascript
import { ref, computed } from 'vue'

const count = ref(0)

// 计算属性会自动跟踪依赖
const doubled = computed(() => count.value * 2)

console.log(doubled.value) // 0
count.value = 5
console.log(doubled.value) // 10

// 可写计算属性
const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

fullName.value = 'Jane Smith'
console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'
监听器:
javascript
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

// 监听单个ref
watch(count, (newValue, oldValue) => {
  console.log(`Count从${oldValue}变为${newValue}`)
})

// 监听多个源
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
  console.log(`Count: ${newCount}, Message: ${newMessage}`)
})

// 监听响应式对象的属性
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (newValue, oldValue) => {
    console.log(`State count从${oldValue}变为${newValue}`)
  }
)

// watchEffect自动跟踪依赖
watchEffect(() => {
  console.log(`Count是${count.value}`)
  // 当count变化时自动重新运行
})

// 立即执行
watch(count, (newValue) => {
  console.log(`Count现在是${newValue}`)
}, { immediate: true })

// 深度监听
const user = reactive({ profile: { name: 'John' } })
watch(user, (newValue) => {
  console.log('User已更改:', newValue)
}, { deep: true })

Composition API

Composition API

The Composition API provides a set of function-based APIs for organizing component logic.
Basic Component with <script setup>:
vue
<script setup>
import { ref, computed, onMounted } from 'vue'

// Props
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0
  }
})

// Emits
const emit = defineEmits(['update', 'delete'])

// Reactive state
const localCount = ref(props.count)
const message = ref('Hello Vue!')

// Computed
const doubledCount = computed(() => localCount.value * 2)

// Methods
function increment() {
  localCount.value++
  emit('update', localCount.value)
}

// Lifecycle
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ message }}</p>
    <p>Count: {{ localCount }}</p>
    <p>Doubled: {{ doubledCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>
Component without <script setup> (verbose syntax):
vue
<script>
import { ref, computed, onMounted } from 'vue'

export default {
  name: 'MyComponent',
  props: {
    title: String,
    count: {
      type: Number,
      default: 0
    }
  },
  emits: ['update', 'delete'],
  setup(props, { emit }) {
    const localCount = ref(props.count)
    const message = ref('Hello Vue!')

    const doubledCount = computed(() => localCount.value * 2)

    function increment() {
      localCount.value++
      emit('update', localCount.value)
    }

    onMounted(() => {
      console.log('Component mounted')
    })

    return {
      localCount,
      message,
      doubledCount,
      increment
    }
  }
}
</script>
Composition API提供了一组基于函数的API,用于组织组件逻辑。
使用<script setup>的基础组件:
vue
<script setup>
import { ref, computed, onMounted } from 'vue'

// Props
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0
  }
})

// 自定义事件
const emit = defineEmits(['update', 'delete'])

// 响应式状态
const localCount = ref(props.count)
const message = ref('Hello Vue!')

// 计算属性
const doubledCount = computed(() => localCount.value * 2)

// 方法
function increment() {
  localCount.value++
  emit('update', localCount.value)
}

// 生命周期钩子
onMounted(() => {
  console.log('组件已挂载')
})
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ message }}</p>
    <p>Count: {{ localCount }}</p>
    <p>翻倍值: {{ doubledCount }}</p>
    <button @click="increment">增加</button>
  </div>
</template>
不使用<script setup>的组件(详细语法):
vue
<script>
import { ref, computed, onMounted } from 'vue'

export default {
  name: 'MyComponent',
  props: {
    title: String,
    count: {
      type: Number,
      default: 0
    }
  },
  emits: ['update', 'delete'],
  setup(props, { emit }) {
    const localCount = ref(props.count)
    const message = ref('Hello Vue!')

    const doubledCount = computed(() => localCount.value * 2)

    function increment() {
      localCount.value++
      emit('update', localCount.value)
    }

    onMounted(() => {
      console.log('组件已挂载')
    })

    return {
      localCount,
      message,
      doubledCount,
      increment
    }
  }
}
</script>

Single-File Components

单文件组件

Single-file components (.vue) combine template, script, and styles in one file.
Complete SFC Example:
vue
<script setup>
import { ref, computed } from 'vue'

const tasks = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: false }
])

const newTaskText = ref('')

const completedCount = computed(() =>
  tasks.value.filter(t => t.completed).length
)

const remainingCount = computed(() =>
  tasks.value.filter(t => !t.completed).length
)

function addTask() {
  if (newTaskText.value.trim()) {
    tasks.value.push({
      id: Date.now(),
      text: newTaskText.value,
      completed: false
    })
    newTaskText.value = ''
  }
}

function toggleTask(id) {
  const task = tasks.value.find(t => t.id === id)
  if (task) task.completed = !task.completed
}

function removeTask(id) {
  tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>

<template>
  <div class="todo-app">
    <h1>Todo List</h1>

    <div class="add-task">
      <input
        v-model="newTaskText"
        @keyup.enter="addTask"
        placeholder="Add new task"
      >
      <button @click="addTask">Add</button>
    </div>

    <ul class="task-list">
      <li
        v-for="task in tasks"
        :key="task.id"
        :class="{ completed: task.completed }"
      >
        <input
          type="checkbox"
          :checked="task.completed"
          @change="toggleTask(task.id)"
        >
        <span>{{ task.text }}</span>
        <button @click="removeTask(task.id)">Delete</button>
      </li>
    </ul>

    <div class="stats">
      <p>Completed: {{ completedCount }}</p>
      <p>Remaining: {{ remainingCount }}</p>
    </div>
  </div>
</template>

<style scoped>
.todo-app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.add-task {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.add-task input {
  flex: 1;
  padding: 8px;
  font-size: 14px;
}

.task-list {
  list-style: none;
  padding: 0;
}

.task-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.task-list li.completed span {
  text-decoration: line-through;
  opacity: 0.6;
}

.stats {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #eee;
}

.stats p {
  margin: 5px 0;
}
</style>
单文件组件(.vue)将模板、脚本和样式整合到一个文件中。
完整SFC示例:
vue
<script setup>
import { ref, computed } from 'vue'

const tasks = ref([
  { id: 1, text: '学习Vue', completed: false },
  { id: 2, text: '构建应用', completed: false }
])

const newTaskText = ref('')

const completedCount = computed(() =>
  tasks.value.filter(t => t.completed).length
)

const remainingCount = computed(() =>
  tasks.value.filter(t => !t.completed).length
)

function addTask() {
  if (newTaskText.value.trim()) {
    tasks.value.push({
      id: Date.now(),
      text: newTaskText.value,
      completed: false
    })
    newTaskText.value = ''
  }
}

function toggleTask(id) {
  const task = tasks.value.find(t => t.id === id)
  if (task) task.completed = !task.completed
}

function removeTask(id) {
  tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>

<template>
  <div class="todo-app">
    <h1>待办事项列表</h1>

    <div class="add-task">
      <input
        v-model="newTaskText"
        @keyup.enter="addTask"
        placeholder="添加新任务"
      >
      <button @click="addTask">添加</button>
    </div>

    <ul class="task-list">
      <li
        v-for="task in tasks"
        :key="task.id"
        :class="{ completed: task.completed }"
      >
        <input
          type="checkbox"
          :checked="task.completed"
          @change="toggleTask(task.id)"
        >
        <span>{{ task.text }}</span>
        <button @click="removeTask(task.id)">删除</button>
      </li>
    </ul>

    <div class="stats">
      <p>已完成: {{ completedCount }}</p>
      <p>剩余: {{ remainingCount }}</p>
    </div>
  </div>
</template>

<style scoped>
.todo-app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.add-task {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.add-task input {
  flex: 1;
  padding: 8px;
  font-size: 14px;
}

.task-list {
  list-style: none;
  padding: 0;
}

.task-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.task-list li.completed span {
  text-decoration: line-through;
  opacity: 0.6;
}

.stats {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #eee;
}

.stats p {
  margin: 5px 0;
}
</style>

Template Syntax and Directives

模板语法与指令

Vue uses an HTML-based template syntax with special directives.
Text Interpolation:
vue
<template>
  <div>
    <!-- Basic interpolation -->
    <p>{{ message }}</p>

    <!-- JavaScript expressions -->
    <p>{{ count + 1 }}</p>
    <p>{{ ok ? 'YES' : 'NO' }}</p>
    <p>{{ message.split('').reverse().join('') }}</p>

    <!-- Calling functions -->
    <p>{{ formatDate(timestamp) }}</p>
  </div>
</template>
v-bind - Attribute Binding:
vue
<template>
  <!-- Bind attribute -->
  <img v-bind:src="imageUrl" v-bind:alt="imageAlt">

  <!-- Shorthand -->
  <img :src="imageUrl" :alt="imageAlt">

  <!-- Dynamic attribute name -->
  <button :[attributeName]="value">Click</button>

  <!-- Bind multiple attributes -->
  <div v-bind="objectOfAttrs"></div>

  <!-- Class binding -->
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>
  <div :class="[activeClass, errorClass]"></div>
  <div :class="[isActive ? activeClass : '', errorClass]"></div>

  <!-- Style binding -->
  <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
  <div :style="[baseStyles, overridingStyles]"></div>
</template>

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

const imageUrl = ref('/path/to/image.jpg')
const imageAlt = ref('Description')
const isActive = ref(true)
const hasError = ref(false)
const activeClass = ref('active')
const errorClass = ref('text-danger')
const activeColor = ref('red')
const fontSize = ref(14)

const objectOfAttrs = reactive({
  id: 'container',
  class: 'wrapper'
})
</script>
v-on - Event Handling:
vue
<template>
  <!-- Method handler -->
  <button v-on:click="handleClick">Click me</button>

  <!-- Shorthand -->
  <button @click="handleClick">Click me</button>

  <!-- Inline handler -->
  <button @click="count++">Increment</button>

  <!-- Pass arguments -->
  <button @click="handleClick('hello', $event)">Click</button>

  <!-- Event modifiers -->
  <form @submit.prevent="onSubmit">Submit</form>
  <button @click.stop="handleClick">Stop Propagation</button>
  <div @click.self="handleClick">Only Self</div>
  <button @click.once="handleClick">Once</button>

  <!-- Key modifiers -->
  <input @keyup.enter="submit">
  <input @keyup.esc="cancel">
  <input @keyup.ctrl.s="save">

  <!-- Mouse modifiers -->
  <button @click.left="handleLeft">Left Click</button>
  <button @click.right="handleRight">Right Click</button>
  <button @click.middle="handleMiddle">Middle Click</button>
</template>

<script setup>
function handleClick(message, event) {
  console.log(message, event)
}

function onSubmit() {
  console.log('Form submitted')
}
</script>
v-model - Two-Way Binding:
vue
<template>
  <!-- Text input -->
  <input v-model="text">
  <p>{{ text }}</p>

  <!-- Textarea -->
  <textarea v-model="message"></textarea>

  <!-- Checkbox -->
  <input type="checkbox" v-model="checked">

  <!-- Multiple checkboxes -->
  <input type="checkbox" value="Vue" v-model="checkedNames">
  <input type="checkbox" value="React" v-model="checkedNames">
  <input type="checkbox" value="Angular" v-model="checkedNames">
  <p>{{ checkedNames }}</p>

  <!-- Radio -->
  <input type="radio" value="One" v-model="picked">
  <input type="radio" value="Two" v-model="picked">

  <!-- Select -->
  <select v-model="selected">
    <option disabled value="">Please select</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>

  <!-- Multiple select -->
  <select v-model="multiSelected" multiple>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>

  <!-- Modifiers -->
  <input v-model.lazy="text">        <!-- Update on change, not input -->
  <input v-model.number="age">       <!-- Auto typecast to number -->
  <input v-model.trim="message">     <!-- Auto trim whitespace -->

  <!-- Custom component v-model -->
  <CustomInput v-model="searchText" />
</template>

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

const text = ref('')
const message = ref('')
const checked = ref(false)
const checkedNames = ref([])
const picked = ref('')
const selected = ref('')
const multiSelected = ref([])
const age = ref(0)
const searchText = ref('')
</script>
v-if, v-else-if, v-else - Conditional Rendering:
vue
<template>
  <div v-if="type === 'A'">
    Type A
  </div>
  <div v-else-if="type === 'B'">
    Type B
  </div>
  <div v-else-if="type === 'C'">
    Type C
  </div>
  <div v-else>
    Not A, B, or C
  </div>

  <!-- v-if with template (doesn't render wrapper) -->
  <template v-if="ok">
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
  </template>
</template>

<script setup>
import { ref } from 'vue'
const type = ref('A')
const ok = ref(true)
</script>
v-show - Toggle Display:
vue
<template>
  <!-- v-show toggles CSS display property -->
  <h1 v-show="isVisible">Hello!</h1>

  <!-- v-if vs v-show:
       - v-if: truly conditional, destroys/recreates elements
       - v-show: always rendered, toggles display CSS
       - Use v-show for frequent toggles
       - Use v-if for rarely changing conditions
  -->
</template>

<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
v-for - List Rendering:
vue
<template>
  <!-- Array iteration -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </ul>

  <!-- With index -->
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      {{ index }}: {{ item.text }}
    </li>
  </ul>

  <!-- Object iteration -->
  <ul>
    <li v-for="(value, key) in user" :key="key">
      {{ key }}: {{ value }}
    </li>
  </ul>

  <!-- With index for objects -->
  <ul>
    <li v-for="(value, key, index) in user" :key="key">
      {{ index }}. {{ key }}: {{ value }}
    </li>
  </ul>

  <!-- Range -->
  <span v-for="n in 10" :key="n">{{ n }}</span>

  <!-- v-for with v-if (not recommended) -->
  <!-- Use computed instead -->
  <ul>
    <li v-for="item in activeItems" :key="item.id">
      {{ item.text }}
    </li>
  </ul>

  <!-- v-for with template -->
  <template v-for="item in items" :key="item.id">
    <li>{{ item.text }}</li>
    <li class="divider"></li>
  </template>
</template>

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

const items = ref([
  { id: 1, text: 'Learn Vue', active: true },
  { id: 2, text: 'Build app', active: false },
  { id: 3, text: 'Deploy', active: true }
])

const user = ref({
  name: 'John',
  age: 30,
  email: 'john@example.com'
})

const activeItems = computed(() =>
  items.value.filter(item => item.active)
)
</script>
Vue使用基于HTML的模板语法,并带有特殊指令。
文本插值:
vue
<template>
  <div>
    <!-- 基础插值 -->
    <p>{{ message }}</p>

    <!-- JavaScript表达式 -->
    <p>{{ count + 1 }}</p>
    <p>{{ ok ? '是' : '否' }}</p>
    <p>{{ message.split('').reverse().join('') }}</p>

    <!-- 调用函数 -->
    <p>{{ formatDate(timestamp) }}</p>
  </div>
</template>
v-bind - 属性绑定:
vue
<template>
  <!-- 绑定属性 -->
  <img v-bind:src="imageUrl" v-bind:alt="imageAlt">

  <!-- 简写 -->
  <img :src="imageUrl" :alt="imageAlt">

  <!-- 动态属性名 -->
  <button :[attributeName]="value">点击</button>

  <!-- 绑定多个属性 -->
  <div v-bind="objectOfAttrs"></div>

  <!-- 类绑定 -->
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>
  <div :class="[activeClass, errorClass]"></div>
  <div :class="[isActive ? activeClass : '', errorClass]"></div>

  <!-- 样式绑定 -->
  <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
  <div :style="[baseStyles, overridingStyles]"></div>
</template>

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

const imageUrl = ref('/path/to/image.jpg')
const imageAlt = ref('描述')
const isActive = ref(true)
const hasError = ref(false)
const activeClass = ref('active')
const errorClass = ref('text-danger')
const activeColor = ref('red')
const fontSize = ref(14)

const objectOfAttrs = reactive({
  id: 'container',
  class: 'wrapper'
})
</script>
v-on - 事件处理:
vue
<template>
  <!-- 方法处理器 -->
  <button v-on:click="handleClick">点击我</button>

  <!-- 简写 -->
  <button @click="handleClick">点击我</button>

  <!-- 内联处理器 -->
  <button @click="count++">增加</button>

  <!-- 传递参数 -->
  <button @click="handleClick('你好', $event)">点击</button>

  <!-- 事件修饰符 -->
  <form @submit.prevent="onSubmit">提交</form>
  <button @click.stop="handleClick">阻止冒泡</button>
  <div @click.self="handleClick">仅自身触发</div>
  <button @click.once="handleClick">仅触发一次</button>

  <!-- 按键修饰符 -->
  <input @keyup.enter="submit">
  <input @keyup.esc="cancel">
  <input @keyup.ctrl.s="save">

  <!-- 鼠标修饰符 -->
  <button @click.left="handleLeft">左键点击</button>
  <button @click.right="handleRight">右键点击</button>
  <button @click.middle="handleMiddle">中键点击</button>
</template>

<script setup>
function handleClick(message, event) {
  console.log(message, event)
}

function onSubmit() {
  console.log('表单已提交')
}
</script>
v-model - 双向绑定:
vue
<template>
  <!-- 文本输入框 -->
  <input v-model="text">
  <p>{{ text }}</p>

  <!-- 文本域 -->
  <textarea v-model="message"></textarea>

  <!-- 复选框 -->
  <input type="checkbox" v-model="checked">

  <!-- 多个复选框 -->
  <input type="checkbox" value="Vue" v-model="checkedNames">
  <input type="checkbox" value="React" v-model="checkedNames">
  <input type="checkbox" value="Angular" v-model="checkedNames">
  <p>{{ checkedNames }}</p>

  <!-- 单选框 -->
  <input type="radio" value="选项1" v-model="picked">
  <input type="radio" value="选项2" v-model="picked">

  <!-- 下拉选择框 -->
  <select v-model="selected">
    <option disabled value="">请选择</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>

  <!-- 多选下拉框 -->
  <select v-model="multiSelected" multiple>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>

  <!-- 修饰符 -->
  <input v-model.lazy="text">        <!-- 在change事件而非input事件时更新 -->
  <input v-model.number="age">       <!-- 自动转换为数字类型 -->
  <input v-model.trim="message">     <!-- 自动去除首尾空格 -->

  <!-- 自定义组件的v-model -->
  <CustomInput v-model="searchText" />
</template>

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

const text = ref('')
const message = ref('')
const checked = ref(false)
const checkedNames = ref([])
const picked = ref('')
const selected = ref('')
const multiSelected = ref([])
const age = ref(0)
const searchText = ref('')
</script>
v-if, v-else-if, v-else - 条件渲染:
vue
<template>
  <div v-if="type === 'A'">
    类型A
  </div>
  <div v-else-if="type === 'B'">
    类型B
  </div>
  <div v-else-if="type === 'C'">
    类型C
  </div>
  <div v-else>
    不是A、B或C
  </div>

  <!-- 与template配合使用的v-if(不会渲染包裹元素) -->
  <template v-if="ok">
    <h1>标题</h1>
    <p>段落1</p>
    <p>段落2</p>
  </template>
</template>

<script setup>
import { ref } from 'vue'
const type = ref('A')
const ok = ref(true)
</script>
v-show - 切换显示:
vue
<template>
  <!-- v-show切换CSS的display属性 -->
  <h1 v-show="isVisible">你好!</h1>

  <!-- v-if vs v-show:
       - v-if: 真正的条件渲染,会销毁/重建元素
       - v-show: 元素始终被渲染,仅切换CSS的display属性
       - 频繁切换时使用v-show
       - 条件很少变化时使用v-if
  -->
</template>

<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
v-for - 列表渲染:
vue
<template>
  <!-- 数组遍历 -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </ul>

  <!-- 带索引 -->
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      {{ index }}: {{ item.text }}
    </li>
  </ul>

  <!-- 对象遍历 -->
  <ul>
    <li v-for="(value, key) in user" :key="key">
      {{ key }}: {{ value }}
    </li>
  </ul>

  <!-- 对象遍历带索引 -->
  <ul>
    <li v-for="(value, key, index) in user" :key="key">
      {{ index }}. {{ key }}: {{ value }}
    </li>
  </ul>

  <!-- 数值范围 -->
  <span v-for="n in 10" :key="n">{{ n }}</span>

  <!-- v-for与v-if配合使用(不推荐) -->
  <!-- 推荐使用计算属性替代 -->
  <ul>
    <li v-for="item in activeItems" :key="item.id">
      {{ item.text }}
    </li>
  </ul>

  <!-- 与template配合使用的v-for -->
  <template v-for="item in items" :key="item.id">
    <li>{{ item.text }}</li>
    <li class="divider"></li>
  </template>
</template>

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

const items = ref([
  { id: 1, text: '学习Vue', active: true },
  { id: 2, text: '构建应用', active: false },
  { id: 3, text: '部署', active: true }
])

const user = ref({
  name: 'John',
  age: 30,
  email: 'john@example.com'
})

const activeItems = computed(() =>
  items.value.filter(item => item.active)
)
</script>

Lifecycle Hooks

生命周期钩子

Lifecycle hooks let you run code at specific stages of a component's lifecycle.
Lifecycle Hooks in Composition API:
vue
<script setup>
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated
} from 'vue'

const count = ref(0)

// Before component is mounted
onBeforeMount(() => {
  console.log('Before mount')
})

// After component is mounted (DOM available)
onMounted(() => {
  console.log('Mounted')
  // Good for: API calls, DOM manipulation, timers
  fetchData()
  setupEventListeners()
})

// Before component updates (reactive data changed)
onBeforeUpdate(() => {
  console.log('Before update')
})

// After component updates
onUpdated(() => {
  console.log('Updated')
  // Good for: DOM operations after data changes
})

// Before component is unmounted
onBeforeUnmount(() => {
  console.log('Before unmount')
  // Good for: Cleanup
})

// After component is unmounted
onUnmounted(() => {
  console.log('Unmounted')
  // Good for: Cleanup timers, event listeners
  clearInterval(interval)
  removeEventListeners()
})

// Error handling
onErrorCaptured((err, instance, info) => {
  console.error('Error captured:', err, info)
  return false // Prevent error from propagating
})

// For components in <keep-alive>
onActivated(() => {
  console.log('Component activated')
})

onDeactivated(() => {
  console.log('Component deactivated')
})
</script>
Lifecycle Diagram:
Creation Phase:
  setup() → onBeforeMount() → onMounted()

Update Phase (when reactive data changes):
  onBeforeUpdate() → onUpdated()

Destruction Phase:
  onBeforeUnmount() → onUnmounted()
生命周期钩子允许你在组件生命周期的特定阶段运行代码。
Composition API中的生命周期钩子:
vue
<script setup>
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated
} from 'vue'

const count = ref(0)

// 组件挂载前
onBeforeMount(() => {
  console.log('挂载前')
})

// 组件挂载后(DOM可用)
onMounted(() => {
  console.log('已挂载')
  // 适用于:API调用、DOM操作、定时器
  fetchData()
  setupEventListeners()
})

// 组件更新前(响应式数据已变化)
onBeforeUpdate(() => {
  console.log('更新前')
})

// 组件更新后
onUpdated(() => {
  console.log('已更新')
  // 适用于:数据变化后的DOM操作
})

// 组件卸载前
onBeforeUnmount(() => {
  console.log('卸载前')
  // 适用于:清理工作
})

// 组件卸载后
onUnmounted(() => {
  console.log('已卸载')
  // 适用于:清理定时器、事件监听器
  clearInterval(interval)
  removeEventListeners()
})

// 错误处理
onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误:', err, info)
  return false // 阻止错误传播
})

// 适用于<keep-alive>中的组件
onActivated(() => {
  console.log('组件已激活')
})

onDeactivated(() => {
  console.log('组件已失活')
})
</script>
生命周期示意图:
创建阶段:
  setup() → onBeforeMount() → onMounted()

更新阶段(响应式数据变化时):
  onBeforeUpdate() → onUpdated()

销毁阶段:
  onBeforeUnmount() → onUnmounted()

Component Communication

组件通信

Props (Parent to Child)

Props(父组件到子组件)

vue
<!-- Child Component: UserCard.vue -->
<script setup>
// Define props with types
const props = defineProps({
  name: String,
  age: Number,
  email: String,
  isActive: {
    type: Boolean,
    default: true
  },
  roles: {
    type: Array,
    default: () => []
  },
  profile: {
    type: Object,
    required: true,
    validator: (value) => {
      return value.id && value.name
    }
  }
})

// Props are reactive and can be used in computed
import { computed } from 'vue'

const displayName = computed(() =>
  `${props.name} (${props.age})`
)
</script>

<template>
  <div class="user-card">
    <h3>{{ displayName }}</h3>
    <p>{{ email }}</p>
    <span v-if="isActive">Active</span>
    <ul>
      <li v-for="role in roles" :key="role">{{ role }}</li>
    </ul>
  </div>
</template>

<!-- Parent Component -->
<script setup>
import UserCard from './UserCard.vue'
import { reactive } from 'vue'

const user = reactive({
  name: 'John Doe',
  age: 30,
  email: 'john@example.com',
  isActive: true,
  roles: ['admin', 'editor'],
  profile: {
    id: 1,
    name: 'John'
  }
})
</script>

<template>
  <UserCard
    :name="user.name"
    :age="user.age"
    :email="user.email"
    :is-active="user.isActive"
    :roles="user.roles"
    :profile="user.profile"
  />

  <!-- Or pass entire object -->
  <UserCard v-bind="user" />
</template>
vue
<!-- 子组件: UserCard.vue -->
<script setup>
// 定义带类型的Props
const props = defineProps({
  name: String,
  age: Number,
  email: String,
  isActive: {
    type: Boolean,
    default: true
  },
  roles: {
    type: Array,
    default: () => []
  },
  profile: {
    type: Object,
    required: true,
    validator: (value) => {
      return value.id && value.name
    }
  }
})

// Props是响应式的,可用于计算属性
import { computed } from 'vue'

const displayName = computed(() =>
  `${props.name} (${props.age})`
)
</script>

<template>
  <div class="user-card">
    <h3>{{ displayName }}</h3>
    <p>{{ email }}</p>
    <span v-if="isActive">活跃</span>
    <ul>
      <li v-for="role in roles" :key="role">{{ role }}</li>
    </ul>
  </div>
</template>

<!-- 父组件 -->
<script setup>
import UserCard from './UserCard.vue'
import { reactive } from 'vue'

const user = reactive({
  name: 'John Doe',
  age: 30,
  email: 'john@example.com',
  isActive: true,
  roles: ['管理员', '编辑'],
  profile: {
    id: 1,
    name: 'John'
  }
})
</script>

<template>
  <UserCard
    :name="user.name"
    :age="user.age"
    :email="user.email"
    :is-active="user.isActive"
    :roles="user.roles"
    :profile="user.profile"
  />

  <!-- 或者传递整个对象 -->
  <UserCard v-bind="user" />
</template>

Emits (Child to Parent)

Emits(子组件到父组件)

vue
<!-- Child Component: TodoItem.vue -->
<script setup>
const props = defineProps({
  todo: {
    type: Object,
    required: true
  }
})

// Define emits
const emit = defineEmits(['toggle', 'delete', 'update'])

// Or with validation
const emit = defineEmits({
  toggle: (id) => {
    if (typeof id === 'number') {
      return true
    } else {
      console.warn('Invalid toggle event payload')
      return false
    }
  },
  delete: (id) => typeof id === 'number',
  update: (id, text) => {
    return typeof id === 'number' && typeof text === 'string'
  }
})

function handleToggle() {
  emit('toggle', props.todo.id)
}

function handleDelete() {
  emit('delete', props.todo.id)
}

function handleUpdate(newText) {
  emit('update', props.todo.id, newText)
}
</script>

<template>
  <div class="todo-item">
    <input
      type="checkbox"
      :checked="todo.completed"
      @change="handleToggle"
    >
    <span>{{ todo.text }}</span>
    <button @click="handleDelete">Delete</button>
  </div>
</template>

<!-- Parent Component -->
<script setup>
import TodoItem from './TodoItem.vue'
import { ref } from 'vue'

const todos = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: false }
])

function toggleTodo(id) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.completed = !todo.completed
}

function deleteTodo(id) {
  todos.value = todos.value.filter(t => t.id !== id)
}

function updateTodo(id, text) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.text = text
}
</script>

<template>
  <div>
    <TodoItem
      v-for="todo in todos"
      :key="todo.id"
      :todo="todo"
      @toggle="toggleTodo"
      @delete="deleteTodo"
      @update="updateTodo"
    />
  </div>
</template>
vue
<!-- 子组件: TodoItem.vue -->
<script setup>
const props = defineProps({
  todo: {
    type: Object,
    required: true
  }
})

// 定义自定义事件
const emit = defineEmits(['toggle', 'delete', 'update'])

// 或者带验证的定义
const emit = defineEmits({
  toggle: (id) => {
    if (typeof id === 'number') {
      return true
    } else {
      console.warn('无效的toggle事件载荷')
      return false
    }
  },
  delete: (id) => typeof id === 'number',
  update: (id, text) => {
    return typeof id === 'number' && typeof text === 'string'
  }
})

function handleToggle() {
  emit('toggle', props.todo.id)
}

function handleDelete() {
  emit('delete', props.todo.id)
}

function handleUpdate(newText) {
  emit('update', props.todo.id, newText)
}
</script>

<template>
  <div class="todo-item">
    <input
      type="checkbox"
      :checked="todo.completed"
      @change="handleToggle"
    >
    <span>{{ todo.text }}</span>
    <button @click="handleDelete">删除</button>
  </div>
</template>

<!-- 父组件 -->
<script setup>
import TodoItem from './TodoItem.vue'
import { ref } from 'vue'

const todos = ref([
  { id: 1, text: '学习Vue', completed: false },
  { id: 2, text: '构建应用', completed: false }
])

function toggleTodo(id) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.completed = !todo.completed
}

function deleteTodo(id) {
  todos.value = todos.value.filter(t => t.id !== id)
}

function updateTodo(id, text) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.text = text
}
</script>

<template>
  <div>
    <TodoItem
      v-for="todo in todos"
      :key="todo.id"
      :todo="todo"
      @toggle="toggleTodo"
      @delete="deleteTodo"
      @update="updateTodo"
    />
  </div>
</template>

Provide/Inject (Ancestor to Descendant)

Provide/Inject(祖先组件到后代组件)

vue
<!-- Ancestor Component: App.vue -->
<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const theme = ref('dark')
const userSettings = ref({
  fontSize: 14,
  language: 'en'
})

// Provide to all descendants
provide('theme', theme)
provide('userSettings', userSettings)

// Provide with readonly to prevent modifications
import { readonly } from 'vue'
provide('theme', readonly(theme))

// Provide functions
function updateTheme(newTheme) {
  theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>

<template>
  <div>
    <ChildComponent />
  </div>
</template>

<!-- Descendant Component (any level deep) -->
<script setup>
import { inject } from 'vue'

// Inject provided values
const theme = inject('theme')
const userSettings = inject('userSettings')
const updateTheme = inject('updateTheme')

// With default value
const locale = inject('locale', 'en')

// With factory function for default
const settings = inject('settings', () => ({ mode: 'light' }))
</script>

<template>
  <div :class="`theme-${theme}`">
    <p>Font size: {{ userSettings.fontSize }}</p>
    <button @click="updateTheme('light')">Light Theme</button>
    <button @click="updateTheme('dark')">Dark Theme</button>
  </div>
</template>
vue
<!-- 祖先组件: App.vue -->
<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const theme = ref('dark')
const userSettings = ref({
  fontSize: 14,
  language: 'zh-CN'
})

// 提供给所有后代组件
provide('theme', theme)
provide('userSettings', userSettings)

// 提供只读值以防止修改
import { readonly } from 'vue'
provide('theme', readonly(theme))

// 提供函数
function updateTheme(newTheme) {
  theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>

<template>
  <div>
    <ChildComponent />
  </div>
</template>

<!-- 后代组件(任意层级) -->
<script setup>
import { inject } from 'vue'

// 注入提供的值
const theme = inject('theme')
const userSettings = inject('userSettings')
const updateTheme = inject('updateTheme')

// 带默认值
const locale = inject('locale', 'zh-CN')

// 带工厂函数的默认值
const settings = inject('settings', () => ({ mode: 'light' }))
</script>

<template>
  <div :class="`theme-${theme}`">
    <p>字体大小: {{ userSettings.fontSize }}</p>
    <button @click="updateTheme('light')">浅色主题</button>
    <button @click="updateTheme('dark')">深色主题</button>
  </div>
</template>

Slots (Parent Content Distribution)

Slots(父组件内容分发)

vue
<!-- Child Component: Card.vue -->
<script setup>
const props = defineProps({
  title: String
})
</script>

<template>
  <div class="card">
    <!-- Named slot with fallback -->
    <header>
      <slot name="header">
        <h2>{{ title }}</h2>
      </slot>
    </header>

    <!-- Default slot -->
    <main>
      <slot>
        <p>Default content</p>
      </slot>
    </main>

    <!-- Named slot -->
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<!-- Parent Component -->
<template>
  <Card title="My Card">
    <template #header>
      <h1>Custom Header</h1>
    </template>

    <p>Main content goes here</p>

    <template #footer>
      <button>Action</button>
    </template>
  </Card>
</template>

<!-- Scoped Slots: Child exposes data to parent -->
<!-- Child Component: TodoList.vue -->
<script setup>
import { ref } from 'vue'

const todos = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: true }
])
</script>

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <!-- Expose todo to parent via slot props -->
      <slot :todo="todo" :index="todo.id"></slot>
    </li>
  </ul>
</template>

<!-- Parent Component -->
<template>
  <TodoList>
    <!-- Access slot props -->
    <template #default="{ todo, index }">
      <span :class="{ completed: todo.completed }">
        {{ index }}. {{ todo.text }}
      </span>
    </template>
  </TodoList>

  <!-- Shorthand for default slot -->
  <TodoList v-slot="{ todo }">
    <span>{{ todo.text }}</span>
  </TodoList>
</template>
vue
<!-- 子组件: Card.vue -->
<script setup>
const props = defineProps({
  title: String
})
</script>

<template>
  <div class="card">
    <!-- 具名插槽,带回退内容 -->
    <header>
      <slot name="header">
        <h2>{{ title }}</h2>
      </slot>
    </header>

    <!-- 默认插槽 -->
    <main>
      <slot>
        <p>默认内容</p>
      </slot>
    </main>

    <!-- 具名插槽 -->
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<!-- 父组件 -->
<template>
  <Card title="我的卡片">
    <template #header>
      <h1>自定义标题</h1>
    </template>

    <p>主内容区域</p>

    <template #footer>
      <button>操作按钮</button>
    </template>
  </Card>
</template>

<!-- 作用域插槽:子组件向父组件暴露数据 -->
<!-- 子组件: TodoList.vue -->
<script setup>
import { ref } from 'vue'

const todos = ref([
  { id: 1, text: '学习Vue', completed: false },
  { id: 2, text: '构建应用', completed: true }
])
</script>

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <!-- 通过插槽属性向父组件暴露todo -->
      <slot :todo="todo" :index="todo.id"></slot>
    </li>
  </ul>
</template>

<!-- 父组件 -->
<template>
  <TodoList>
    <!-- 访问插槽属性 -->
    <template #default="{ todo, index }">
      <span :class="{ completed: todo.completed }">
        {{ index }}. {{ todo.text }}
      </span>
    </template>
  </TodoList>

  <!-- 默认插槽的简写 -->
  <TodoList v-slot="{ todo }">
    <span>{{ todo.text }}</span>
  </TodoList>
</template>

State Management Patterns

状态管理模式

Local Component State

组件本地状态

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

// Simple counter state
const count = ref(0)

function increment() {
  count.value++
}

// Form state
const formData = reactive({
  name: '',
  email: '',
  message: ''
})

function submitForm() {
  console.log('Submitting:', formData)
}

function resetForm() {
  formData.name = ''
  formData.email = ''
  formData.message = ''
}
</script>
vue
<script setup>
import { ref, reactive } from 'vue'

// 简单计数器状态
const count = ref(0)

function increment() {
  count.value++
}

// 表单状态
const formData = reactive({
  name: '',
  email: '',
  message: ''
})

function submitForm() {
  console.log('提交:', formData)
}

function resetForm() {
  formData.name = ''
  formData.email = ''
  formData.message = ''
}
</script>

Composables (Reusable State Logic)

可组合函数(可复用状态逻辑)

javascript
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  const doubled = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubled,
    increment,
    decrement,
    reset
  }
}

// Usage in component
<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, doubled, increment, decrement, reset } = useCounter(10)
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>
Mouse Position Composable:
javascript
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return { x, y }
}

// Usage
<script setup>
import { useMouse } from '@/composables/useMouse'

const { x, y } = useMouse()
</script>

<template>
  <p>Mouse position: {{ x }}, {{ y }}</p>
</template>
Fetch Composable:
javascript
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  watchEffect(async () => {
    loading.value = true
    data.value = null
    error.value = null

    const urlValue = toValue(url)

    try {
      const response = await fetch(urlValue)
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

// Usage
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'

const userId = ref(1)
const url = computed(() => `https://api.example.com/users/${userId.value}`)

const { data: user, error, loading } = useFetch(url)
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else-if="user">{{ user.name }}</div>
</template>
javascript
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  const doubled = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubled,
    increment,
    decrement,
    reset
  }
}

// 在组件中使用
<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, doubled, increment, decrement, reset } = useCounter(10)
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>翻倍值: {{ doubled }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">重置</button>
  </div>
</template>
鼠标位置可组合函数:
javascript
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return { x, y }
}

// 使用
<script setup>
import { useMouse } from '@/composables/useMouse'

const { x, y } = useMouse()
</script>

<template>
  <p>鼠标位置: {{ x }}, {{ y }}</p>
</template>
数据获取可组合函数:
javascript
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  watchEffect(async () => {
    loading.value = true
    data.value = null
    error.value = null

    const urlValue = toValue(url)

    try {
      const response = await fetch(urlValue)
      if (!response.ok) throw new Error(`HTTP错误!状态码: ${response.status}`)
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

// 使用
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'

const userId = ref(1)
const url = computed(() => `https://api.example.com/users/${userId.value}`)

const { data: user, error, loading } = useFetch(url)
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">错误: {{ error.message }}</div>
  <div v-else-if="user">{{ user.name }}</div>
</template>

Global State with Pinia

使用Pinia进行全局状态管理

javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Option 1: Setup Stores (Composition API style)
export const useCounterStore = defineStore('counter', () => {
  // State
  const count = ref(0)
  const name = ref('Counter')

  // Getters
  const doubleCount = computed(() => count.value * 2)

  // Actions
  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  async function incrementAsync() {
    await new Promise(resolve => setTimeout(resolve, 1000))
    count.value++
  }

  return {
    count,
    name,
    doubleCount,
    increment,
    decrement,
    incrementAsync
  }
})

// Option 2: Options Stores
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter'
  }),

  getters: {
    doubleCount: (state) => state.count * 2,
    doublePlusOne() {
      return this.doubleCount + 1
    }
  },

  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

// Usage in component
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()

// Extract reactive state (preserves reactivity)
const { count, name, doubleCount } = storeToRefs(counterStore)

// Actions can be destructured directly
const { increment, decrement } = counterStore
</script>

<template>
  <div>
    <p>{{ name }}: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>
javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 方式1: Setup Stores(Composition API风格)
export const useCounterStore = defineStore('counter', () => {
  // 状态
  const count = ref(0)
  const name = ref('计数器')

  // Getter
  const doubleCount = computed(() => count.value * 2)

  // Action
  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  async function incrementAsync() {
    await new Promise(resolve => setTimeout(resolve, 1000))
    count.value++
  }

  return {
    count,
    name,
    doubleCount,
    increment,
    decrement,
    incrementAsync
  }
})

// 方式2: Options Stores
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: '计数器'
  }),

  getters: {
    doubleCount: (state) => state.count * 2,
    doublePlusOne() {
      return this.doubleCount + 1
    }
  },

  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

// 在组件中使用
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()

// 提取响应式状态(保留响应式)
const { count, name, doubleCount } = storeToRefs(counterStore)

// Action可直接解构
const { increment, decrement } = counterStore
</script>

<template>
  <div>
    <p>{{ name }}: {{ count }}</p>
    <p>翻倍值: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

Routing with Vue Router

使用Vue Router进行路由

Router Setup:
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: About
  },
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/User.vue'), // Lazy loading
    props: true // Pass route params as props
  },
  {
    path: '/posts',
    name: 'posts',
    component: () => import('@/views/Posts.vue'),
    children: [
      {
        path: ':id',
        name: 'post-detail',
        component: () => import('@/views/PostDetail.vue')
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Navigation guards
router.beforeEach((to, from, next) => {
  // Check authentication, etc.
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

export default router
Using Router in Components:
vue
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'

const router = useRouter()
const route = useRoute()

// Access route params
const userId = computed(() => route.params.id)

// Access query params
const searchQuery = computed(() => route.query.q)

// Programmatic navigation
function goToHome() {
  router.push('/')
}

function goToUser(id) {
  router.push({ name: 'user', params: { id } })
}

function goBack() {
  router.back()
}

function goToSearch(query) {
  router.push({ path: '/search', query: { q: query } })
}
</script>

<template>
  <div>
    <!-- Declarative navigation -->
    <router-link to="/">Home</router-link>
    <router-link :to="{ name: 'about' }">About</router-link>
    <router-link :to="`/user/${userId}`">User Profile</router-link>

    <!-- Active class styling -->
    <router-link
      to="/dashboard"
      active-class="active"
      exact-active-class="exact-active"
    >
      Dashboard
    </router-link>

    <!-- Programmatic navigation -->
    <button @click="goToHome">Go Home</button>
    <button @click="goToUser(123)">View User 123</button>
    <button @click="goBack">Go Back</button>

    <!-- Render matched component -->
    <router-view />

    <!-- Named views -->
    <router-view name="sidebar" />
    <router-view name="main" />
  </div>
</template>
路由配置:
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: About
  },
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/User.vue'), // 懒加载
    props: true // 将路由参数作为Props传递
  },
  {
    path: '/posts',
    name: 'posts',
    component: () => import('@/views/Posts.vue'),
    children: [
      {
        path: ':id',
        name: 'post-detail',
        component: () => import('@/views/PostDetail.vue')
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 导航守卫
router.beforeEach((to, from, next) => {
  // 检查认证等
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

export default router
在组件中使用路由:
vue
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'

const router = useRouter()
const route = useRoute()

// 访问路由参数
const userId = computed(() => route.params.id)

// 访问查询参数
const searchQuery = computed(() => route.query.q)

// 编程式导航
function goToHome() {
  router.push('/')
}

function goToUser(id) {
  router.push({ name: 'user', params: { id } })
}

function goBack() {
  router.back()
}

function goToSearch(query) {
  router.push({ path: '/search', query: { q: query } })
}
</script>

<template>
  <div>
    <!-- 声明式导航 -->
    <router-link to="/">首页</router-link>
    <router-link :to="{ name: 'about' }">关于</router-link>
    <router-link :to="`/user/${userId}`">用户资料</router-link>

    <!-- 激活类样式 -->
    <router-link
      to="/dashboard"
      active-class="active"
      exact-active-class="exact-active"
    >
      仪表盘
    </router-link>

    <!-- 编程式导航 -->
    <button @click="goToHome">返回首页</button>
    <button @click="goToUser(123)">查看用户123</button>
    <button @click="goBack">返回上一页</button>

    <!-- 渲染匹配的组件 -->
    <router-view />

    <!-- 命名视图 -->
    <router-view name="sidebar" />
    <router-view name="main" />
  </div>
</template>

Advanced Features

高级特性

Teleport

Teleport

Move content to a different location in the DOM.
vue
<script setup>
import { ref } from 'vue'

const showModal = ref(false)
</script>

<template>
  <div class="app">
    <h1>My App</h1>
    <button @click="showModal = true">Open Modal</button>

    <!-- Teleport modal to body -->
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <div class="modal-content">
          <h2>Modal Title</h2>
          <p>Modal content</p>
          <button @click="showModal = false">Close</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<style>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
}
</style>
将内容移动到DOM中的不同位置。
vue
<script setup>
import { ref } from 'vue'

const showModal = ref(false)
</script>

<template>
  <div class="app">
    <h1>我的应用</h1>
    <button @click="showModal = true">打开模态框</button>

    <!-- 将模态框Teleport到body -->
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <div class="modal-content">
          <h2>模态框标题</h2>
          <p>模态框内容</p>
          <button @click="showModal = false">关闭</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<style>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
}
</style>

Suspense

Suspense

Handle async components with loading states.
vue
<!-- Async component -->
<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>

<template>
  <div>{{ data }}</div>
</template>

<!-- Parent using Suspense -->
<template>
  <Suspense>
    <!-- Component with async setup -->
    <template #default>
      <AsyncComponent />
    </template>

    <!-- Loading state -->
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<!-- Error handling with Suspense -->
<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  return true
})
</script>

<template>
  <div v-if="error">Error: {{ error.message }}</div>
  <Suspense v-else>
    <AsyncComponent />
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
处理带有加载状态的异步组件。
vue
<!-- 异步组件 -->
<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>

<template>
  <div>{{ data }}</div>
</template>

<!-- 使用Suspense的父组件 -->
<template>
  <Suspense>
    <!-- 带有异步setup的组件 -->
    <template #default>
      <AsyncComponent />
    </template>

    <!-- 加载状态 -->
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

<!-- 结合Suspense的错误处理 -->
<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  return true
})
</script>

<template>
  <div v-if="error">错误: {{ error.message }}</div>
  <Suspense v-else>
    <AsyncComponent />
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

Transitions

过渡动画

Animate elements entering/leaving the DOM.
vue
<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<template>
  <button @click="show = !show">Toggle</button>

  <!-- Basic transition -->
  <Transition>
    <p v-if="show">Hello</p>
  </Transition>

  <!-- Named transition -->
  <Transition name="fade">
    <p v-if="show">Fade transition</p>
  </Transition>

  <!-- Custom classes -->
  <Transition
    enter-active-class="animate__animated animate__fadeIn"
    leave-active-class="animate__animated animate__fadeOut"
  >
    <p v-if="show">Custom animation</p>
  </Transition>

  <!-- List transitions -->
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </TransitionGroup>
</template>

<style>
/* Transition classes */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* List transitions */
.list-enter-active,
.list-leave-active {
  transition: all 0.3s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.list-move {
  transition: transform 0.3s ease;
}
</style>
为进入/离开DOM的元素添加动画。
vue
<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<template>
  <button @click="show = !show">切换</button>

  <!-- 基础过渡 -->
  <Transition>
    <p v-if="show">你好</p>
  </Transition>

  <!-- 命名过渡 -->
  <Transition name="fade">
    <p v-if="show">淡入淡出过渡</p>
  </Transition>

  <!-- 自定义类 -->
  <Transition
    enter-active-class="animate__animated animate__fadeIn"
    leave-active-class="animate__animated animate__fadeOut"
  >
    <p v-if="show">自定义动画</p>
  </Transition>

  <!-- 列表过渡 -->
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </TransitionGroup>
</template>

<style>
/* 过渡类 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* 列表过渡 */
.list-enter-active,
.list-leave-active {
  transition: all 0.3s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.list-move {
  transition: transform 0.3s ease;
}
</style>

Custom Directives

自定义指令

Create custom directives for DOM manipulation.
javascript
// directives/focus.js
export const vFocus = {
  mounted(el) {
    el.focus()
  }
}

// directives/click-outside.js
export const vClickOutside = {
  mounted(el, binding) {
    el.clickOutsideEvent = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el.clickOutsideEvent)
  },
  unmounted(el) {
    document.removeEventListener('click', el.clickOutsideEvent)
  }
}

// Usage in component
<script setup>
import { vFocus } from '@/directives/focus'
import { vClickOutside } from '@/directives/click-outside'
import { ref } from 'vue'

const show = ref(false)

function closeDropdown() {
  show.value = false
}
</script>

<template>
  <!-- Auto-focus input -->
  <input v-focus type="text">

  <!-- Click outside to close -->
  <div v-click-outside="closeDropdown">
    <button @click="show = !show">Toggle</button>
    <div v-if="show">Dropdown content</div>
  </div>
</template>
创建用于DOM操作的自定义指令。
javascript
// directives/focus.js
export const vFocus = {
  mounted(el) {
    el.focus()
  }
}

// directives/click-outside.js
export const vClickOutside = {
  mounted(el, binding) {
    el.clickOutsideEvent = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el.clickOutsideEvent)
  },
  unmounted(el) {
    document.removeEventListener('click', el.clickOutsideEvent)
  }
}

// 在组件中使用
<script setup>
import { vFocus } from '@/directives/focus'
import { vClickOutside } from '@/directives/click-outside'
import { ref } from 'vue'

const show = ref(false)

function closeDropdown() {
  show.value = false
}
</script>

<template>
  <!-- 自动聚焦输入框 -->
  <input v-focus type="text">

  <!-- 点击外部关闭 -->
  <div v-click-outside="closeDropdown">
    <button @click="show = !show">切换</button>
    <div v-if="show">下拉菜单内容</div>
  </div>
</template>

Performance Optimization

性能优化

Computed vs Methods

Computed vs 方法

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

const items = ref([1, 2, 3, 4, 5])

// Computed: cached, only re-runs when dependencies change
const total = computed(() => {
  console.log('Computing total')
  return items.value.reduce((sum, n) => sum + n, 0)
})

// Method: runs on every render
function getTotal() {
  console.log('Getting total')
  return items.value.reduce((sum, n) => sum + n, 0)
}
</script>

<template>
  <!-- Computed is called once and cached -->
  <p>{{ total }}</p>
  <p>{{ total }}</p>

  <!-- Method is called twice -->
  <p>{{ getTotal() }}</p>
  <p>{{ getTotal() }}</p>
</template>
vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([1, 2, 3, 4, 5])

// Computed: 缓存,仅在依赖变化时重新计算
const total = computed(() => {
  console.log('计算总和')
  return items.value.reduce((sum, n) => sum + n, 0)
})

// 方法:每次渲染都会运行
function getTotal() {
  console.log('获取总和')
  return items.value.reduce((sum, n) => sum + n, 0)
}
</script>

<template>
  <!-- Computed仅调用一次并缓存 -->
  <p>{{ total }}</p>
  <p>{{ total }}</p>

  <!-- 方法会调用两次 -->
  <p>{{ getTotal() }}</p>
  <p>{{ getTotal() }}</p>
</template>

v-once and v-memo

v-once和v-memo

vue
<template>
  <!-- Render once, never update -->
  <div v-once>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>

  <!-- Memoize based on dependencies -->
  <div v-memo="[count, message]">
    <p>{{ count }}</p>
    <p>{{ message }}</p>
    <!-- Only re-renders when count or message changes -->
  </div>

  <!-- Useful for long lists -->
  <div
    v-for="item in list"
    :key="item.id"
    v-memo="[item.selected]"
  >
    <!-- Only re-renders when item.selected changes -->
    {{ item.name }}
  </div>
</template>
vue
<template>
  <!-- 仅渲染一次,永不更新 -->
  <div v-once>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>

  <!-- 根据依赖缓存 -->
  <div v-memo="[count, message]">
    <p>{{ count }}</p>
    <p>{{ message }}</p>
    <!-- 仅在count或message变化时重新渲染 -->
  </div>

  <!-- 对长列表很有用 -->
  <div
    v-for="item in list"
    :key="item.id"
    v-memo="[item.selected]"
  >
    <!-- 仅在item.selected变化时重新渲染 -->
    {{ item.name }}
  </div>
</template>

Lazy Loading Components

懒加载组件

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

// Lazy load component
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// With loading and error components
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
})
</script>

<template>
  <Suspense>
    <HeavyComponent />
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
vue
<script setup>
import { defineAsyncComponent } from 'vue'

// 懒加载组件
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// 带加载和错误组件
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
})
</script>

<template>
  <Suspense>
    <HeavyComponent />
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

Virtual Scrolling

虚拟滚动

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

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: `Item ${i}`
})))

const containerHeight = 400
const itemHeight = 50
const visibleCount = Math.ceil(containerHeight / itemHeight)

const scrollTop = ref(0)

const startIndex = computed(() =>
  Math.floor(scrollTop.value / itemHeight)
)

const endIndex = computed(() =>
  Math.min(startIndex.value + visibleCount + 1, items.value.length)
)

const visibleItems = computed(() =>
  items.value.slice(startIndex.value, endIndex.value)
)

const offsetY = computed(() =>
  startIndex.value * itemHeight
)

const totalHeight = computed(() =>
  items.value.length * itemHeight
)

function handleScroll(event) {
  scrollTop.value = event.target.scrollTop
}
</script>

<template>
  <div
    class="virtual-scroll-container"
    :style="{ height: `${containerHeight}px`, overflow: 'auto' }"
    @scroll="handleScroll"
  >
    <div :style="{ height: `${totalHeight}px`, position: 'relative' }">
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <div
          v-for="item in visibleItems"
          :key="item.id"
          :style="{ height: `${itemHeight}px` }"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>
vue
<script setup>
import { ref, computed } from 'vue'

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: `条目 ${i}`
})))

const containerHeight = 400
const itemHeight = 50
const visibleCount = Math.ceil(containerHeight / itemHeight)

const scrollTop = ref(0)

const startIndex = computed(() =>
  Math.floor(scrollTop.value / itemHeight)
)

const endIndex = computed(() =>
  Math.min(startIndex.value + visibleCount + 1, items.value.length)
)

const visibleItems = computed(() =>
  items.value.slice(startIndex.value, endIndex.value)
)

const offsetY = computed(() =>
  startIndex.value * itemHeight
)

const totalHeight = computed(() =>
  items.value.length * itemHeight
)

function handleScroll(event) {
  scrollTop.value = event.target.scrollTop
}
</script>

<template>
  <div
    class="virtual-scroll-container"
    :style="{ height: `${containerHeight}px`, overflow: 'auto' }"
    @scroll="handleScroll"
  >
    <div :style="{ height: `${totalHeight}px`, position: 'relative' }">
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <div
          v-for="item in visibleItems"
          :key="item.id"
          :style="{ height: `${itemHeight}px` }"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

TypeScript Integration

TypeScript集成

Basic Setup

基础配置

typescript
// Component with TypeScript
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'

// Type annotations
const count: Ref<number> = ref(0)
const message = ref<string>('Hello')

// Interface for objects
interface User {
  id: number
  name: string
  email: string
}

const user = ref<User>({
  id: 1,
  name: 'John',
  email: 'john@example.com'
})

// Props with types
interface Props {
  title: string
  count?: number
  user: User
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})

// Emits with types
interface Emits {
  (e: 'update', value: number): void
  (e: 'delete', id: number): void
}

const emit = defineEmits<Emits>()

// Computed with type inference
const doubled = computed(() => props.count * 2)

// Typed function
function updateUser(id: number, name: string): void {
  user.value.id = id
  user.value.name = name
}
</script>
typescript
// 带TypeScript的组件
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'

// 类型注解
const count: Ref<number> = ref(0)
const message = ref<string>('你好')

// 对象接口
interface User {
  id: number
  name: string
  email: string
}

const user = ref<User>({
  id: 1,
  name: 'John',
  email: 'john@example.com'
})

// 带类型的Props
interface Props {
  title: string
  count?: number
  user: User
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})

// 带类型的自定义事件
interface Emits {
  (e: 'update', value: number): void
  (e: 'delete', id: number): void
}

const emit = defineEmits<Emits>()

// 带类型推断的计算属性
const doubled = computed(() => props.count * 2)

// 带类型的函数
function updateUser(id: number, name: string): void {
  user.value.id = id
  user.value.name = name
}
</script>

Composables with TypeScript

带TypeScript的可组合函数

typescript
// composables/useCounter.ts
import { ref, computed, type Ref, type ComputedRef } from 'vue'

interface UseCounterReturn {
  count: Ref<number>
  doubled: ComputedRef<number>
  increment: () => void
  decrement: () => void
}

export function useCounter(initialValue = 0): UseCounterReturn {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment(): void {
    count.value++
  }

  function decrement(): void {
    count.value--
  }

  return {
    count,
    doubled,
    increment,
    decrement
  }
}
typescript
// composables/useCounter.ts
import { ref, computed, type Ref, type ComputedRef } from 'vue'

interface UseCounterReturn {
  count: Ref<number>
  doubled: ComputedRef<number>
  increment: () => void
  decrement: () => void
}

export function useCounter(initialValue = 0): UseCounterReturn {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment(): void {
    count.value++
  }

  function decrement(): void {
    count.value--
  }

  return {
    count,
    doubled,
    increment,
    decrement
  }
}

Testing

测试

Component Testing with Vitest

使用Vitest进行组件测试

javascript
// Counter.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter, {
      props: {
        initialCount: 5
      }
    })

    expect(wrapper.text()).toContain('5')
  })

  it('increments count when button clicked', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.vm.count).toBe(1)
    expect(wrapper.text()).toContain('1')
  })

  it('emits update event', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.emitted()).toHaveProperty('update')
    expect(wrapper.emitted('update')[0]).toEqual([1])
  })
})
javascript
// Counter.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('渲染初始count', () => {
    const wrapper = mount(Counter, {
      props: {
        initialCount: 5
      }
    })

    expect(wrapper.text()).toContain('5')
  })

  it('点击按钮时增加count', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.vm.count).toBe(1)
    expect(wrapper.text()).toContain('1')
  })

  it('触发update事件', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.emitted()).toHaveProperty('update')
    expect(wrapper.emitted('update')[0]).toEqual([1])
  })
})

Composable Testing

可组合函数测试

javascript
// useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })

  it('initializes with custom value', () => {
    const { count } = useCounter(10)
    expect(count.value).toBe(10)
  })

  it('increments count', () => {
    const { count, increment } = useCounter()
    increment()
    expect(count.value).toBe(1)
  })

  it('computes doubled value', () => {
    const { count, doubled, increment } = useCounter()
    expect(doubled.value).toBe(0)
    increment()
    expect(doubled.value).toBe(2)
  })
})
javascript
// useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('使用默认值初始化', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })

  it('使用自定义值初始化', () => {
    const { count } = useCounter(10)
    expect(count.value).toBe(10)
  })

  it('增加count', () => {
    const { count, increment } = useCounter()
    increment()
    expect(count.value).toBe(1)
  })

  it('计算翻倍值', () => {
    const { count, doubled, increment } = useCounter()
    expect(doubled.value).toBe(0)
    increment()
    expect(doubled.value).toBe(2)
  })
})

Best Practices

最佳实践

1. Use Composition API for Complex Logic

1. 对复杂逻辑使用Composition API

Composition API provides better code organization and reusability.
vue
<script setup>
// Good: Organized by feature
import { useUser } from '@/composables/useUser'
import { useProducts } from '@/composables/useProducts'
import { useCart } from '@/composables/useCart'

const { user, login, logout } = useUser()
const { products, fetchProducts } = useProducts()
const { cart, addToCart, removeFromCart } = useCart()
</script>
Composition API提供更好的代码组织和可复用性。
vue
<script setup>
// 好:按功能组织
import { useUser } from '@/composables/useUser'
import { useProducts } from '@/composables/useProducts'
import { useCart } from '@/composables/useCart'

const { user, login, logout } = useUser()
const { products, fetchProducts } = useProducts()
const { cart, addToCart, removeFromCart } = useCart()
</script>

2. Keep Components Small and Focused

2. 保持组件小巧且聚焦

Break large components into smaller, reusable pieces.
vue
<!-- Good: Focused components -->
<template>
  <div>
    <UserHeader :user="user" />
    <UserProfile :user="user" />
    <UserPosts :posts="posts" />
  </div>
</template>

<!-- Bad: One large component -->
<template>
  <div>
    <!-- 500 lines of mixed concerns -->
  </div>
</template>
将大型组件拆分为更小的可复用片段。
vue
<!-- 好:聚焦的组件 -->
<template>
  <div>
    <UserHeader :user="user" />
    <UserProfile :user="user" />
    <UserPosts :posts="posts" />
  </div>
</template>

<!-- 差:单个大型组件 -->
<template>
  <div>
    <!-- 500行混合关注点的代码 -->
  </div>
</template>

3. Use Computed for Derived State

3. 对派生状态使用Computed

Don't compute values in templates or methods.
vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([...])

// Good: Computed property
const activeItems = computed(() =>
  items.value.filter(item => item.active)
)

// Bad: Method called in template
function getActiveItems() {
  return items.value.filter(item => item.active)
}
</script>

<template>
  <!-- Good -->
  <div v-for="item in activeItems" :key="item.id">

  <!-- Bad: Computed on every render -->
  <div v-for="item in getActiveItems()" :key="item.id">
</template>
不要在模板或方法中计算值。
vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([...])

// 好:Computed属性
const activeItems = computed(() =>
  items.value.filter(item => item.active)
)

// 差:在模板中调用方法
function getActiveItems() {
  return items.value.filter(item => item.active)
}
</script>

<template>
  <!-- 好 -->
  <div v-for="item in activeItems" :key="item.id">

  <!-- 差:每次渲染都计算 -->
  <div v-for="item in getActiveItems()" :key="item.id">
</template>

4. Always Use Keys in v-for

4. 在v-for中始终使用key

Keys help Vue identify which items have changed.
vue
<!-- Good -->
<div v-for="item in items" :key="item.id">

<!-- Bad: No key -->
<div v-for="item in items">

<!-- Bad: Using index as key (for dynamic lists) -->
<div v-for="(item, index) in items" :key="index">
key帮助Vue识别哪些条目已更改。
vue
<!-- 好 -->
<div v-for="item in items" :key="item.id">

<!-- 差:没有key -->
<div v-for="item in items">

<!-- 差:使用index作为key(针对动态列表) -->
<div v-for="(item, index) in items" :key="index">

5. Avoid v-if with v-for

5. 避免v-if与v-for一起使用

Use computed properties to filter lists instead.
vue
<script setup>
import { computed } from 'vue'

// Good: Filter with computed
const activeItems = computed(() =>
  items.value.filter(item => item.active)
)
</script>

<template>
  <!-- Good -->
  <div v-for="item in activeItems" :key="item.id">

  <!-- Bad: v-if with v-for -->
  <div v-for="item in items" :key="item.id" v-if="item.active">
</template>
改用计算属性过滤列表。
vue
<script setup>
import { computed } from 'vue'

// 好:使用computed过滤
const activeItems = computed(() =>
  items.value.filter(item => item.active)
)
</script>

<template>
  <!-- 好 -->
  <div v-for="item in activeItems" :key="item.id">

  <!-- 差:v-if与v-for一起使用 -->
  <div v-for="item in items" :key="item.id" v-if="item.active">
</template>

6. Prop Validation

6. Prop验证

Always validate props in production components.
vue
<script setup>
// Good: Validated props
defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value) => value >= 0
  },
  status: {
    type: String,
    validator: (value) => ['draft', 'published', 'archived'].includes(value)
  }
})
</script>
在生产组件中始终验证Props。
vue
<script setup>
// 好:已验证的Props
defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value) => value >= 0
  },
  status: {
    type: String,
    validator: (value) => ['draft', 'published', 'archived'].includes(value)
  }
})
</script>

7. Use Provide/Inject Sparingly

7. 谨慎使用Provide/Inject

Provide/Inject is for deep component trees, not a replacement for props.
vue
<!-- Good: Use for app-level state -->
<script setup>
provide('theme', theme)
provide('i18n', i18n)
</script>

<!-- Bad: Use for direct parent-child communication -->
<!-- Use props instead -->
Provide/Inject适用于深层组件树,而非Props的替代品。
vue
<!-- 好:用于应用级状态 -->
<script setup>
provide('theme', theme)
provide('i18n', i18n)
</script>

<!-- 差:用于直接父子组件通信 -->
<!-- 改用Props -->

8. Cleanup in onUnmounted

8. 在onUnmounted中清理

Always cleanup side effects to prevent memory leaks.
vue
<script setup>
import { onMounted, onUnmounted } from 'vue'

let interval

onMounted(() => {
  interval = setInterval(() => {
    // Do something
  }, 1000)
})

onUnmounted(() => {
  clearInterval(interval)
})
</script>
始终清理副作用以防止内存泄漏。
vue
<script setup>
import { onMounted, onUnmounted } from 'vue'

let interval

onMounted(() => {
  interval = setInterval(() => {
    // 执行操作
  }, 1000)
})

onUnmounted(() => {
  clearInterval(interval)
})
</script>

9. Use Scoped Styles

9. 使用Scoped样式

Prevent style leaking with scoped styles.
vue
<style scoped>
/* Styles only apply to this component */
.button {
  background: blue;
}
</style>

<!-- Deep selector for child components -->
<style scoped>
.parent :deep(.child) {
  color: red;
}
</style>
使用Scoped样式防止样式泄漏。
vue
<style scoped>
/* 样式仅应用于当前组件 */
.button {
  background: blue;
}
</style>

<!-- 针对子组件的深度选择器 -->
<style scoped>
.parent :deep(.child) {
  color: red;
}
</style>

10. Lazy Load Routes

10. 懒加载路由

Improve initial load time with route-based code splitting.
javascript
const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue')
  }
]
基于路由的代码分割可提高初始加载速度。
javascript
const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue')
  }
]

Summary

总结

This Vue.js development skill covers:
  1. Reactivity System: ref(), reactive(), computed(), watch()
  2. Composition API: <script setup>, composables, lifecycle hooks
  3. Single-File Components: Template, script, and style organization
  4. Directives: v-if, v-for, v-model, v-bind, v-on
  5. Component Communication: Props, emits, provide/inject, slots
  6. State Management: Local state, composables, Pinia
  7. Routing: Vue Router navigation and guards
  8. Advanced Features: Teleport, Suspense, Transitions, Custom Directives
  9. Performance: Computed vs methods, v-memo, lazy loading, virtual scrolling
  10. TypeScript: Type-safe props, emits, composables
  11. Testing: Component and composable testing
  12. Best Practices: Modern Vue 3 patterns and optimization techniques
The patterns and examples are based on official Vue.js documentation (Trust Score: 9.7) and represent modern Vue 3 development practices with Composition API and <script setup> syntax.
本Vue.js开发技能涵盖:
  1. 响应式系统:ref(), reactive(), computed(), watch()
  2. Composition API<script setup>、可组合函数、生命周期钩子
  3. 单文件组件:模板、脚本和样式的组织
  4. 指令:v-if、v-for、v-model、v-bind、v-on
  5. 组件通信:Props、自定义事件、provide/inject、插槽
  6. 状态管理:本地状态、可组合函数、Pinia
  7. 路由:Vue Router导航和守卫
  8. 高级特性:Teleport、Suspense、过渡动画、自定义指令
  9. 性能:Computed vs 方法、v-memo、懒加载、虚拟滚动
  10. TypeScript:类型安全的Props、自定义事件、可组合函数
  11. 测试:组件和可组合函数测试
  12. 最佳实践:现代Vue 3模式和优化技巧
这些模式和示例基于Vue.js官方文档(可信度评分:9.7),代表了使用Composition API和<script setup>语法的现代Vue 3开发实践。