作为一名长期奋战在前端开发一线的工程师,我见证了Vue从2.x到3.x的演进过程。这次API设计理念的变革,绝不仅仅是语法形式上的改变,而是Vue团队对现代前端开发需求的深度思考。让我们先通过一个实际案例来感受两者的差异:
假设我们要开发一个计数器组件,在Vue2中我们会这样写:
javascript复制export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('组件已挂载')
}
}
而在Vue3中,同样的功能可以这样实现:
javascript复制import { ref, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
onMounted(() => {
console.log('组件已挂载')
})
return {
count,
increment
}
}
}
从表面上看,两种写法都能实现相同的功能,但背后的设计哲学和适用场景却大不相同。选项式API(Options API)通过将代码按功能类型分组(data、methods、生命周期等),提供了结构化的代码组织方式;而组合式API(Composition API)则通过逻辑关注点组织代码,将相关功能集中在一起。
关键区别:选项式API是按"功能类型"组织代码,组合式API是按"逻辑关注点"组织代码。前者像图书馆按书籍类型分类,后者像把完成一个项目所需的所有资料放在一个文件夹。
Vue2使用Object.defineProperty实现响应式,这也是为什么data中的数据默认就是响应式的。但这种实现方式有几个固有缺陷:
Vue3则采用了Proxy作为响应式系统的核心,带来了以下改进:
javascript复制// Vue2响应式原理简化实现
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`读取${key}`)
return val
},
set(newVal) {
console.log(`设置${key}为${newVal}`)
val = newVal
}
})
}
// Vue3响应式原理简化实现
const reactive = (target) => {
return new Proxy(target, {
get(target, key, receiver) {
console.log(`读取${key}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置${key}为${value}`)
return Reflect.set(target, key, value, receiver)
}
})
}
这种底层实现的差异直接影响了API的设计。在Vue3中,你需要显式地使用ref或reactive来创建响应式数据,这虽然增加了一点学习成本,但带来了更大的灵活性和更好的性能。
setup函数是组合式API的核心,它有以下几个关键特点:
javascript复制export default {
props: {
title: String
},
setup(props, context) {
// props是响应式的,不能解构
console.log(props.title)
// context包含attrs、slots、emit等非响应式属性
const { attrs, slots, emit } = context
return {
// 模板中可用的数据和方法
}
}
}
重要提示:不要解构props,这会破坏其响应性。如果需要解构,可以使用toRefs:
javascript复制import { toRefs } from 'vue' setup(props) { const { title } = toRefs(props) // 现在title是响应式的 }
在组合式API中,创建响应式数据主要有两种方式:
ref:用于基本类型(string、number等)或需要替换整个对象引用的情况
reactive:用于对象类型
javascript复制import { ref, reactive } from 'vue'
setup() {
// 适合用ref的情况
const count = ref(0)
const message = ref('Hello')
// 适合用reactive的情况
const user = reactive({
name: 'Alice',
age: 25
})
// 修改ref
count.value++
// 修改reactive
user.age++
return {
count,
message,
user
}
}
实际开发中的经验法则:
Vue3中的生命周期钩子与Vue2类似,但有一些变化:
| Vue2选项式API | Vue3组合式API | 说明 |
|---|---|---|
| beforeCreate | 无 | 被setup替代 |
| created | 无 | 被setup替代 |
| beforeMount | onBeforeMount | 基本相同 |
| mounted | onMounted | 基本相同 |
| beforeUpdate | onBeforeUpdate | 基本相同 |
| updated | onUpdated | 基本相同 |
| beforeDestroy | onBeforeUnmount | 更名 |
| destroyed | onUnmounted | 更名 |
| errorCaptured | onErrorCaptured | 基本相同 |
使用示例:
javascript复制import { onMounted, onUnmounted } from 'vue'
setup() {
onMounted(() => {
console.log('组件挂载完成')
})
onUnmounted(() => {
console.log('组件即将卸载')
})
}
虽然Vue3支持两种API风格混用,但需要注意以下几点:
javascript复制export default {
data() {
return {
oldData: 'Vue2 data'
}
},
methods: {
oldMethod() {
console.log('Vue2 method')
}
},
setup() {
const newData = ref('Vue3 data')
const newMethod = () => {
console.log('Vue3 method')
// 这里无法直接访问this.oldData或this.oldMethod
}
return {
newData,
newMethod
}
},
mounted() {
// 可以访问setup返回的内容
console.log(this.newData) // 'Vue3 data'
this.newMethod() // 'Vue3 method'
}
}
javascript复制// Vue2
this.items.splice(index, 1, newValue)
// Vue3
items.value[index] = newValue // 可以直接工作
javascript复制// Vue2
Vue.set(this.obj, 'newProp', value)
// Vue3
obj.value.newProp = value // 可以直接工作
组合式API最强大的特性之一是能够创建可重用的组合函数。这类似于Vue2中的mixins,但解决了mixins的所有主要缺点:
javascript复制// useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
return {
count,
increment,
decrement
}
}
// 在组件中使用
import { useCounter } from './useCounter'
setup() {
const { count, increment } = useCounter(10)
return {
count,
increment
}
}
javascript复制import { shallowRef, shallowReactive, markRaw } from 'vue'
setup() {
// 只有.value变化会触发更新
const largeList = shallowRef([])
// 只有第一层属性变化会触发更新
const config = shallowReactive({
theme: 'dark',
settings: { // 这个嵌套对象不是响应式的
fontSize: 14
}
})
// 这个对象永远不会被转为响应式
const staticData = markRaw({
constantValue: 42
})
}
javascript复制import { computed, watchEffect, watch } from 'vue'
setup(props) {
// 高效的计算属性
const fullName = computed(() => {
return `${props.firstName} ${props.lastName}`
})
// 自动收集依赖的watchEffect
watchEffect(() => {
console.log(`FullName changed: ${fullName.value}`)
})
// 更精确控制的watch
watch(
() => props.userId,
(newId, oldId) => {
fetchUser(newId)
}
)
}
问题1:为什么我的ref在模板中不更新?
可能原因:
解决方案:
javascript复制setup() {
const data = ref(null)
fetchData().then(res => {
// 添加组件是否卸载的检查
if (!isUnmounted) {
data.value = res
}
})
// 确保返回所有模板需要的数据
return {
data
}
}
问题2:如何在组合式API中使用this.$router或this.$store?
解决方案:
javascript复制import { getCurrentInstance } from 'vue'
setup() {
const instance = getCurrentInstance()
const router = instance.appContext.config.globalProperties.$router
const store = instance.appContext.config.globalProperties.$store
// 更好的方式是直接导入
// import { useRouter, useStore } from 'vue-router'/'vuex'
// const router = useRouter()
// const store = useStore()
}
问题3:组合式API中的代码组织建议
最佳实践:
javascript复制setup(props) {
// 用户相关功能
const { user, login, logout } = useUserAuth()
// 数据获取功能
const { data, loading, error } = useFetchData(props.id)
// UI状态
const { isMobile, screenSize } = useScreenSize()
return {
user,
login,
logout,
data,
loading,
error,
isMobile
}
}
经过近两年的Vue3生产环境实践,我发现组合式API确实大幅提高了代码的可维护性,特别是在处理复杂业务逻辑时。虽然初期需要适应新的思维方式,但一旦掌握,开发效率会有显著提升。对于新项目,我建议直接使用组合式API;对于老项目,可以采取渐进式迁移策略。