Vue3 的模板语法是构建响应式用户界面的基础工具集。与 Vue2 相比,Vue3 在保持 API 兼容性的同时,通过编译器优化和组合式 API 的引入,使得模板语法更加高效和灵活。理解这些核心指令的工作原理和最佳实践,是开发高质量 Vue 应用的前提。
Vue 的指令系统遵循"声明式渲染"的理念,开发者只需描述"UI 应该是什么状态",而不需要手动操作 DOM。这种抽象带来了几个显著优势:
在 Vue3 中,指令系统经过重新设计,主要改进包括:
| 指令 | 主要用途 | 适用场景 | 特殊行为 |
|---|---|---|---|
| v-if | 条件渲染 | 需要完全销毁/重建 DOM 的情况 | 支持 v-else/v-else-if 链式调用 |
| v-for | 列表渲染 | 动态生成相似结构的元素列表 | 需要指定唯一的 key 属性 |
| v-model | 双向数据绑定 | 表单输入和组件通信 | 支持自定义修饰符和转换器 |
| slot | 内容分发 | 创建可复用的组件模板结构 | 支持作用域插槽和具名插槽 |
虽然 v-if 和 v-show 都能控制元素显示隐藏,但它们的实现机制完全不同:
性能考量:
经验法则:如果切换频率高于 1 次/秒,优先考虑 v-show;否则使用 v-if
html复制<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>Default</div>
javascript复制// 不推荐
<div v-if="user.roles.some(role => role.level > 3)">Admin</div>
// 推荐 - 使用计算属性
computed: {
isAdmin() {
return this.user.roles.some(role => role.level > 3)
}
}
html复制<!-- 不推荐 - 每次渲染都会重新计算条件 -->
<div v-for="item in items" v-if="item.isActive">...</div>
<!-- 推荐 - 先过滤数据再渲染 -->
<div v-for="item in activeItems">...</div>
key 是 Vue 识别节点身份的唯一标识,正确使用 key 可以:
最佳实践:
html复制<!-- 使用唯一稳定的 ID 作为 key -->
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
<!-- 避免使用索引作为 key(除非列表是静态的) -->
<li v-for="(item, index) in items" :key="index"> <!-- 不推荐 -->
{{ item.text }}
</li>
javascript复制// 对象迭代示例
<div v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</div>
Vue 能够检测以下数组方法的变化:
但以下情况不会触发更新:
vm.items[index] = newValuevm.items.length = newLength解决方案:
javascript复制// Vue.set 或 this.$set
this.$set(this.items, index, newValue)
// 使用可触发更新的方法
this.items.splice(index, 1, newValue)
v-model 实际上是以下语法糖的简写:
html复制<input
:value="searchText"
@input="searchText = $event.target.value"
>
在 Vue3 中,v-model 有以下改进:
不同表单元素 v-model 的行为有所不同:
| 元素类型 | v-model 绑定值 | 事件 | 注意事项 |
|---|---|---|---|
| text | string | input | 自动 trim 使用 .trim 修饰符 |
| checkbox | boolean (单个) / array (多个) | change | 多个复选框绑定到同一数组 |
| radio | string | change | 同一组 radio 使用相同 v-model |
| select | string (单选) / array (多选) | change | 多选需添加 multiple 属性 |
在 Vue3 中实现自定义 v-model:
javascript复制// 子组件
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`
}
使用多个 v-model:
html复制<UserName
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
html复制<!-- 子组件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot :user="user"></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!-- 父组件使用 -->
<ChildComponent>
<template v-slot:header>
<h1>Page Title</h1>
</template>
<template v-slot:default="slotProps">
<p>Main content for {{ slotProps.user.name }}</p>
</template>
<template v-slot:footer>
<p>Footer content</p>
</template>
</ChildComponent>
html复制<template v-slot:[dynamicSlotName]>
...
</template>
html复制<!-- 默认插槽 -->
<template #default="props">...</template>
<!-- 具名插槽 -->
<template #header>...</template>
javascript复制// 无渲染组件示例
export default {
render() {
return this.$slots.default({
data: this.internalData,
methods: this.internalMethods
})
}
}
在 Vue2 中,v-for 优先级高于 v-if,而在 Vue3 中则相反。这会导致不同的行为:
html复制<!-- Vue2: 先循环再条件判断 -->
<li v-for="item in items" v-if="item.isActive">...</li>
<!-- Vue3: 先条件判断再循环 -->
<li v-for="item in items" v-if="item.isActive">...</li>
最佳实践是避免在同一元素上使用两者,改为:
html复制<template v-for="item in items">
<li v-if="item.isActive" :key="item.id">...</li>
</template>
javascript复制props: {
modelValue: String,
modelModifiers: {
type: Object,
default: () => ({})
}
},
created() {
if (this.modelModifiers.capitalize) {
// 处理 capitalize 修饰符逻辑
}
}
javascript复制// 在自定义组件上使用原生事件
<CustomInput
v-model="searchText"
@keydown.enter="search"
/>
优化示例:
javascript复制// 不推荐 - 每次渲染都会创建新函数
<template #item="{ data }">
<div @click="() => handleClick(data.id)">...</div>
</template>
// 推荐 - 提前绑定
methods: {
getClickHandler(id) {
if (!this._clickHandlers) this._clickHandlers = {}
if (!this._clickHandlers[id]) {
this._clickHandlers[id] = () => this.handleClick(id)
}
return this._clickHandlers[id]
}
}
javascript复制// BaseInput.vue
<template>
<div class="form-group">
<label v-if="label">{{ label }}</label>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
v-bind="$attrs"
>
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
</template>
<script>
export default {
props: {
modelValue: [String, Number],
label: String,
error: String
},
emits: ['update:modelValue']
}
</script>
javascript复制// useFormValidation.js
import { ref, computed } from 'vue'
export function useFormValidation() {
const errors = ref({})
const validate = (rules, formData) => {
errors.value = {}
let isValid = true
Object.keys(rules).forEach(key => {
const rule = rules[key]
const value = formData[key]
if (rule.required && !value) {
errors.value[key] = rule.message || 'This field is required'
isValid = false
}
// 添加更多验证规则...
})
return isValid
}
const getError = field => errors.value[field]
return { errors, validate, getError }
}
javascript复制// UserForm.vue
<template>
<form @submit.prevent="handleSubmit">
<BaseInput
v-model="formData.name"
label="Full Name"
:error="getError('name')"
/>
<BaseInput
v-model="formData.email"
label="Email"
type="email"
:error="getError('email')"
/>
<button type="submit">Submit</button>
</form>
</template>
<script>
import { reactive } from 'vue'
import { useFormValidation } from './useFormValidation'
import BaseInput from './BaseInput.vue'
export default {
components: { BaseInput },
setup() {
const formData = reactive({
name: '',
email: ''
})
const { validate, getError } = useFormValidation()
const rules = {
name: { required: true, message: 'Please enter your name' },
email: { required: true, message: 'Email is required' }
}
const handleSubmit = () => {
if (validate(rules, formData)) {
console.log('Form submitted:', formData)
}
}
return { formData, handleSubmit, getError }
}
}
</script>
使用 Vue 的编译器警告可以帮助发现潜在问题:
javascript复制// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compilerOptions = {
whitespace: 'condense',
isCustomElement: tag => tag.startsWith('app-'),
// 开启所有警告
warn: (msg, range) => console.warn(msg, range)
}
return options
})
}
}
javascript复制import { markRaw } from 'vue'
export default {
setup() {
const data = markRaw({
// 大型不可变数据
})
return { data }
}
}
html复制<div v-once>
<h1>Static Title</h1>
<p>This content will never change</p>
</div>
常见内存泄漏场景:
解决方案:
javascript复制import { onBeforeUnmount } from 'vue'
export default {
setup() {
const timer = setInterval(() => {
// do something
}, 1000)
onBeforeUnmount(() => {
clearInterval(timer)
})
}
}
typescript复制import { defineComponent, PropType } from 'vue'
interface User {
id: number
name: string
email: string
}
export default defineComponent({
props: {
// 基本类型
count: {
type: Number,
required: true
},
// 复杂类型
user: {
type: Object as PropType<User>,
required: true
},
// 带默认值的可选prop
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
default: 'medium'
}
}
})
typescript复制import { ref, computed, defineComponent } from 'vue'
export default defineComponent({
setup() {
const count = ref(0) // 自动推断为 Ref<number>
const double = computed(() => count.value * 2) // ComputedRef<number>
const increment = () => {
count.value++
}
return {
count,
double,
increment
}
}
})
typescript复制import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
setup() {
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
if (inputRef.value) {
inputRef.value.focus()
}
})
return {
inputRef
}
},
template: `
<input ref="inputRef" type="text">
`
})
推荐的项目结构:
code复制src/
components/
base/ # 基础UI组件 (Button, Input等)
modules/ # 业务模块组件
layouts/ # 布局组件
views/ # 路由级组件
composables/ # 组合式函数
stores/ # 状态管理
types/ # 类型定义
根据应用规模选择方案:
统一的异步操作处理:
typescript复制import { ref } from 'vue'
export function useAsyncTask<T extends (...args: any[]) => Promise<any>>(task: T) {
const loading = ref(false)
const error = ref<Error | null>(null)
const result = ref<Awaited<ReturnType<T>> | null>(null)
const execute = async (...args: Parameters<T>) => {
loading.value = true
error.value = null
try {
result.value = await task(...args)
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
}
return {
loading,
error,
result,
execute
}
}
使用示例:
typescript复制const { loading, error, result, execute } = useAsyncTask(fetchUserList)
onMounted(() => {
execute(1, 20) // 分页参数
})