Code前端首页关于Code前端联系我们

Vue3 Options API的watch到底怎么用?和Composition API的区别大吗?踩过的坑有哪些?

terry 2小时前 阅读数 28 #Vue

刚开始用Vue3的时候,很多习惯了Options API的老开发者会有点懵:明明Composition API在setup里写逻辑更自由,但有时候还是怀念旧时代data里直接定义变量、methods里写方法、watch里直接监听数据的“清爽感”,甚至不确定Vue3的Options API watch有没有偷偷变了,还有新手开发者,刚接触框架,对两种API的选择、watch的参数细节都摸不清,写出来的监听要么不生效,要么重复触发,要么监听层级不对,今天咱们就把Vue3 Options API的watch彻底讲透,连容易踩的小陷阱都给你拎出来。

Vue3 Options API watch的基础功能和参数

先从最基础的讲起,Vue3的Options API watch虽然和Vue2有相似之处,但细节上确实优化了一些,比如新增了一些默认值,不过核心逻辑还是没变:用来监听响应式数据的变化,变化时执行自定义的回调函数。

监听单个数据:字符串键或函数

和Vue2一样,Options API里的watch是一个对象,对象的可以是你要监听的data、computed、props里的变量名(字符串),也可以是返回要监听值的函数可以是回调函数、包含配置项的对象、或者methods里的方法名

先看最简单的字符串键+回调函数的写法:

data() {
  return {
    userName: '小明',
    userAge: 18
  }
},
watch: {
  // 监听userName的变化
  userName(newVal, oldVal) {
    console.log(`用户名从${oldVal}变成了${newVal}`)
  },
  // 监听userAge的变化,用methods里的方法
  userAge: 'updateUserInfo'
},
methods: {
  updateUserInfo(newVal, oldVal) {
    console.log(`年龄从${oldVal}变成了${newVal}`)
  }
}

这种写法最常用,但只能监听顶层的响应式变量,如果要监听对象的某个属性、数组的某个元素,或者更复杂的表达式,就需要用返回值的函数形式:

data() {
  return {
    user: {
      name: '小红',
      address: {
        city: '北京',
        district: '朝阳区'
      }
    },
    hobbies: ['读书', '旅行']
  }
},
watch: {
  // 监听user.address.city的变化
  'user.address.city': function(newVal, oldVal) {
    console.log(`所在城市从${oldVal}变成了${newVal}`)
  },
  // 监听user对象的整体变化(浅监听),或者某个计算属性
  // 也可以监听数组的长度变化
  () => this.user.address.district + this.hobbies.length, 
  function(newVal, oldVal) {
    console.log(`触发了复合条件的变化:${newVal}`)
  }
}

