Vue 2 Options API
大白话解释: Options API 就像"按类型分文件柜"。把组件的逻辑按类型分开存放:
- data:数据放在这个柜子里
- methods:方法放在这个柜子里
- computed:计算属性放在这个柜子里
- watch:监听器放在这个柜子里
为什么用 Options API?
- 结构清晰:每个选项有明确的职责,新手容易理解
- Vue 2 默认方式:Vue 2 项目都用这种方式
- 适合简单组件:逻辑不复杂时,按类型组织很直观
Options API vs Composition API:
- Options API:按类型组织(data、methods、computed 分开)
- Composition API:按功能组织(同一个功能的数据、方法、计算属性放一起)
Vue 2 以选项式 API 为核心,通过 data、methods、computed、watch 等选项组织组件逻辑。这是 Vue 2 最基础也是最核心的编程范式。
组件选项完整结构
js
export default {
name: 'MyComponent', // 组件名,用于调试和递归组件
components: {}, // 局部注册子组件
directives: {}, // 局部注册自定义指令
filters: {}, // 局部注册过滤器(Vue 3 已移除)
mixins: [], // 混入
extends: {}, // 扩展另一个组件
inheritAttrs: false, // 控制 attrs 是否应用到根元素
model: { prop: 'checked', event: 'change' }, // 自定义 v-model
data() { return {} }, // 组件的响应式数据,必须是函数
props: {}, // 父组件传入的数据(只读)
computed: {}, // 计算属性,有缓存,依赖变化时才重新计算
watch: {}, // 侦听器,数据变化时执行副作用(发请求、操作 DOM)
methods: {}, // 组件方法,绑定到实例上,模板中通过 @事件 调用
beforeCreate() {}, // 实例初始化后,data/methods/computed/watch 均未初始化
created() {}, // data/methods/computed/watch 已初始化,DOM 尚未挂载
beforeMount() {}, // 模板编译完成,即将挂载 DOM
mounted() {}, // DOM 已挂载,可操作 this.$el 和 this.$refs
beforeUpdate() {}, // 数据变化后,DOM 更新前
updated() {}, // DOM 已更新完成
beforeDestroy() {}, // 实例仍可用,常用于清理定时器、取消监听
destroyed() {}, // 实例已销毁,子组件和事件监听已解绑
activated() {}, // keep-alive 缓存组件激活时调用
deactivated() {}, // keep-alive 缓存组件失活时调用
errorCaptured() {}, // 捕获后代组件错误时调用
}data
组件的响应式数据,必须是函数,返回一个新的对象实例。
js
export default {
data() {
return {
count: 0,
user: { name: '张三', age: 25 },
list: [],
form: {
username: '',
password: '',
remember: false,
},
}
},
}为什么 data 必须是函数?
js
// ❌ 错误写法 —— 对象引用共享,多个组件实例会互相影响
data: {
count: 0,
}
// ✅ 正确写法 —— 函数返回独立对象
data() {
return { count: 0 }
}data 中必须声明所有响应式属性
js
data() {
return {
// ✅ 提前声明,后续修改是响应式的
user: { name: '', age: 0 },
list: [],
}
}
// 后续赋值
this.user.name = '张三' // ✅ 响应式
this.list.push(1) // ✅ 响应式⚠️ 如果 data 中没有声明某个属性,后续
this.xxx = value不是响应式的,需要用this.$set()。详见 Vue 2 响应式原理。
props
父组件传递给子组件的数据。
基础用法
vue
<!-- 父组件 -->
<ChildComponent title="标题" :count="num" :user="userInfo" />js
// 子组件
export default {
// 数组写法(简单场景)
props: ['title', 'count', 'user'],
}对象写法(推荐,带类型校验)
js
export default {
props: {
// 基础类型校验
title: {
type: String,
required: true,
},
// 多种类型
id: {
type: [String, Number],
required: true,
},
// 带默认值
count: {
type: Number,
default: 0,
},
// 对象/数组默认值必须用工厂函数
user: {
type: Object,
default() {
return { name: '', age: 0 }
},
},
// 自定义校验函数
status: {
type: String,
validator(value) {
return ['loading', 'success', 'error'].includes(value)
},
default: 'loading',
},
// 布尔类型,默认 false
disabled: {
type: Boolean,
default: false,
},
},
}prop 的单向数据流
js
export default {
props: ['initialValue'],
data() {
return {
// ✅ 用 prop 初始化本地数据,后续修改不影响父组件
currentValue: this.initialValue,
}
},
watch: {
// 监听 prop 变化,同步到本地数据
initialValue(newVal) {
this.currentValue = newVal
},
},
}⚠️ 不要直接修改 prop。直接修改会报 warning,且父组件数据不会更新。
inheritAttrs 与 $attrs
vue
<!-- 父组件 -->
<ChildComponent type="text" placeholder="请输入" class="input" />js
// 子组件
export default {
// 不自动把非 prop 的 attribute 应用到根元素
inheritAttrs: false,
props: ['type'],
mounted() {
// $attrs 包含所有非 prop 的 attribute
console.log(this.$attrs) // { placeholder: '请输入' }(class 和 style 不在 $attrs 中,自动合并到根元素)
},
}vue
<template>
<!-- 手动绑定 $attrs 到指定元素 -->
<div class="wrapper">
<input v-bind="$attrs" />
</div>
</template>computed
计算属性,具有缓存特性,只有依赖变化时才重新计算。
js
export default {
data() {
return {
firstName: '张',
lastName: '三',
price: 100,
quantity: 3,
items: [
{ name: '苹果', price: 5, count: 2 },
{ name: '香蕉', price: 3, count: 5 },
],
}
},
computed: {
// 只读计算属性
fullName() {
return this.firstName + this.lastName
},
// 计算总价
totalPrice() {
return this.price * this.quantity
},
// 数组过滤
expensiveItems() {
return this.items.filter((item) => item.price > 4)
},
// 数组求和
totalAmount() {
return this.items.reduce((sum, item) => sum + item.price * item.count, 0)
},
// 可读写的计算属性
fullNameRW: {
get() {
return this.firstName + this.lastName
},
set(val) {
this.firstName = val[0]
this.lastName = val.slice(1)
},
},
},
}computed vs methods
js
computed: {
// ✅ computed —— 有缓存,依赖不变时不会重新计算
expensiveCalc() {
return this.bigList.filter(...).map(...).reduce(...)
},
},
methods: {
// ❌ methods —— 每次渲染都重新执行
expensiveCalc() {
return this.bigList.filter(...).map(...).reduce(...)
},
}💡 模板中
调用 computed 只在依赖变化时执行;同名 methods 每次渲染都执行。如果计算开销大,用 computed。
computed 缓存的副作用
js
computed: {
// ❌ 不要在 computed 中执行副作用
fullName() {
this.sideEffectCount++ // 副作用不可控,缓存机制导致执行时机不确定
return this.firstName + this.lastName
},
}methods
组件方法,绑定到组件实例上,模板中通过 @事件="方法名" 调用。
js
export default {
data() {
return {
count: 0,
inputVal: '',
list: [],
}
},
methods: {
// 普通方法
increment() {
this.count++
},
// 带参数
addToCount(n) {
this.count += n
},
// 事件对象
handleClick(event) {
console.log(event.target)
},
// 带参数 + 事件对象
handleClickWithArgs(msg, event) {
console.log(msg, event.target)
},
// 异步方法
async fetchList() {
try {
const res = await this.$http.get('/api/list')
this.list = res.data
} catch (err) {
this.$message.error('请求失败')
}
},
// 防抖搜索
onSearch() {
clearTimeout(this._searchTimer)
this._searchTimer = setTimeout(() => {
this.doSearch(this.inputVal)
}, 300)
},
doSearch(keyword) {
// 实际搜索逻辑
},
},
}vue
<template>
<!-- 基本调用 -->
<button @click="increment">+1</button>
<!-- 带参数 -->
<button @click="addToCount(5)">+5</button>
<!-- 带参数 + 原生事件对象 -->
<button @click="handleClickWithArgs('hello', $event)">点击</button>
<!-- 事件修饰符 -->
<form @submit.prevent="onSubmit">...</form>
<div @click.stop="handleClick">...</div>
<input @keyup.enter="onEnter" />
</template>⚠️ methods 中的
this指向组件实例,不能用箭头函数定义 methods,否则this会丢失。
watch
侦听器,监听数据变化并执行副作用(异步操作、DOM 操作、开销较大的操作)。
基础用法
js
export default {
data() {
return {
keyword: '',
selectedId: null,
user: { name: '', address: { city: '' } },
}
},
watch: {
// 基本监听
keyword(newVal, oldVal) {
console.log(`搜索词: ${oldVal} → ${newVal}`)
this.fetchResults(newVal)
},
// 监听对象的某个属性(字符串路径)
'user.name'(newVal, oldVal) {
console.log(`用户名变化: ${newVal}`)
},
// 深度监听对象
user: {
handler(newVal) {
console.log('user 对象变化了', newVal)
},
deep: true, // 深度监听
immediate: true, // 立即执行一次
},
},
}computed vs watch 使用场景
js
// ✅ computed —— 一个值依赖另一个值,纯计算
computed: {
fullName() {
return this.firstName + this.lastName
},
}
// ✅ watch —— 数据变化时执行副作用(发请求、操作 DOM、修改其他数据)
watch: {
selectedId(newId) {
this.fetchUserDetail(newId) // 发请求
this.$router.replace({ query: { id: newId } }) // 修改路由
},
}💡 能用 computed 就不用 watch。computed 有缓存,更高效。
watch 的 $watch 实例方法
js
export default {
mounted() {
// 动态创建监听器,返回取消函数
const unwatch = this.$watch(
'keyword',
(newVal) => {
this.fetchResults(newVal)
},
{ immediate: true }
)
// 取消监听
// unwatch()
},
}filters(Vue 2 独有,Vue 3 已移除)
js
// 全局注册
Vue.filter('formatDate', (value, format = 'YYYY-MM-DD') => {
return dayjs(value).format(format)
})
// 局部注册
export default {
filters: {
currency(value, symbol = '¥') {
return `${symbol}${Number(value).toFixed(2)}`
},
},
}vue
<template>
<!-- 模板中使用管道符 -->
<p>{{ date | formatDate('YYYY年MM月DD日') }}</p>
<p>{{ price | currency('¥') }}</p>
</template>⚠️ Vue 3 中 filters 被移除,用 computed 或方法代替。
生命周期详解
初始化阶段:
beforeCreate → created
挂载阶段:
beforeMount → mounted
更新阶段:
beforeUpdate → updated
销毁阶段:
beforeDestroy → destroyed
特殊:
activated / deactivated —— keep-alive
errorCaptured —— 错误捕获各阶段详解
js
export default {
beforeCreate() {
// data、methods、computed、watch 均未初始化
// this.$data 为 {}(空对象,尚未与 data 选项合并)
// 极少使用
console.log('beforeCreate', this.$data) // undefined
},
created() {
// ✅ data、methods、computed、watch 已初始化
// ✅ 可以访问 this.xxx、调用 methods
// ❌ DOM 尚未挂载,不能操作 this.$el
// 常用场景:发起初始化请求
this.fetchData()
this.initWebSocket()
},
beforeMount() {
// 模板编译完成,即将挂载 DOM
// 很少使用
},
mounted() {
// ✅ DOM 已挂载,可以操作 this.$el、this.$refs
// 常用场景:操作第三方 DOM 库、初始化图表
this.initChart()
this.bindResizeEvent()
},
beforeUpdate() {
// 数据变化后,DOM 更新前
// 可以访问旧的 DOM 状态
console.log('旧 DOM:', this.$el.innerHTML)
},
updated() {
// DOM 已更新
// ⚠️ 避免在此修改数据,可能导致无限循环
console.log('新 DOM:', this.$el.innerHTML)
},
beforeDestroy() {
// ✅ 组件实例仍可用
// 常用场景:清理定时器、取消事件监听、断开连接
clearInterval(this.timer)
window.removeEventListener('resize', this.handleResize)
this.socket.close()
},
destroyed() {
// 组件实例已销毁
// 子组件已销毁,事件监听已解绑
},
// keep-alive 相关
activated() {
// 从缓存中激活时调用
// 可重新获取数据
this.refreshData()
},
deactivated() {
// 进入缓存时调用
// 清理副作用
this.stopPolling()
},
// 错误捕获
errorCaptured(err, vm, info) {
console.error('子组件错误:', err, info)
// 返回 false 阻止错误继续向上传播
return false
},
}生命周期执行顺序
父 beforeCreate → 父 created → 父 beforeMount
→ 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted💡 父组件等待所有子组件挂载完成后才 mounted。如果需要在父组件 mounted 中操作子组件 DOM,是安全的。
常用实例属性与方法
js
export default {
mounted() {
// ---- 数据 ----
this.$data // 响应式数据对象
this.$props // 当前 props(只读)
this.$options // 组件选项对象
// ---- DOM ----
this.$el // 根 DOM 元素
this.$refs // 所有 ref 引用(对象)
this.$refs.input.focus() // 操作 DOM 或子组件
// ---- 关系 ----
this.$parent // 父组件实例
this.$children // 子组件实例数组(顺序不确定,不推荐依赖,且非响应式)
this.$root // 根组件实例
this.$slots // 插槽内容
this.$scopedSlots // 作用域插槽
this.$listeners // 事件监听器
// ---- 方法 ----
this.$emit('change', this.count) // 触发事件
this.$set(this.obj, 'key', 'val') // 新增响应式属性
this.$delete(this.obj, 'key') // 删除响应式属性
this.$forceUpdate() // 强制重新渲染
this.$nextTick(() => {}) // DOM 更新后执行
// ---- 全局 ----
this.$router // Vue Router 实例
this.$route // 当前路由对象
this.$store // Vuex Store 实例
this.$http // axios(如果挂载到原型)
this.$message // UI 库消息组件
this.$confirm // UI 库确认弹窗
// ---- 监听 ----
this.$watch('count', (newVal, oldVal) => {}, { deep: true })
// ⚠️ Vue 3 已移除 $on / $once / $off
// Vue 3 中实例不再支持事件总线模式,需用外部库(如 mitt)替代
this.$on('event', handler) // 监听事件(自己触发的)
this.$once('event', handler) // 只监听一次
this.$off('event', handler) // 取消监听
},
}常见坑点
1. 箭头函数丢失 this
js
export default {
methods: {
// ❌ 箭头函数,this 不是组件实例
fetchData: () => {
this.list = [] // this 指向外层作用域
},
// ✅ 普通函数
fetchData() {
this.list = []
},
},
}2. computed 和 data 同名
js
export default {
data() { return { name: '张三' } },
computed: {
// ❌ 同名会覆盖 data,且报 warning
name() { return '李四' },
},
}3. 模板中直接修改数据
vue
<template>
<!-- ❌ 避免在模板中执行复杂逻辑 -->
<p>{{ list.filter(item => item.active).length }}</p>
<!-- ✅ 用 computed -->
<p>{{ activeCount }}</p>
</template>4. v-for 与 v-if 同时使用
vue
<template>
<!-- ❌ Vue 2 中 v-for 优先级高于 v-if,每个元素都会判断 -->
<li v-for="item in list" v-if="item.active" :key="item.id">
{{ item.name }}
</li>
<!-- ✅ 用 template 包裹,或用 computed 过滤 -->
<template v-for="item in list">
<li v-if="item.active" :key="item.id">{{ item.name }}</li>
</template>
</template>