目录
4.6 shallowRef() 和 shallowReactive()
1. Vue3简介与新特性
1.1 Vue3的革命性变化
Vue3是Vue.js的下一个主要版本,它带来了许多激动人心的新特性和改进。最重要的是,Vue3完全向后兼容Vue2的绝大部分功能,同时提供了更强大、更灵活的开发体验。
为什么要学习Vue3?
- 更好的性能:Bundle体积减少41%,初次渲染快55%,更新渲染快133%
- 更好的Tree-shaking:未使用的功能不会被打包
- Composition API:更好的逻辑复用和代码组织
- 更好的TypeScript支持:原生TypeScript支持
- 新的特性:Teleport、Suspense、Fragment等
1.2 Vue3的核心新特性
// Vue2的写法
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
}
}
// Vue3 Composition API的写法
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('Hello')
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
return {
count,
message,
doubleCount,
increment
}
}
}
关键差异解释:
- Vue2使用Options API(选项式API),把数据、方法、计算属性分开写
- Vue3引入了Composition API(组合式API),可以把相关的逻辑组织在一起
ref()
创建响应式数据,通过.value
访问computed()
创建计算属性setup()
是组合式API的入口函数
1.3 主要改进点
1. 性能提升
// Vue3的响应式系统使用Proxy,性能更好
const state = reactive({
name: 'Vue3',
version: '3.0'
})
// 直接修改就能响应,不需要Vue.set
state.newProperty = 'new value' // ✅ 响应式的
delete state.version // ✅ 响应式的
2. 更好的开发体验
<!-- Vue3支持多个根元素 -->
<template>
<header>头部</header>
<main>主要内容</main>
<footer>底部</footer>
</template>
3. 更小的包体积
// Vue3支持按需引入
import { createApp, ref, computed } from 'vue'
// 只引入需要的功能,减少包体积
2. 环境搭建与项目创建
2.1 使用Vite创建项目(推荐)
Vite是Vue3官方推荐的构建工具,启动速度极快,热更新体验优秀。
# 使用npm
npm create vue@latest my-vue3-project
# 使用yarn
yarn create vue my-vue3-project
# 使用pnpm
pnpm create vue my-vue3-project
创建过程中会询问项目配置:
✔ Project name: … my-vue3-project
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
2.2 项目结构解析
my-vue3-project/
├── public/ # 静态资源目录
│ └── favicon.ico
├── src/ # 源码目录
│ ├── assets/ # 资源文件
│ ├── components/ # 组件目录
│ ├── views/ # 页面组件
│ ├── router/ # 路由配置
│ ├── stores/ # Pinia状态管理
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── index.html # HTML模板
├── vite.config.js # Vite配置
└── package.json # 项目配置
2.3 入口文件对比
Vue2的main.js
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App)
}).$mount('#app')
Vue3的main.js
import { createApp } from 'vue'
import App from './App.vue'
// 创建应用实例
const app = createApp(App)
// 挂载到DOM
app.mount('#app')
// 可以链式调用
createApp(App)
.use(router)
.use(store)
.mount('#app')
重要变化说明:
- Vue3不再是构造函数,而是通过
createApp
创建应用实例 - 应用实例与全局Vue分离,避免了全局污染
- 支持多个应用实例同时存在
2.4 开发服务器启动
cd my-vue3-project
npm install
npm run dev
Vite的开发服务器通常在几秒内就能启动,相比Webpack要快很多。
3. Composition API核心概念
3.1 为什么需要Composition API?
Options API的局限性:
// Vue2: 相关逻辑分散在不同选项中
export default {
data() {
return {
// 用户相关数据
users: [],
currentUser: null,
// 产品相关数据
products: [],
currentProduct: null
}
},
computed: {
// 用户相关计算属性
userCount() {
return this.users.length
},
// 产品相关计算属性
productCount() {
return this.products.length
}
},
methods: {
// 用户相关方法
fetchUsers() { /* ... */ },
updateUser() { /* ... */ },
// 产品相关方法
fetchProducts() { /* ... */ },
updateProduct() { /* ... */ }
}
}
当组件变得复杂时,相关的逻辑被分散在不同的选项中,难以维护。
Composition API的优势:
// Vue3: 相关逻辑可以组织在一起
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// 用户相关逻辑
const useUsers = () => {
const users = ref([])
const currentUser = ref(null)
const userCount = computed(() => users.value.length)
const fetchUsers = async () => {
// 获取用户逻辑
}
const updateUser = (user) => {
// 更新用户逻辑
}
onMounted(fetchUsers)
return {
users,
currentUser,
userCount,
fetchUsers,
updateUser
}
}
// 产品相关逻辑
const useProducts = () => {
const products = ref([])
const currentProduct = ref(null)
const productCount = computed(() => products.value.length)
const fetchProducts = async () => {
// 获取产品逻辑
}
return {
products,
currentProduct,
productCount,
fetchProducts
}
}
// 组合使用
const userState = useUsers()
const productState = useProducts()
return {
...userState,
...productState
}
}
}
3.2 setup函数基础
setup
函数是Composition API的入口点,它在组件创建之前执行。
export default {
// Props定义
props: {
title: String,
count: Number
},
// setup函数
setup(props, context) {
console.log('Props:', props) // 响应式的props对象
console.log('Context:', context) // 包含attrs, slots, emit, expose
// 在这里定义响应式数据
const message = ref('Hello Vue3')
// 定义方法
const updateMessage = () => {
message.value = 'Updated!'
}
// 返回模板需要的数据和方法
return {
message,
updateMessage
}
}
}
setup函数的参数:
- props: 组件接收的props(响应式)
- context: 包含四个属性的对象
attrs
: 非响应式的属性对象slots
: 插槽对象emit
: 触发事件的函数expose
: 暴露公共属性的函数
3.3 script setup语法糖
Vue3.2引入的<script setup>
语法糖让代码更简洁:
<!-- 传统setup写法 -->
<script>
import { ref, computed } from 'vue'
export default {
props: {
title: String
},
emits: ['update'],
setup(props, { emit }) {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
emit('update', count.value)
}
return {
count,
doubleCount,
increment
}
}
}
</script>
<!-- script setup语法糖 -->
<script setup>
import { ref, computed } from 'vue'
// 定义props
const props = defineProps({
title: String
})
// 定义emits
const emit = defineEmits(['update'])
// 响应式数据
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
// 方法
const increment = () => {
count.value++
emit('update', count.value)
}
// 不需要return,所有顶层变量都会暴露给模板
</script>
script setup的优势:
- 代码更简洁,减少样板代码
- 更好的性能(编译时优化)
- 更好的TypeScript推导
- 自动暴露顶层变量
4. 响应式系统详解
4.1 Vue3响应式原理
Vue3使用ES6的Proxy重写了响应式系统,相比Vue2的Object.defineProperty有很大优势。
// Vue2的响应式(Object.defineProperty)
// 只能监听已存在的属性,无法监听数组索引和新增属性
// Vue3的响应式(Proxy)
const target = { name: 'Vue3', version: '3.0' }
const proxy = new Proxy(target, {
get(target, key) {
console.log(`获取 ${key}: ${target[key]}`)
return target[key]
},
set(target, key, value) {
console.log(`设置 ${key}: ${value}`)
target[key] = value
return true
},
deleteProperty(target, key) {
console.log(`删除 ${key}`)
delete target[key]
return true
}
})
// 可以监听所有操作
proxy.name // 获取 name: Vue3
proxy.newProp = 'new' // 设置 newProp: new
delete proxy.version // 删除 version
4.2 reactive() - 深度响应式
reactive()
用于创建深度响应式的对象:
import { reactive } from 'vue'
export default {
setup() {
// 创建响应式对象
const state = reactive({
count: 0,
user: {
name: '张三',
profile: {
age: 25,
city: '北京'
}
},
todos: [
{ id: 1, text: '学习Vue3', done: false }
]
})
// 所有嵌套属性都是响应式的
const updateUser = () => {
state.user.name = '李四' // ✅ 响应式
state.user.profile.age = 26 // ✅ 响应式
state.todos[0].done = true // ✅ 响应式
// 直接添加新属性也是响应式的
state.user.email = 'lisi@example.com' // ✅ 响应式
// 数组操作都是响应式的
state.todos.push({ id: 2, text: '学习Composition API', done: false }) // ✅ 响应式
}
return {
state,
updateUser
}
}
}
reactive的特点:
- 只能用于对象类型(对象、数组、Map、Set等)
- 返回的是Proxy对象,不等于原始对象
- 深度响应式,所有嵌套属性都会变成响应式
- 不能被解构,解构后会失去响应式
// ❌ 错误:解构会失去响应式
const { count } = reactive({ count: 0 })
count++ // 不会触发更新
// ✅ 正确:保持对象结构
const state = reactive({ count: 0 })
state.count++ // 会触发更新
4.3 ref() - 基本类型响应式
ref()
用于创建任何类型的响应式数据:
import { ref } from 'vue'
export default {
setup() {
// 基本类型
const count = ref(0)
const message = ref('Hello')
const isVisible = ref(true)
// 对象类型(会自动转换为reactive)
const user = ref({
name: '张三',
age: 25
})
// 数组
const items = ref(['apple', 'banana'])
// 修改值需要通过.value
const updateData = () => {
count.value++
message.value = 'Hi'
isVisible.value = false
// 对象类型可以直接修改属性
user.value.name = '李四'
// 或者替换整个对象
user.value = { name: '王五', age: 30 }
// 数组操作
items.value.push('orange')
}
return {
count,
message,
isVisible,
user,
items,
updateData
}
}
}
ref的特点:
- 可以用于任何类型的数据
- 通过
.value
访问和修改值 - 在模板中会自动解包,不需要
.value
- 对象类型会自动调用
reactive()
<template>
<!-- 模板中自动解包,不需要.value -->
<div>{
{ count }}</div>
<div>{
{ message }}</div>
<div>{
{ user.name }}</div>
</template>
4.4 toRef() 和 toRefs()
用于将响应式对象的属性转换为ref:
import { reactive, toRef, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
name: '张三',
age: 25,
city: '北京'
})
// toRef: 转换单个属性
const name = toRef(state, 'name')
console.log(name.value) // '张三'
name.value = '李四' // 会同步更新state.name
// toRefs: 转换所有属性
const stateRefs = toRefs(state)
// 现在可以解构了
const { age, city } = stateRefs
return {
name,
age,
city,
// 或者直接解构
...toRefs(state)
}
}
}
使用场景:
// 在自定义Hook中返回响应式数据
function useCounter() {
const state = reactive({
count: 0,
step: 1
})
const increment = () => {
state.count += state.step
}
// 返回时使用toRefs,让组件可以解构使用
return {
...toRefs(state),
increment
}
}
// 在组件中使用
export default {
setup() {
// 可以直接解构,不会失去响应式
const { count, step, increment } = useCounter()
return {
count,
step,
increment
}
}
}
4.5 readonly() - 只读响应式
创建只读的响应式数据:
import { reactive, readonly } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
user: { name: '张三' }
})
// 创建只读版本
const readonlyState = readonly(state)
// ❌ 在开发模式下会警告
readonlyState.count = 1
readonlyState.user.name = '李四'
// ✅ 原始数据仍然可以修改
state.count = 1
return {
state,
readonlyState
}
}
}
4.6 shallowRef() 和 shallowReactive()
创建浅层响应式数据:
import { shallowRef, shallowReactive } from 'vue'
export default {
setup() {
// shallowRef: 只有.value的赋值是响应式的
const state = shallowRef({
count: 0,
user: { name: '张三' }
})
// ✅ 响应式
state.value = { count: 1, user: { name: '李四' } }
// ❌ 不响应式
state.value.count = 2
state.value.user.name = '王五'
// shallowReactive: 只有根级别属性是响应式的
const shallowState = shallowReactive({
count: 0,
user: { name: '张三' }
})
// ✅ 响应式
shallowState.count = 1
// ❌ 不响应式
shallowState.user.name = '李四'
return {
state,
shallowState
}
}
}
性能优化场景:
- 大型数据结构,只需要监听根级别变化
- 与第三方库集成,避免深度响应式的性能开销
5. setup函数深入解析
5.1 setup函数的执行时机
export default {
beforeCreate() {
console.log('1. beforeCreate')
},
// setup在beforeCreate之前执行
setup() {
console.log('0. setup执行')
// 此时还无法访问this
console.log(this) // undefined
return {}
},
created() {
console.log('2. created')
}
}
重要提醒:
setup
执行时组件实例还未创建,所以无法访问this
setup
中不能使用其他选项式API(如data、computed、methods等)- 其他选项式API可以访问
setup
返回的数据
5.2 setup参数详解
export default {
props: {
title: String,
count: {
type: Number,
default: 0
}
},
emits: ['update', 'delete'],
setup(props, context) {
// 第一个参数:props(响应式的)
console.log('Props:', props)
console.log('Title:', props.title)
// props是响应式的,可以监听变化
watch(() => props.count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
// ❌ 不要解构props,会失去响应式
// const { title, count } = props
// ✅ 如果需要解构,使用toRefs
const { title, count } = toRefs(props)
// 第二个参数:context
const { attrs, slots, emit, expose } = context
// attrs: 非props的属性
console.log('Attrs:', attrs)
// slots: 插槽
console.log('Slots:', slots)
// emit: 触发事件
const handleUpdate = () => {
emit('update', 'new value')
}
// expose: 暴露组件方法给父组件
const internalMethod = () => {
console.log('Internal method')
}
// 暴露给父组件使用
expose({
internalMethod
})
return {
handleUpdate
}
}
}
5.3 与Options API混用
Vue3完全兼容Vue2的Options API,可以混合使用:
export default {
// Options API
data() {
return {
optionsCount: 0
}
},
computed: {
optionsDouble() {
return this.optionsCount * 2
}
},
// Composition API
setup() {
const compositionCount = ref(0)
const compositionDouble = computed(() => compositionCount.value * 2)
return {
compositionCount,
compositionDouble
}
},
methods: {
// 可以访问setup返回的数据
logData() {
console.log('Options count:', this.optionsCount)
console.log('Composition count:', this.compositionCount)
}
}
}
混用规则:
- setup返回的数据可以在Options API中通过this访问
- Options API中的数据无法在setup中访问
- 避免在同一个组件中混用,选择一种风格坚持使用
5.4 setup中的this和生命周期
export default {
setup() {
// ❌ setup中没有this
console.log(this) // undefined
// ✅ 使用组合式生命周期
onMounted(() => {
console.log('Component mounted')
})
onUpdated(() => {
console.log('Component updated')
})
onUnmounted(() => {
console.log('Component unmounted')
})
return {}
}
}
5.5 异步setup
注意:setup
不能是async函数
export default {
// ❌ 错误:setup不能是async
async setup() {
const data = await fetchData()
return { data }
},
// ✅ 正确:在setup内部处理异步
setup() {
const data = ref(null)
const loading = ref(true)
const fetchData = async () => {
try {
loading.value = true
const response = await api.getData()
data.value = response.data
} catch (error) {
console.error('Failed to fetch data:', error)
} finally {
loading.value = false
}
}
// 组件挂载时获取数据
onMounted(fetchData)
return {
data,
loading,
fetchData
}
}
}
如果确实需要异步setup,可以配合Suspense使用:
<!-- 父组件 -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
// AsyncComponent.vue
export default {
async setup() {
// 这里可以使用async,但需要配合Suspense
const data = await fetchData()
return { data }
}
}
6. ref和reactive详解
6.1 ref的深入理解
ref
是Vue3中创建响应式数据的基础函数:
import { ref, isRef, unref } from 'vue'
export default {
setup() {
// 基本类型
const count = ref(0)
const message = ref('hello')
const isVisible = ref(true)
// 复杂类型
const user = ref({
name: '张三',
age: 25,
hobbies: ['读书', '游泳']
})
// 判断是否为ref
console.log(isRef(count)) // true
console.log(isRef(message)) // true
console.log(isRef(123)) // false
// 获取ref的值(如果是ref返回.value,否则返回原值)
console.log(unref(count)) // 0
console.log(unref(123)) // 123
// ref的自动解包
const nestedRef = ref({
count: ref(1), // 嵌套的ref会自动解包
user: ref({ name: '李四' })
})
console.log(nestedRef.value.count) // 1,不需要.value.count.value
return {
count,
message,
isVisible,
user,
nestedRef
}
}
}
ref的自动解包规则:
<template>
<!-- 在模板中自动解包 -->
<div>{
{ count }}</div> <!-- 不需要count.value -->
<!-- 在响应式对象中自动解包 -->
<div>{
{ state.count }}</div> <!-- 如果state.count是ref,会自动解包 -->
</template>
<script>
export default {
setup() {
const count = ref(0)
// 在JavaScript中需要.value
console.log(count.value)
// 在响应式对象中会自动解包
const state = reactive({
count: count // 这里count会自动解包
})
console.log(state.count) // 0,不需要state.count.value
return {
count,
state
}
}
}
</script>
6.2 reactive的深入理解
reactive
用于创建深度响应式对象:
import { reactive, isReactive, toRaw, markRaw } from 'vue'
export default {
setup() {
// 创建响应式对象
const state = reactive({
count: 0,
user: {
name: '张三',
profile: {
age: 25,
city: '北京'
}
},
items: [
{ id: 1, name: 'item1' },
{ id: 2, name: 'item2' }
],
map: new Map([
['key1', 'value1'],
['key2', 'value2']
]),
set: new Set([1, 2, 3])
})
// 判断是否为响应式对象
console.log(isReactive(state)) // true
console.log(isReactive(state.user)) // true(嵌套对象也是响应式的)
console.log(isReactive(state.items)) // true
// 获取原始对象
const raw = toRaw(state)
console.log(raw === state) // false,state是代理对象
// 标记对象为非响应式
const nonReactiveObj = markRaw({
expensiveData: new Array(10000).fill(0)
})
state.nonReactive = nonReactiveObj
console.log(isReactive(state.nonReactive)) // false
// 操作响应式数据
const updateData = () => {
// 对象属性
state.count++
state.user.name = '李四'
state.user.profile.age = 26
// 数组操作
state.items.push({ id: 3, name: 'item3' })
state.items[0].name = 'updated item1'
// Map操作
state.map.set('key3', 'value3')
state.map.delete('key1')
// Set操作
state.set.add(4)
state.set.delete(1)
// 动态添加属性
state.newProperty = 'new value' // Vue3中这是响应式的
}
return {
state,
updateData
}
}
}
6.3 ref vs reactive选择指南
export default {
setup() {
// ✅ 使用ref的场景
// 1. 基本类型数据
const count = ref(0)
const message = ref('')
const isLoading = ref(false)
// 2. 需要替换整个对象的引用
const user = ref({ name: '张三' })
// 可以完全替换
user.value = { name: '李四', age: 25 }
// 3. 需要在模板中传递给组件
const currentUser = ref(null)
// <UserComponent :user="currentUser" />
// ✅ 使用reactive的场景
// 1. 复杂的对象结构,不需要替换引用
const form = reactive({
username: '',
password: '',
email: '',
profile: {
age: null,
city: ''
}
})
// 2. 本地组件状态
const uiState = reactive({
isMenuOpen: false,
selectedTab: 'home',
modals: {
user: false,
settings: false
}
})
// ❌ 避免的用法
// 1. 不要解构reactive
// const { isMenuOpen, selectedTab } = uiState // 失去响应式
// 2. 不要将reactive对象赋值给ref
// const state = ref(reactive({ count: 0 })) // 不推荐
return {
count,
message,
isLoading,
user,
currentUser,
form,
uiState
}
}
}
选择原则:
- 基本类型:使用
ref
- 对象类型且需要替换引用:使用
ref
- 对象类型且不需要替换引用:使用
reactive
- 需要解构的对象:使用
ref
或配合toRefs
6.4 响应式工具函数
import {
ref,
reactive,
isRef,
isReactive,
isReadonly,
isProxy,
toRaw,
markRaw,
shallowRef,
shallowReactive,
readonly,
shallowReadonly
} from 'vue'
export default {
setup() {
const refValue = ref(0)
const reactiveValue = reactive({ count: 0 })
const readonlyValue = readonly(reactiveValue)
// 类型检查
console.log('isRef:', isRef(refValue)) // true
console.log('isReactive:', isReactive(reactiveValue)) // true
console.log('isReadonly:', isReadonly(readonlyValue)) // true
console.log('isProxy:', isProxy(reactiveValue)) // true
// 获取原始值
console.log('toRaw:', toRaw(reactiveValue))
// 创建非响应式对象
const rawObj = markRaw({
heavyData: new Array(1000000).fill(0)
})
// 浅层响应式
const shallowState = shallowReactive({
count: 0,
deep: { value: 1 }
})
// 浅层只读
const shallowReadonlyState = shallowReadonly({
count: 0,
deep: { value: 1 }
})
return {
refValue,
reactiveValue,
readonlyValue,
rawObj,
shallowState,
shallowReadonlyState
}
}
}
6.5 响应式最佳实践
// 自定义Hook示例
function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count: readonly(count), // 只读,防止外部直接修改
increment,
decrement,
reset
}
}
// 复杂状态管理
function useFormState() {
const form = reactive({
data: {
username: '',
email: '',
password: ''
},
errors: {},
touched: {},
isSubmitting: false
})
const setField = (field, value) => {
form.data[field] = value
// 清除该字段的错误
if (form.errors[field]) {
delete form.errors[field]
}
}
const setError = (field, message) => {
form.errors[field] = message
}
const setTouched = (field) => {
form.touched[field] = true
}
const isFieldValid = (field) => {
return !form.errors[field] && form.touched[field]
}
const isFormValid = computed(() => {
return Object.keys(form.errors).length === 0 &&
Object.keys(form.touched).length > 0
})
return {
form: readonly(form),
setField,
setError,
setTouched,
isFieldValid,
isFormValid
}
}
export default {
setup() {
const { count, increment, decrement, reset } = useCounter(10)
const { form, setField, setError, isFormValid } = useFormState()
return {
count,
increment,
decrement,
reset,
form,
setField,
setError,
isFormValid
}
}
}
7. computed和watch新用法
7.1 computed组合式API
Vue3的computed
更加灵活和强大:
import { ref, computed, readonly } from 'vue'
export default {
setup() {
const firstName = ref('张')
const lastName = ref('三')
// 基础计算属性(只读)
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 可写计算属性
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value) {
const names = value.split(' ')
firstName.value = names[0] || ''
lastName.value = names[1] || ''
}
})
// 复杂计算属性
const users = ref([
{ id: 1, name: '张三', age: 25, active: true },
{ id: 2, name: '李四', age: 30, active: false },
{ id: 3, name: '王五', age: 35, active: true }
])
const activeUsers = computed(() => {
return users.value.filter(user => user.active)
})
const userStats = computed(() => {
const total = users.value.length
const active = activeUsers.value.length
const averageAge = users.value.reduce((sum, user) => sum + user.age, 0) / total
return {
total,
active,
inactive: total - active,
averageAge: Math.round(averageAge)
}
})
// 计算属性的依赖是响应式的
const expensiveValue = computed(() => {
console.log('计算 expensiveValue') // 只有依赖变化时才会重新计算
return users.value.map(user => ({
...user,
displayName: `${user.name} (${user.age}岁)`
}))
})
return {
firstName,
lastName,
fullName,
fullNameWritable,
users,
activeUsers,
userStats,
expensiveValue
}
}
}
7.2 watch深入解析
Vue3的watch
提供了更多的选项和更强的功能:
import { ref, reactive, watch, watchEffect, nextTick } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('hello')
const user = reactive({
name: '张三',
profile: {
age: 25,
city: '北京'
}
})
// 1. 监听单个ref
watch(count, (newValue, oldValue) => {
console.log(`count changed: ${oldValue} -> ${newValue}`)
})
// 2. 监听多个源
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
console.log('Multiple values changed:')
console.log(`count: ${oldCount} -> ${newCount}`)
console.log(`message: ${oldMessage} -> ${newMessage}`)
})
// 3. 监听响应式对象
watch(user, (newUser, oldUser) => {
console.log('User object changed:', newUser)
}, {
deep: true // 深度监听
})
// 4. 监听对象的特定属性
watch(() => user.name, (newName, oldName) => {
console.log(`User name changed: ${oldName} -> ${newName}`)
})
// 5. 监听嵌套属性
watch(() => user.profile.age, (newAge, oldAge) => {
console.log(`User age changed: ${oldAge} -> ${newAge}`)
})
// 6. 立即执行的监听器
watch(count, (newValue) => {
console.log('Count immediate:', newValue)
}, {
immediate: true // 立即执行一次
})
// 7. 回调的触发时机
watch(count, async (newValue) => {
console.log('Before DOM update:', newValue)
await nextTick()
console.log('After DOM update:', newValue)
}, {
flush: 'pre' // 'pre' | 'post' | 'sync'
})
// 8. 停止监听
const stopWatcher = watch(message, (newValue) => {
console.log('Message changed:', newValue)
// 某些条件下停止监听
if (newValue === 'stop') {
stopWatcher()
}
})
// 9. 监听器的返回值处理
watch(count, async (newValue) => {
console.log('Async operation start')
try {
const result = await fetch(`/api/data/${newValue}`)
const data = await result.json()
console.log('Data loaded:', data)
} catch (error) {
console.error('Failed to load data:', error)
}
})
return {
count,
message,
user
}
}
}
7.3 watchEffect的强大功能
watchEffect
自动追踪其依赖并在依赖变化时重新执行:
import { ref, watchEffect, watchPostEffect, watchSyncEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const enabled = ref(true)
const url = ref('/api/users')
// 1. 基础watchEffect
watchEffect(() => {
console.log(`Count is: ${count.value}`)
// 自动追踪count的变化
})
// 2. 条件性追踪
watchEffect(() => {
if (enabled.value) {
console.log(`Enabled count: ${count.value}`)
// 只有当enabled为true时才追踪count
}
})
// 3. 清理副作用
watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log('Timer executed')
}, 1000)
// 清理函数
onInvalidate(() => {
clearTimeout(timer)
console.log('Timer cleared')
})
})
// 4. 异步操作
watchEffect(async (onInvalidate) => {
let cancelled = false
onInvalidate(() => {
cancelled = true
})
try {
const response = await fetch(url.value)
// 检查是否已被取消
if (!cancelled) {
const data = await response.json()
console.log('Data loaded:', data)
}
} catch (error) {
if (!cancelled) {
console.error('Failed to load data:', error)
}
}
})
// 5. 停止监听
const stop = watchEffect(() => {
console.log('This will stop automatically')
if (count.value >= 10) {
stop()
}
})
// 6. 调试依赖追踪
watchEffect(() => {
console.log(`Count: ${count.value}, URL: ${url.value}`)
}, {
onTrack(e) {
console.log('Tracked:', e)
},
onTrigger(e) {
console.log('Triggered:', e)
}
})
return {
count,
enabled,
url
}
}
}
7.4 watch vs watchEffect
export default {
setup() {
const searchText = ref('')
const category = ref('all')
const results = ref([])
// 使用watch:明确指定依赖
watch([searchText, category], async ([newSearch, newCategory]) => {
if (newSearch.trim()) {
results.value = await searchItems(newSearch, newCategory)
} else {
results.value = []
}
})
// 使用watchEffect:自动追踪依赖
watchEffect(async () => {
if (searchText.value.trim()) {
results.value = await searchItems(searchText.value, category.value)
} else {
results.value = []
}
})
return {
searchText,
category,
results
}
}
}
选择指南:
- watch:当你需要明确控制哪些状态变化应该触发副作用时
- watchEffect:当副作用函数只依赖其内部使用的响应式状态时
7.5 高级监听模式
// 防抖监听
function useDebouncedWatch(source, callback, options = {}) {
const { delay = 300, ...watchOptions } = options
let timeoutId
return watch(source, (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
callback(...args)
}, delay)
}, watchOptions)
}
// 节流监听
function useThrottledWatch(source, callback, options = {}) {
const { delay = 300, ...watchOptions } = options
let lastExecTime = 0
return watch(source, (...args) => {
const now = Date.now()
if (now - lastExecTime >= delay) {
lastExecTime = now
callback(...args)
}
}, watchOptions)
}
// 使用示例
export default {
setup() {
const searchInput = ref('')
const scrollPosition = ref(0)
// 防抖搜索
useDebouncedWatch(searchInput, (newValue) => {
console.log('Search for:', newValue)
// 执行搜索
}, { delay: 500 })
// 节流滚动
useThrottledWatch(scrollPosition, (newPosition) => {
console.log('Scroll position:', newPosition)
// 处理滚动
}, { delay: 100 })
return {
searchInput,
scrollPosition
}
}
}
8. 生命周期组合式API
8.1 生命周期对比
Vue3提供了与Vue2生命周期对应的组合式API:
// Vue2 Options API vs Vue3 Composition API
export default {
// Vue2生命周期
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
beforeUnmount() { // Vue3中重命名
console.log('beforeUnmount')
},
unmounted() { // Vue3中重命名
console.log('unmounted')
},
// Vue3 Composition API
setup() {
// setup执行时相当于beforeCreate和created之间
console.log('setup')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
return {}
}
}
8.2 生命周期实际应用
import {
ref,
onMounted,
onUpdated,
onUnmounted,
onBeforeUnmount,
onActivated,
onDeactivated,
onErrorCaptured
} from 'vue'
export default {
setup() {
const element = ref(null)
const data = ref([])
const loading = ref(true)
// 组件挂载后执行
onMounted(async () => {
console.log('组件已挂载,DOM可用')
// 获取DOM元素
console.log('Element:', element.value)
// 初始化第三方库
initializeThirdPartyLibrary(element.value)
// 获取初始数据
try {
const response = await fetch('/api/data')
data.value = await response.json()
} catch (error) {
console.error('数据加载失败:', error)
} finally {
loading.value = false
}
// 添加事件监听器
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleScroll)
})
// 组件更新后执行
onUpdated(() => {
console.log('组件已更新,DOM已重新渲染')
// 如果需要在DOM更新后执行某些操作
updateThirdPartyLibrary()
})
// 组件卸载前清理
onBeforeUnmount(() => {
console.log('组件即将卸载,开始清理工作')
// 清理定时器
clearInterval(timer)
// 取消网络请求
abortController.abort()
})
// 组件卸载后清理
onUnmounted(() => {
console.log('组件已卸载')
// 移除事件监听器
window.removeEventListener('resize', handleResize)
window.removeEventListener('scroll', handleScroll)
// 清理第三方库
destroyThirdPartyLibrary()
})
// Keep-alive组件激活时
onActivated(() => {
console.log('组件被激活')
// 重新获取数据或重新启动定时器
})
// Keep-alive组件停用时
onDeactivated(() => {
console.log('组件被停用')
// 暂停某些操作,保存状态
})
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err)
console.error('错误组件:', instance)
console.error('错误信息:', info)
// 错误上报
reportError(err, { instance, info })
// 返回false阻止错误继续传播
return false
})
// 定义一些工具函数
let timer = null
const abortController = new AbortController()
const handleResize = () => {
console.log('窗口大小改变')
}
const handleScroll = () => {
console.log('页面滚动')
}
const initializeThirdPartyLibrary = (el) => {
// 初始化第三方库
}
const updateThirdPartyLibrary = () => {
// 更新第三方库
}
const destroyThirdPartyLibrary = () => {
// 销毁第三方库
}
const reportError = (error, context) => {
// 错误上报逻辑
}
return {
element,
data,
loading
}
}
}
8.3 多个生命周期钩子
在同一个组件中可以多次调用同一个生命周期钩子:
export default {
setup() {
// 第一个onMounted
onMounted(() => {
console.log('First onMounted')
initializeComponent()
})
// 第二个onMounted
onMounted(() => {
console.log('Second onMounted')
loadData()
})
// 第三个onMounted
onMounted(() => {
console.log('Third onMounted')
setupEventListeners()
})
// 多个onUnmounted用于不同的清理工作
onUnmounted(() => {
console.log('清理数据')
})
onUnmounted(() => {
console.log('清理事件监听器')
})
onUnmounted(() => {
console.log('清理第三方库')
})
return {}
}
}
8.4 生命周期在自定义Hook中的使用
// 自定义Hook:useWindowSize
function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
const updateSize = () => {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => {
window.addEventListener('resize', updateSize)
})
onUnmounted(() => {
window.removeEventListener('resize', updateSize)
})
return {
width: readonly(width),
height: readonly(height)
}
}
// 自定义Hook:useInterval
function useInterval(callback, delay) {
const savedCallback = ref(callback)
// 更新回调函数
watch(() => callback, (newCallback) => {
savedCallback.value = newCallback
})
onMounted(() => {
if (delay !== null) {
const timer = setInterval(() => {
savedCallback.value()
}, delay)
onUnmounted(() => {
clearInterval(timer)
})
}
})
}
// 自定义Hook:useEventListener
function useEventListener(target, event, handler, options) {
onMounted(() => {
target.addEventListener(event, handler, options)
})
onUnmounted(() => {
target.removeEventListener(event, handler, options)
})
}
// 在组件中使用
export default {
setup() {
// 使用窗口大小Hook
const { width, height } = useWindowSize()
// 使用定时器Hook
const count = ref(0)
useInterval(() => {
count.value++
}, 1000)
// 使用事件监听Hook
useEventListener(window, 'keydown', (e) => {
if (e.key === 'Escape') {
console.log('Escape pressed')
}
})
return {
width,
height,
count
}
}
}
8.5 生命周期最佳实践
export default {
setup() {
// 1. 数据初始化
const data = ref([])
const loading = ref(true)
const error = ref(null)
// 2. 异步数据获取
const fetchData = async () => {
try {
loading.value = true
error.value = null
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err.message
console.error('数据获取失败:', err)
} finally {
loading.value = false
}
}
// 3. 组件挂载时获取数据
onMounted(() => {
fetchData()
})
// 4. 清理资源
const cleanup = () => {
// 清理定时器、事件监听器、网络请求等
}
onUnmounted(cleanup)
// 5. 错误处理
onErrorCaptured((err, instance, info) => {
console.error('组件错误:', err)
// 错误上报
if (process.env.NODE_ENV === 'production') {
// 发送错误到监控服务
}
return false
})
return {
data,
loading,
error,
fetchData
}
}
}
9. 模板语法与指令更新
9.1 Vue3模板语法改进
Vue3在保持向后兼容的同时,对模板语法进行了一些优化:
<template>
<!-- Vue3支持多个根元素 -->
<header>
<h1>{
{ title }}</h1>
</header>
<main>
<!-- 插值表达式 -->
<p>{
{ message }}</p>
<p>{
{ rawHtml }}</p>
<div v-html="rawHtml"></div>
<!-- 属性绑定 -->
<div v-bind:id="dynamicId" v-bind:class="classObject">
<img :src="imageSrc" :alt="imageAlt">
</div>
<!-- 事件监听 -->
<button @click="handleClick">点击我</button>
<button @click="handleClickWithArgs('hello', $event)">传参点击</button>
<!-- 条件渲染 -->
<div v-if="showContent">显示的内容</div>
<div v-else>隐藏时显示的内容</div>