黑马Vue_day11:Vue3的watch,ref和reactive,vue2->vue3,defineOptions、Model

第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 回调!

正确解决方案:

  1. 显式开启 { deep: true } :

    JavaScript

    javascript 复制代码
    watch(user, () => {
      console.log('用户数据变了')
    }, { deep: true }) // ✅ 开启后修改 age 会触发
  2. 或者监听 .value (监听 ref 内部的 Proxy 对象时,会自动开启深度监听):

    JavaScript

    javascript 复制代码
    watch(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 就会触发回调了
)

三、 实战使用避坑口诀

  1. 听基本类型 ref :直接传变量名 watch(count, cb)。

  2. 听对象特定属性 :一律写成函数 watch(() => obj.prop, cb)。

  3. 听 ref 对象 :记得带上 { deep: true },或者直接监听 watch(obj.value, cb)。

  4. 听 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 的致命陷阱:

    1. 不能对 reactive 对象直接重新赋值 (如 state = { a: 1 }),这会导致响应式丢失。

    2. 不能直接对 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() 函数的局限性:

  1. 冗余代码多 :定义了一遍变量,末尾还得在 return { ... } 里再抄一遍。

  2. 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) :

    1. 不能直接重赋值 :如果 let state = reactive({ a: 1 }),后续写 state = { a: 2 } 会导致原有的 Proxy 代理断开,响应式彻底失效。

    2. 不能解构 :写 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

    javascript 复制代码
    const 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 一样自然。
相关推荐
前端snow14 分钟前
ai agent --- 异步处理之 Rabbit MQ
前端
念何架构之路18 分钟前
zap扩展生态与总结
java·前端·数据库
独孤九剑打醒他33 分钟前
【原创开源】【概念设计】源 - 栅 - 漏 - 栅 - 源 横向双栅 MOS,低压交流多值逻辑芯片探索
前端·其他·架构·开源·硬件工程
mantou1321 小时前
我给 AI Agent 做了个「油猴」:让 Claude Code / Codex 直接用你已登录的浏览器
前端·javascript·后端
默_笙1 小时前
🍕 一个主编、三个工种、两本手册:搭一支 AI 调研队
前端·javascript
纸片人1 小时前
Blender 建模 + Three.js 展示:和 AI 一起做一个光储充超充站数字孪生大屏
前端
智能直播1 小时前
网络RTMP拉流不卡顿、声音不同步?一文讲透缓冲区、时间戳与MEDAI V2实战调优
前端
第七页独白1 小时前
汽车零件厂如何通过 QMS 真正落地 IATF 16949——QMS软件系统:品质检验-内审稽核-8d客诉管理:全星质量管理软件系统
java·前端·数据库
纸片人1 小时前
只用 three.js + OpenStreetMap,手搓一个「成都城市 3D」数据大屏
前端
星若生辉1 小时前
Hugo 静态站点搭建|从本地调试到上线部署实操指南
前端