第1个
Vue3的watch
javascript
watch(state, ()=>{
console.log('数据变化了')
})
const changeStateByCount = ()=>{
// 直接修改不会引发回调执行
state.value.count++
}
这句话的意思是:直接改 state.value.count 的值,watch 监听的回调函数不会被触发,控制台不会打印"数据变化了"。
要搞懂为什么"直接修改不会引发回调执行",你需要了解 Vue 3 中 watch 的监听机制。
一、 核心原因:你监听的是"引用",还是"里面的属性"?
假设你的 state 是这样定义的:
csharp
const state = ref({ count: 0 })
当你写 watch(state, () => { ... }) 时,Vue 默认只监听 state 这个变量指向的"内存地址/引用" 是否改变。
1. 为什么 state.value.count++ 不会触发?
-
state.value.count++修改的只是对象内部的一个属性 ,state.value本身依然是同一个对象(内存地址没变)。 -
因为
state的引用没有变,所以 Vue 认为state"没有发生变化",自然就不会去执行回调函数。
2. 什么才叫"非直接修改 / 改变了引用"?
如果你直接把整个 state.value 替换成一个全新的对象 ,内存地址变了,watch 就会触发:
ini
const changeStateByCount = () => {
// 重新赋值一个新对象(改变了引用)
state.value = { count: state.value.count + 1 }
// 此时 watch(state, ...) 的回调【会】执行!
}
二、 怎么修改代码才能让它监听属性的变化?
如果你希望修改 count 时能够触发 watch,有两种标准的解决办法:
方案 1:开启深度监听 { deep: true }(推荐监听整个对象时使用)
添加第三个参数 { deep: true },告诉 Vue:"请深入到这个对象的每一个内部属性去监听"。
JavaScript
javascript
watch(state, () => {
console.log('数据变化了')
}, { deep: true }) // 开启深度监听
const changeStateByCount = () => {
state.value.count++ // 此时修改 count 就会打印"数据变化了"!
}
方案 2:使用 getter 函数精确监听具体属性(推荐只关注某个属性时使用)
如果你只关心 count 这一个属性,推荐写成函数的形式(getter 函数):
javascript
// 明确告诉 Vue 只监听 state.value.count 的变化
watch(() => state.value.count, () => {
console.log('数据变化了')
})
const changeStateByCount = () => {
state.value.count++ // 此时也会正常打印"数据变化了"!
}
三、 补充说明:如果是 reactive 定义的呢?
如果你使用的是 reactive 而不是 ref:
javascript
const state = reactive({ count: 0 })
// 监听 reactive 对象时,Vue 默认会自动开启 deep: true
watch(state, () => {
console.log('数据变化了')
})
const changeStateByCount = () => {
state.count++ // 这种情况下回调【会】正常执行!
}
总结口诀:
- 监听
ref定义的对象,默认只看引用地址 。不加{ deep: true },只改内部属性(如count++)就是"直接修改",不会触发监听。
第2个
Vue watch,ref和reactive坑点
在 Vue 3 中,使用 watch 监听 ref 和 reactive 对象时,由于它们的底层代理实现不同,很容易在深度监听 和获取新旧值(newValue / oldValue) 上踩坑。
下表总结了它们的核心区别与常见陷阱:
一、 核心区别对比
| 监听对象类型 | watch 监听的写法 | 是否默认开启深度监听 (deep) | 新旧值 (oldValue) 是否有效 |
|---|---|---|---|
ref 基本类型 |
watch(num, cb) |
否(基本类型无需深度监听) | 有效,能准确区分新旧值 |
ref 对象类型 |
watch(obj, cb) |
否 (默认只监听 .value 的引用替换) |
无效 (由于对象引用相同,newValue 和 oldValue 指向同一地址) |
reactive 对象 |
watch(obj, cb) |
是(强制开启,无法手动关闭) | 无效 (newValue 和 oldValue 永远指向相同的新对象) |
| 对象的某个属性 | watch(() => obj.a, cb) |
否(若属性是对象,修改其内部属性默认不触发) | 视情况(若属性是基本类型则有效) |
二、 最容易踩的 4 个坑点及正确写法
坑点 1:监听 ref 对象时,只修改属性不触发回调
错误示范:
JavaScript
javascript
const user = ref({ name: '张三', age: 18 })
// 默认只监听 user.value 内存地址改变
watch(user, () => {
console.log('用户数据变了')
})
user.value.age = 19 // ❌ 不会触发 watch 回调!
正确解决方案:
-
显式开启
{ deep: true }:JavaScript
javascriptwatch(user, () => { console.log('用户数据变了') }, { deep: true }) // ✅ 开启后修改 age 会触发 -
或者监听
.value(监听ref内部的Proxy对象时,会自动开启深度监听):JavaScript
javascriptwatch(user.value, () => { console.log('用户数据变了') }) // ✅ 会触发
坑点 2:监听 reactive 对象时,oldValue 与 newValue 完全相等
表现:
无论你怎么修改 reactive 对象的内部属性,拿到的 oldValue 永远和 newValue 一模一样(打印出来都是修改后的值)。
JavaScript
scss
const state = reactive({ count: 0 })
watch(state, (newVal, oldVal) => {
console.log(newVal.count, oldVal.count) // ❌ 修改后两次打印都是最新的 count 值
})
state.count++
原因分析:
Vue 内部没有为你深拷贝修改前的旧对象。因为 state 是同一个对象的代理,数据修改后,指向的依然是同一块内存地址。
正确解决方案:
如果你必须对比新旧值的具体变化 ,请使用 Getter 函数 + 深拷贝/展开运算符:
JavaScript
scss
// 返回一个浅拷贝/深拷贝的新对象
watch(
() => ({ ...state }),
(newVal, oldVal) => {
console.log(newVal.count, oldVal.count) // ✅ 可以正常区分新旧值了
}
)
坑点 3:直接监听对象的某个"基本类型属性"报错或无效
错误示范:
JavaScript
javascript
const state = reactive({ count: 0 })
// ❌ 错误写法!把 state.count (相当于数字 0) 直接传给了 watch
watch(state.count, (newVal) => {
console.log('count 变了')
})
报错/警告 :Vue 会警告
Invalid watch source,因为watch的第一个参数必须是ref、reactive对象、Array或Function。
正确解决方案:
监听对象的特定属性时,必须写成 Getter 函数形式 () => state.count:
JavaScript
javascript
watch(
() => state.count,
(newVal, oldVal) => {
console.log('count 变了:', newVal) // ✅ 正常触发,且能精准拿到旧值
}
)
坑点 4:用 Getter 监听"嵌套对象属性"时,内部属性修改失效
错误示范:
JavaScript
javascript
const state = reactive({
info: { name: '张三' }
})
// 使用了 getter 函数返回 state.info 对象
watch(
() => state.info,
() => {
console.log('info 变了')
}
)
state.info.name = '李四' // ❌ 不会触发!因为 state.info 对象的引用没变
原因分析:
一旦给 watch 传入的是 Getter 函数 ,Vue 默认不会 自动开启深度监听(即使 state 是 reactive)。
正确解决方案:
需要手动追加 { deep: true }:
JavaScript
javascript
watch(
() => state.info,
() => {
console.log('info 变了')
},
{ deep: true } // ✅ 手动加上 deep,修改 name 就会触发回调了
)
三、 实战使用避坑口诀
-
听基本类型
ref:直接传变量名watch(count, cb)。 -
听对象特定属性 :一律写成函数
watch(() => obj.prop, cb)。 -
听
ref对象 :记得带上{ deep: true },或者直接监听watch(obj.value, cb)。 -
听
reactive对象 :默认就是深度监听,但需要对比新旧值时,记得用() => ({ ...state })做解构。
第3个
Vue2过渡到Vue3
变动速览
1. vue3新增:setup 与 <script setup>
-
最容易忽视的痛点 :在传统的
setup()函数中,千万不能使用this(此时组件实例尚未创建,this是undefined)。 -
<script setup>隐藏优势 :导入的子组件无需在components: {}中手动注册 ,写了import直接在模板里用。
2. ref vs reactive
-
模板自动解包 :
ref在 JS 里必须写.value,但在<template>模板中不需要写.value,Vue 会自动解包。 -
reactive的致命陷阱:-
不能对
reactive对象直接重新赋值 (如state = { a: 1 }),这会导致响应式丢失。 -
不能直接对
reactive对象解构 (如const { msg } = state),也会导致响应式丢失。
-
3. watch 的深浅监听逻辑
-
监听
ref对象 :默认是浅层监听 (只看对象的内存地址变不变)。只有加{ deep: true }或者直接监听watch(state.value, ...),才能监听到内部属性(如count++)的变化。 -
监听
reactive对象 :默认强制开启深度监听 (不需要加deep: true也会生效)。 -
监听对象的某个特定属性 :必须写成 Getter 函数 形式,例如:
watch(() => state.count, (newVal) => {})。
4. 生命周期函数
-
取消了
created和beforeCreate:原先写在created里的初始化代码(如 Ajax 请求),在 Vue 3 中直接写在<script setup>的根层级下即可。 -
销毁钩子改名 :Vue 2 的
beforeDestroy和destroyed,在 Vue 3 中改名为了onBeforeUnmount和onUnmounted。
5. 父子通信与组件暴露
-
编译宏无需导入 :
defineProps、defineEmits、defineExpose、defineOptions、defineModel都是编译器宏(Compiler Macros) ,不需要手写import导入,直接用。 -
<script setup>的封闭性 :父组件通过ref拿到的子组件实例,默认拿不到子组件的任何变量和方法 ,子组件必须用defineExpose明确导出的才能被看到。
6. provide 与 inject 最佳实践
- 数据修改原则 :跨层级传递数据时,如果底层组件需要修改顶层组件的数据,强烈建议把修改数据的方法一并传递过去,由数据提供方(顶层组件)负责修改,确保数据流向清晰。
一、 核心思维演变:从"选项"到"组合"
在 Vue 2 中,你的思维方式是 "填空题" :
把变量填进 data,把函数填进 methods,把监听器填进 watch。
- 痛点:当页面逻辑变复杂时,同一个功能的代码被硬生生拆散在不同的选项中,维护时需要频繁上下翻滚代码。
在 Vue 3 中,你的思维方式变成了 "搭积木" :
不再有 data / methods 的限制,你可以把"登录逻辑"、"购物车逻辑"分别写成独立的小模块,随拿随用。
二、 演进路线:从 setup() 函数到 <script setup> 语法糖
Vue 3 的语法并不是一步到位的,理解它的演进过程能帮你彻底看懂各种新老代码。
第一阶段:原始的 setup() 函数(过渡期写法)
Vue 3 为了兼容 Vue 2,保留了 export default {} 的外壳,但引入了一个全新的入口函数:setup()。
代码段
xml
<template>
<div>
<p>{{ count }}</p>
<button @click="addCount">增加</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'Counter',
// 1. setup 替代了 beforeCreate 和 created
setup() {
// 2. 在 setup 内部定义变量和函数
const count = ref(0)
const addCount = () => {
count.value++
}
// 3. 致命繁琐点:必须 return,模板里才能用!
return {
count,
addCount
}
}
}
</script>
setup() 函数的局限性:
-
冗余代码多 :定义了一遍变量,末尾还得在
return { ... }里再抄一遍。 -
this消失 :在setup()内部是没有this的,不能再像 Vue 2 那样通过this.xx拿属性。
第二阶段:<script setup> 语法糖(现代 Vue 3 标准写法)
为了解决 return 的繁琐问题,官方推出了 <script setup> 编译宏语法。
原理 :在 <script> 标签上加上 setup,编译器在打包时会自动把里面的代码包裹进 setup() 函数中,并自动将所有顶层声明的变量和函数进行 return。
代码段
xml
<template>
<div>
<p>{{ count }}</p>
<button @click="addCount">增加</button>
<!-- 引入的组件直接用,无需 components 注册 -->
<ChildHeader />
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildHeader from './ChildHeader.vue' // 导入即注册!
// 声明即导出!
const count = ref(0)
const addCount = () => {
count.value++
}
</script>
三、 响应式核心:ref 与 reactive 的本质区别
Vue 2 的 data 底层依靠 Object.defineProperty 拦截 getter/setter;而 Vue 3 改用了 ES6 的 Proxy。
这就导致 Vue 3 定义响应式数据时,必须显式调用 ref 或 reactive:
1. ref(万能包装盒)
-
底层原理 :把数据包裹在一个带有
.value属性的对象中(利用类 setter/getter 拦截)。 -
使用规则:
-
在
<script>中 :操作必须加.value(如count.value++)。 -
在
<template>中 :Vue 会自动解包,绝对不能写.value(写成{{ count }}即可)。
-
2. reactive(Proxy 代理对象)
-
底层原理 :使用 ES6
Proxy直接代理整个对象。 -
致命坑点(为什么大家更推荐全用
ref) :-
不能直接重赋值 :如果
let state = reactive({ a: 1 }),后续写state = { a: 2 }会导致原有的 Proxy 代理断开,响应式彻底失效。 -
不能解构 :写
const { a } = state后,a就变成了一个普通变量,失去了响应式。
-
过渡建议 :为了减少大脑负担,一律优先使用
ref。遇到对象类型,ref({ a: 1 })在底层也会自动调用reactive处理,既安全又统一。
四、 监听与计算:computed 与 watch
变化的核心在于:从 Vue 2 的"配置项"变成了 Vue 3 的"函数" 。
1. computed
不再写在 computed: {} 对象里,而是调用 computed() 函数并传入回调:
JavaScript
csharp
import { ref, computed } from 'vue'
const price = ref(10)
const count = ref(2)
// 返回一个只读的 ref 对象
const totalPrice = computed(() => price.value * count.value)
// 在 JS 中访问也要加 .value
console.log(totalPrice.value) // 20
2. watch
Vue 3 的 watch 变得更加精确,但监听规则需要特别注意:
-
监听单个
ref:直接传变量名watch(count, (newVal) => {}) -
监听对象的某个具体属性 :必须写成 Getter 函数:
JavaScript
javascriptconst user = ref({ name: '张三', age: 18 }) // 必须写成 () => user.value.age watch(() => user.value.age, (newAge) => { console.log('年龄变了:', newAge) })
五、 组件通信与生命周期的对应演进
1. 生命周期的转化
Vue 3 废弃了 beforeCreate 和 created,其逻辑直接写在 <script setup> 的根层级下执行 即可(因为 <script setup> 本身就在这个时机运行)。
| Vue 2 选项 | Vue 3 | 变动说明 |
|---|---|---|
beforeCreate / created |
无 | 直接写在 <script setup> 顶级作用域中 |
mounted |
onMounted(() => {}) |
前面统一加 on |
beforeDestroy |
onBeforeUnmount(() => {}) |
改名,语义更贴合 Vue 实例卸载过程 |
destroyed |
onUnmounted(() => {}) |
改名 |
2. 父子通信的变化
Vue 2 中的 props 和 this.$emit,在 <script setup> 中被替换成了两个编译器宏(Compiler Macros):
-
defineProps:接收父组件传来的属性。 -
defineEmits:定义并触发给父组件的事件。
代码段
xml
<!-- 子组件 Child.vue -->
<script setup>
// 1. 声明接收的 props
const props = defineProps({
title: String
})
// 2. 声明要触发的自定义事件
const emit = defineEmits(['update'])
const handleClick = () => {
emit('update', '新数据')
}
</script>
六、 Vue 3 核心改动对照一览
| 功能/概念 | Vue 2 写法 | Vue 3 变动与替代方案 |
|---|---|---|
| 全局挂载 | Vue.prototype.$http = axios |
废弃 $http,推荐导出独立的 API 函数直接 import,或使用 app.config.globalProperties |
| DOM 元素获取 | this.$refs.box |
使用 const box = ref(null) + 模板标签绑定 ref="box" |
| 组件导出访问 | 默认公开所有 data/methods |
<script setup> 默认全部私有 ,需通过 defineExpose({ ... }) 暴露 |
| 过滤器 (Filter) | {{ time 或 formatDate }} |
彻底废弃! 官方建议直接改用普通方法调用或 computed |
| 逻辑复用 | Mixins(容易导致变量命名冲突、来源不明) |
改用 Composables(自定义 Hook) ,利用函数闭包实现类型安全的逻辑复用 |
第4个
defineOptions 和 defineModel
在 Vue 3.3 中引入的 defineOptions 和 defineModel ,是针对 <script setup> 语法糖下两个最痛点问题的"终极补丁"。
它们都是 编译器宏(Compiler Macros) ,这意味着在 <script setup> 中不需要从 vue 中 import 导入,直接用就行!
下面用最直观的方式把这两个 API 的诞生背景、用法和原理讲透:
一、 defineOptions:解决"定义组件自身选项"的痛点
1. 为什么会有这个 API?(诞生背景)
在没有 defineOptions 之前,<script setup> 解决了很多问题,但带来了一个尴尬的局限:
无法轻松定义组件自身的选项(如组件名 name、inheritAttrs 等)。
当时如果你想给组件起个名字,只有两种繁琐的变通方案:
-
方案 A(写两个
<script>标签) :代码段
xml<script> export default { name: 'MyHeader' // 为了起个名字,必须额外写一个普通 script } </script> <script setup> // 复杂的 setup 业务逻辑写在这 </script> -
方案 B :借助第三方插件(如
vite-plugin-vue-setup-extend)。
为了彻底终结为了一个 name 属性不得不写两个 <script> 的尴尬局面,Vue 3.3 引入了 defineOptions。
2. defineOptions 怎么用?
它允许你在 <script setup> 内部,直接用声明式的方式定义任意 Options API(选项式) 的配置。
基本用法:
代码段
xml
<script setup>
// 直接定义组件的选项,不需要写外部的 export default
defineOptions({
name: 'MyHeader', // 1. 自定义组件名称(调试工具/递归调用时很有用)
inheritAttrs: false, // 2. 禁用透传属性(禁止父组件传的 class/style 自动加到根元素上)
// 也可以写其他选项,如 customOptions 等
})
// 下面照常写组件的业务逻辑
import { ref } from 'vue'
const count = ref(0)
</script>
注意事项:
defineOptions 不能用来定义 props、emits、expose 和 slots,因为这些已经有专属的宏(defineProps、defineEmits、defineExpose、defineSlots)来处理了。
二、 defineModel:解决"自定义组件双向绑定 (v-model)"的繁琐
1. 为什么会有这个 API?(诞生背景)
在 Vue 3 中,如果你想让一个自定义组件支持 v-model 双向绑定,写法非常繁琐:
传统 Vue 3 写法(Vue 3.2 及之前):
父组件写:<MyInput v-model="searchText"/>
子组件必须拆成两步处理:接收固定的 modelValue 属性,并在输入时触发 update:modelValue 事件:
代码段
xml
<!-- 子组件 MyInput.vue (传统写法) -->
<script setup>
// 1. 声明接收 modelValue
const props = defineProps(['modelValue'])
// 2. 声明触发 update:modelValue 事件
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
// 3. 手动通知父组件修改数据
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="handleInput" />
</template>
可以看到,为了做一个简单的双向绑定,写了大量的模板代码,而且极易记错事件名字。
2. defineModel 怎么用?
Vue 3.3+(Vue 3.4 正式稳定)引入了 defineModel,它把上面的 props 接收、emit 定义以及手动的逻辑触发打包成了"一行代码" !
现代 Vue 3 写法:
代码段
xml
<!-- 子组件 MyInput.vue (defineModel 写法) -->
<script setup>
// 声明一个绑定,返回的是一个响应式的 ref 对象!
const model = defineModel()
</script>
<template>
<!-- 在子组件内部,可以直接把 model 传给原生的 v-model -->
<input v-model="model" />
</template>
只要子组件的 input 发生变化,父组件绑定给 v-model 的变量会自动发生改变 ,完全不需要你手动写 emit('update:modelValue')!
3. defineModel 的高级用法
① 绑定多个 v-model(具名 v-model)
如果你的组件需要绑定多个值(例如姓名和年龄),父组件写:
<MyForm v-model:age="userAge" v-model:name="username"/>
子组件通过传入字符串参数来绑定指定的名称:
代码段
xml
<!-- 子组件 MyForm.vue -->
<script setup>
// 对应 v-model:name
const name = defineModel('name')
// 对应 v-model:age
const age = defineModel('age')
</script>
<template>
<input v-model="name" placeholder="请输入姓名" />
<input v-model.number="age" placeholder="请输入年龄" />
</template>
② 设置默认值和校验选项
可以像 defineProps 一样,为 defineModel 指定数据类型和默认值:
代码段
xml
<script setup>
// 设置默认值和类型限制
const count = defineModel({
type: Number,
default: 0
})
</script>
③ 处理 修饰符 (Modifiers)
如果父组件使用了修饰符,例如 <MyInput v-model.capitalize="text"/>(首字母大写),子组件可以通过数组解构获取修饰符对象:
代码段
xml
<script setup>
// 解构第二个参数 [modelValue, modelModifiers]
const [model, modifiers] = defineModel({
// 可以利用 set 拦截器,在更新数据前先处理修饰符逻辑
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>
三、 一句话总结速记
| 编译宏 | 一句话作用 | 解决了什么痛点 |
|---|---|---|
defineOptions |
在 <script setup> 里直接写 name、inheritAttrs 等配置。 |
彻底告别了"为了起个组件名,不得不写两个 <script>"的荒谬现象。 |
defineModel |
一行代码搞定自定义组件的 v-model 双向绑定。 |
自动帮你处理 props 接收与 emit('update:xxx') 派发,写代码就像在本地操作 ref 一样自然。 |