这里提一下,虽然Vue3的Proxy代理可以深度追踪对象,但用字符串键直接写嵌套属性(user.address.city')也是完全支持的,不需要像某些早期文章说的那样必须用函数,但函数形式的灵活性更高,比如可以结合其他变量做复合监听。

三个核心配置项:immediate、deep、flush

光有回调函数不够,很多场景需要调整监听的触发时机、深度,这时候就要用包含配置项的对象来写值,回调函数放在handler属性里,这三个配置项是Vue3 watch(包括Composition API的watch)最核心的,也是最容易踩坑的地方。

immediate:组件挂载后立刻触发一次

默认情况下,watch只有在监听的数据首次变化之后才会触发回调,但有时候我们需要在组件刚挂载、数据还是初始值的时候,就执行一次逻辑(比如根据初始的city加载对应的district列表),这时候就可以设置immediate: true。

watch: {
  'user.address.city': {
    handler(newVal) {
      // 这里可以调用接口,根据newVal加载区县
      console.log(`加载${newVal}的区县列表`)
    },
    immediate: true // 组件挂载后立刻执行,此时newVal是初始值'北京',oldVal是undefined
  }
}

这里要注意,设置immediate: true的时候,第一次触发回调的oldVal是undefined,因为数据还没有发生过“从旧到新”的变化。

deep:深度监听对象或数组的变化

刚才提到,用字符串键直接写顶层对象(比如user)的话,默认是浅监听——只有当user的引用发生变化(比如重新赋值了一个新的对象:this.user = {name: '小刚'})时才会触发回调,而如果只是修改user里的某个属性(比如this.user.name = '小刚'),或者数组里的某个元素(比如this.hobbies[0] = '跑步')、数组的长度(比如push、pop),浅监听是不会生效的。 这时候就要设置deep: true,开启深度监听,不过深度监听会有性能损耗,因为Vue需要递归遍历整个对象或数组,所以要谨慎使用——如果只需要监听对象的某个属性,直接用'user.name'的字符串键或者对应的函数就行,不要给整个对象开deep。

watch: {
  // 浅监听user,只有引用变才触发
  user: {
    handler(newVal, oldVal) {
      console.log('user引用变化了')
    }
  },
  // 深度监听hobbies,数组元素、长度变都触发
  hobbies: {
    handler(newVal, oldVal) {
      console.log('hobbies变化了')
    },
    deep: true
  }
}

还要注意一个细节:开启deep:true之后,修改对象或数组的内容时,回调函数里的newVal和oldVal是同一个引用(因为修改的是同一个对象/数组,没有生成新的引用),所以无法通过对比newVal和oldVal的属性来判断具体哪里变了,如果需要知道具体的变化项,可以用watchEffect(不过Composition API的watchEffect更常用),或者手动在回调里做对比。

flush:调整回调的触发时机

Vue3的响应式更新是批量的——当你在一个tick里修改多个数据时,Vue不会立刻更新DOM,也不会立刻触发所有的watch回调,而是会等到下一个tick再统一执行,这样可以避免重复渲染和计算,提升性能。 但有时候我们需要调整watch回调的触发时机,比如在DOM更新之前执行(比如获取旧DOM的高度),或者在DOM更新之后同步执行(比如获取新DOM的高度),这时候就可以用flush配置项,它有三个可选值:

  • 'pre':默认值,在DOM更新之前触发回调
  • 'post':在DOM更新之后触发回调
  • 'sync':在数据变化同步触发回调(跳过批量更新,可能会影响性能,不推荐频繁使用)

举个例子,比如我们有一个输入框,输入内容后要更新一个文本,同时获取更新后文本的高度:

<template>
  <input v-model="inputText" placeholder="输入内容">
  <p ref="textRef">{{ inputText }}</p>
</template>
<script>
export default {
  data() {
    return {
      inputText: ''
    }
  },
  watch: {
    inputText: {
      handler(newVal) {
        // 默认flush是pre,此时DOM还没更新,textRef的高度是旧的
        console.log('pre触发时的高度:', this.$refs.textRef?.offsetHeight)
      }
    },
    // 另一个监听,用post
    'inputText-post': {
      // 哦不对,应该直接监听inputText,设置flush: 'post'
      // 重新写
      inputText: {
        handler(newVal) {
          // flush是post,此时DOM已经更新,textRef的高度是新的
          console.log('post触发时的高度:', this.$refs.textRef?.offsetHeight)
        },
        flush: 'post'
      }
    }
  }
}
</script>

刚才的例子写错了,一个watch键不能有两个值,要实现这种需求,可以把两个逻辑放在同一个handler里,或者用两个不同的函数键,或者用Composition API的多个watch,不过Options API里更简单的是用同一个handler,里面可以先处理pre的逻辑,然后用nextTick处理post的逻辑——不过有了flush: 'post'就更方便了。

Vue3 Options API watch和Composition API watch的区别

虽然核心逻辑都是监听响应式数据,但两种API的watch在写法、灵活性上还是有一些区别的,老开发者切换的时候要注意,新手也可以根据场景选择合适的API。

写法上的区别

Options API的watch是挂载在组件实例的watch选项里的,键是监听的目标,值是回调或配置对象;而Composition API的watch是在setup函数里直接调用的,可以多次调用,每次调用监听一个或多个目标。 举个对比的例子:

// Options API
export default {
  data() {
    return {
      a: 1,
      b: 2
    }
  },
  watch: {
    a(newVal) { console.log('a变了', newVal) },
    b(newVal) { console.log('b变了', newVal) }
  }
}
// Composition API
import { ref, watch } from 'vue'
export default {
  setup() {
    const a = ref(1)
    const b = ref(2)
    watch(a, (newVal) => { console.log('a变了', newVal) })
    watch(b, (newVal) => { console.log('b变了', newVal) })
    // 也可以同时监听多个
    watch([a, b], ([newA, newB], [oldA, oldB]) => {
      console.log('a或b变了', newA, newB)
    })
    return { a, b }
  }
}

从写法上看,Composition API的watch更灵活,可以同时监听多个目标,而且多个watch之间的逻辑可以写得更近,不需要分开在watch选项里,对于复杂的组件逻辑来说,更容易维护。

监听目标的区别

Options API的watch监听的是组件实例上的属性(data、computed、props),而Composition API的watch监听的是响应式对象本身(ref、reactive、computed,或者返回值的函数)。 这里有个小区别:监听reactive对象的时候,Composition API的watch默认就是深度监听,而Options API的watch监听顶层reactive转换后的data对象,默认还是浅监听。

// Options API
export default {
  data() {
    return {
      user: { name: '小明' } // data里的对象会被Vue3自动用reactive转换
    }
  },
  watch: {
    user() {
      // 默认浅监听,修改user.name不会触发
      console.log('user变了')
    }
  }
}
// Composition API
import { reactive, watch } from 'vue'
export default {
  setup() {
    const user = reactive({ name: '小明' })
    watch(user, () => {
      // 默认深度监听,修改user.name会触发
      console.log('user变了')
    })
    return { user }
  }
}

如果不想让Composition API的watch深度监听reactive对象,可以手动设置deep: false。

生命周期的区别

Options API的watch是在组件实例创建之后、挂载之前(如果是immediate: true的话,是在created之后、beforeMount之前)初始化的;而Composition API的watch是在setup函数执行时同步初始化的,如果设置了immediate: true,也是在setup里同步触发的。

取消监听的区别

默认情况下,两种API的watch都会在组件卸载时自动取消监听,不需要手动处理,但如果需要手动提前取消监听,Composition API的watch会返回一个取消函数,直接调用就行;而Options API的watch没有直接返回取消函数,不过可以通过$watch方法手动添加监听,然后获取取消函数。 举个例子:

// Options API手动取消监听
export default {
  data() {
    return {
      a: 1
    }
  },
  mounted() {
    // 用$watch手动添加监听,获取取消函数
    this.unwatchA = this.$watch('a', (newVal) => {
      console.log('a变了', newVal)
      if (newVal === 5) {
        // 当a变成5时,取消监听
        this.unwatchA()
      }
    })
  }
}
// Composition API手动取消监听
import { ref, watch } from 'vue'
export default {
  setup() {
    const a = ref(1)
    const unwatchA = watch(a, (newVal) => {
      console.log('a变了', newVal)
      if (newVal === 5) {
        unwatchA()
      }
    })
    return { a }
  }
}

从这个角度看,Composition API的取消监听更方便,因为取消函数直接就在setup里,不需要等到mounted或者其他生命周期。

Vue3 Options API watch的常见踩坑点

讲完了基础功能和区别,咱们来聊聊Vue3 Options API watch最容易踩的几个坑,很多老开发者和新手都中过招。

坑一:直接监听data里的数组/对象,修改属性/元素不触发

刚才在讲deep配置项的时候提到过,这个是最常见的坑,解决办法要么是给对应的watch配置项加上deep: true,要么是直接监听具体的属性/元素,要么是修改数组/对象时生成新的引用(比如用展开运算符:this.user = {...this.user, name: '小刚'};this.hobbies = [...this.hobbies, '摄影'])。

坑二:设置immediate: true时,oldVal是undefined

这个刚才也提到过,不是bug,是正常的设计,解决办法就是在handler里判断一下oldVal是不是undefined,如果是就跳过某些逻辑,或者只执行初始化的逻辑。

坑三:开启deep: true后,newVal和oldVal是同一个引用

这个也是Proxy代理的特性导致的,因为修改的是同一个对象/数组,没有生成新的内存地址,解决办法要么是不要用deep: true,直接监听具体的属性,要么是在handler里手动做深拷贝,对比深拷贝后的对象的属性,要么是用watchEffect。

坑四:监听props时,修改了props的内容

虽然Vue3会在开发模式下给你警告,但生产模式下不会,而且props是父组件传递过来的,子组件不应该直接修改,否则会导致数据流混乱,如果子组件需要修改props的内容,应该通过emit事件通知父组件修改,或者用computed做一个中间层。

坑五:在handler里直接修改监听的数据,导致无限循环

这个也是比较常见的,比如监听a,然后在a的handler里又修改a,就会导致无限循环,解决办法要么是避免直接修改监听的数据,要么是在修改前加一个判断,只有满足某个条件才修改,要么是用watchEffect的stop选项(不过Options API里用$watch的话,可以在修改前调用unwatch,修改后再重新watch,但比较麻烦)。

坑六:监听的目标是普通的非响应式变量

Options API里的watch只能监听data、computed、props里的响应式变量,如果你监听的是在created、mounted或者其他生命周期里直接定义的普通变量(比如this.c = 3,没有放在data里),那watch是不会生效的,因为这个变量不是响应式的,解决办法要么是把变量放在data里,要么是用ref/reactive在Composition API里定义(不过如果坚持用Options API的话,还是放在data里吧)。

什么时候用Vue3 Options API的watch,什么时候用Composition API的?

两种API的watch各有优缺点,具体用哪种可以根据你的项目场景和个人习惯来选择:

  • 如果你的项目是从Vue2迁移过来的,而且主要逻辑都还是用Options API写的,那继续用Options API的watch就可以,不用特意改成Composition API的,这样可以减少迁移成本。
  • 如果你的项目是新项目,而且组件逻辑比较简单,比如只有几个监听、几个方法,那用Options API的watch也没问题,写法比较直观,适合新手。
  • 如果你的项目是新项目,而且组件逻辑比较复杂,比如有很多相关的监听、计算属性、方法,或者需要手动取消监听、同时监听多个目标,那用Composition API的watch会更方便,逻辑可以写得更紧凑,更容易维护。
  • 如果你同时用两种API写组件(Vue3支持混合使用),那根据逻辑所在的位置选择就行——如果逻辑在Options API里,就用Options API的watch;如果逻辑在setup里,就用Composition API的watch。

最后再总结一下:Vue3 Options API的watch和Vue2的核心逻辑差不多,新增了一些默认值和优化,三个核心配置项immediate、deep、flush一定要掌握,常见的踩坑点也要注意避免,至于选择哪种API的watch,还是那句话:适合自己的就是最好的。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门