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

uniapp vue3 watch到底怎么用才顺手?避坑+高级用法全讲透

terry 2小时前 阅读数 26 #Vue

最近用uniapp vue3重构老项目,发现不少同行和新手群里,watch相关的提问刷屏——“为啥watch监听data没反应?”“监听props和vue2不一样了?”“immediate用了反而出问题?”“deep和immediate一起用咋设置?”“watch和computed选哪个更省性能?”其实只要摸透uniapp vue3底层是用vue3 composition API + uni-app自身的平台适配逻辑,这些问题都能迎刃而解,今天我就结合自己半年多的开发经验,整理出一份从基础到进阶的落地指南,覆盖新手必踩的坑和老手常用的骚操作。

基础入门:先搞懂uniapp vue3 watch和vue2的核心区别

很多从vue2转过来的人,一上手就直接用watch: { a(newVal, oldVal) { } }这种选项式写法,或者直接照搬vue3 composition API的import { watch } from 'vue',结果要么是监听非响应式数据没动静,要么是监听uniapp特有的数据(比如页面栈、tabbar index?不对,tabbar index页面里不能直接watch,后面会讲)出问题,先别急着写代码,花3分钟搞懂这3个核心区别,能少踩一半的坑:

第一个区别是选项式API vs 组合式API的默认监听源变化,在vue2选项式里,你可以直接把watch写到export default的选项里,不管监听的是data、props还是computed,都是自动追踪响应式的;但在uniapp vue3组合式API(推荐用这个,因为能更好地复用逻辑)里,不能直接监听“裸的”变量,必须是响应式的源——比如ref包装的基本类型数据、reactive包装的对象/数组、computed的返回值,或者是一个返回响应式数据的getter函数,举个例子,新手常犯的错误:

// ❌ 错误写法:直接监听裸变量
<template>
  <button @click="count++">点我加1</button>
</template>
<script setup>
let count = 0
watch(count, (newVal, oldVal) => { // 这里count不是响应式的,watch根本不会触发
  console.log('count变了', newVal, oldVal)
})
</script>

改成组合式的正确写法:

// ✅ 正确写法:用ref包装基本类型
<template>
  <button @click="count++">点我加1</button>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
  console.log('count变了', newVal, oldVal)
})
</script>

如果是监听reactive对象的整个属性,也可以直接传,但如果要监听对象里的某个属性,必须传getter函数,这个是第二个区别的一部分。

第二个区别是深度监听和属性监听的处理方式,在vue2里,要深度监听对象或数组,得加deep: true;要监听对象里的某个属性,比如user.name,直接把键写成字符串'user.name'就行,但在uniapp vue3组合式API里,深度监听reactive对象是默认开启的!但要注意:默认开启的深度监听,只能监听到对象或数组内部属性的变化(比如user.name = '张三'list.push(1)),但监听不到对象或数组的重新赋值(比如user = { name: '李四' }list = [2,3])——因为重新赋值会把ref/reactive创建的响应式引用给覆盖掉,变成非响应式的了,那如果要监听reactive对象的重新赋值怎么办?要么用ref包装这个对象(ref包装对象的话,重新赋值是可以监听到的,而且深度监听也要显式传deep: true),要么传getter函数返回这个对象。

举个例子,对比vue2和uniapp vue3组合式的属性监听和深度监听:

// vue2选项式写法
<script>
export default {
  data() {
    return {
      user: { name: '张三', age: 18 },
      list: [1,2,3]
    }
  },
  watch: {
    // 监听整个user对象,默认浅监听,内部属性变化不触发
    user(newVal, oldVal) { },
    // 监听user.name属性
    'user.name'(newVal, oldVal) { },
    // 深度监听user对象,内部属性变化也触发
    user: {
      handler(newVal, oldVal) { },
      deep: true
    }
  }
}
</script>
// uniapp vue3组合式写法
<script setup>
import { ref, reactive, watch } from 'vue'
const reactiveUser = reactive({ name: '张三', age: 18 })
const refUser = ref({ name: '张三', age: 18 })
const list = reactive([1,2,3])
// 1. 直接监听reactiveUser,默认深度监听内部属性,但重新赋值不触发
watch(reactiveUser, (newVal, oldVal) => {
  console.log('reactiveUser内部变了', newVal, oldVal)
})
reactiveUser.name = '李四' // ✅ 触发
// reactiveUser = { name: '王五' } // ❌ 重新赋值,引用变了,不会触发,而且后面reactiveUser的响应式也失效了
// 2. 用getter监听reactiveUser的重新赋值(必须先把reactiveUser改成let变量,但组合式一般推荐const,除非必须重新赋值)
let letReactiveUser = reactive({ name: '张三' })
watch(() => letReactiveUser, (newVal, oldVal) => {
  console.log('letReactiveUser重新赋值或内部变了?不,这里如果没加deep,只有重新赋值才会触发')
}, { deep: true }) // 加上deep的话,重新赋值+内部属性变化都触发
letReactiveUser = reactive({ name: '李四' }) // ✅ 重新赋值,触发
letReactiveUser.age = 18 // ✅ 内部变了,加了deep触发
// 3. 直接监听refUser,默认浅监听,重新赋值触发,内部属性变化不触发
watch(refUser, (newVal, oldVal) => {
  console.log('refUser重新赋值了', newVal, oldVal)
})
refUser.value = { name: '李四' } // ✅ 重新赋值,触发
refUser.value.age = 18 // ❌ 内部变了,默认浅监听,不触发
// 4. 监听refUser的内部属性变化,必须加deep: true
watch(refUser, (newVal, oldVal) => {
  console.log('refUser内部变了', newVal, oldVal)
}, { deep: true })
refUser.value.age = 18 // ✅ 触发
// 5. 监听对象的某个属性,不管是ref还是reactive包装的,都推荐用getter函数(比字符串路径更灵活,支持复杂逻辑)
watch(() => reactiveUser.name, (newVal, oldVal) => {
  console.log('reactiveUser.name变了', newVal, oldVal)
})
watch(() => refUser.value.age, (newVal, oldVal) => {
  console.log('refUser.age变了', newVal, oldVal)
})
</script>

这里补充一句:为什么监听对象的某个属性推荐用getter函数而不是字符串路径?因为在组合式API里,字符串路径的写法虽然也支持(比如watch('user.name', handler)),但必须和选项式API配合用,或者把整个setup的返回对象暴露给选项式?不对不对,组合式setup是不能和选项式watch一起用的(至少官方文档不推荐,而且可能有兼容性问题),所以在组合式里,字符串路径的写法基本没用,还是乖乖用getter函数吧。

第三个区别是旧值(oldVal)的获取方式,在vue2里,不管是浅监听还是深监听对象/数组,都能拿到和新值对比的旧值;但在uniapp vue3组合式API里,只有浅监听基本类型数据时,能拿到准确的旧值;如果是浅监听ref包装的对象/数组(重新赋值时),能拿到旧的引用对象(但这个旧引用对象的内部属性可能已经被修改了,因为JS里对象/数组是引用传递的);如果是深度监听ref/reactive包装的对象/数组,不管是内部属性变化还是重新赋值,都拿不到准确的旧值,新值和旧值会是同一个引用对象,这个是vue3本身的设计问题,不是uniapp的锅,目前官方也没有给出完美的解决方案,但如果必须拿到旧值,可以用computed或者watchEffect的副作用来提前保存,后面高级用法里会讲。

新手必踩:uniapp vue3 watch的8个高频坑

讲完核心区别,接下来聊新手最容易踩的8个坑,这些坑我自己都踩过,或者在群里见过不下10次:

坑1:监听uniapp页面栈、tabbar index等非vue响应式数据,直接watch完全没反应

很多人会想当然地认为,uniapp里的所有数据都是响应式的,比如getCurrentPages()获取的页面栈,或者uni.getStorageSync读取的本地数据,但其实这些数据都是普通的JS对象/数组,不是通过vue的ref/reactive创建的,所以直接watch根本不会触发,那怎么监听这些数据的变化呢?

首先说页面栈:页面栈的变化一般只有在调用uni.navigateTo、uni.redirectTo、uni.switchTab、uni.navigateBack这些API的时候才会发生,所以可以在这些API的回调函数里手动处理,或者用uniapp的生命周期钩子(比如onShow、onHide、onUnload)来判断页面栈的变化,举个例子,如果你想监听当前页面是不是最后一个页面,可以在onShow里手动获取页面栈并判断:

<script setup>
import { onShow } from '@dcloudio/uni-app'
onShow(() => {
  const pages = getCurrentPages()
  const isLastPage = pages.length === 1
  console.log('当前页面是不是最后一个:', isLastPage)
  // 在这里处理你的逻辑
})
</script>

然后说本地数据:uni.getStorageSync读取的是静态数据,不会自动更新,所以如果要监听本地数据的变化,最好的方式是封装一个响应式的本地存储hook,把uni.setStorageSync和uni.removeStorageSync的操作都和ref/reactive绑定起来,这样监听这个hook的返回值就可以了,后面高级用法里会给大家分享一个我自己常用的响应式本地存储hook。

坑2:监听props时,用了ref/reactive重新包装,导致子组件不能响应父组件的props变化

这个也是从vue2转过来的人常犯的错误——在vue2里,子组件如果要修改props(虽然官方不推荐,但有时候为了方便还是会这么做),会把props赋值给data里的一个变量,然后修改data里的变量;但在uniapp vue3组合式里,很多人会直接用ref/reactive把props包装起来,

// ❌ 错误写法:用ref重新包装props
<template>
  <div>{{ childCount }}</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps(['count'])
const childCount = ref(props.count) // 这里只是把props.count的初始值赋值给了childCount,父组件的count变化时,childCount不会自动更新
</script>

如果子组件不需要修改props,只是想展示或者监听,直接用props就可以了,监听的时候用getter函数:

// ✅ 正确写法:直接用props,监听用getter函数
<template>
  <div>{{ count }}</div>
</template>
<script setup>
import { watch } from 'vue'
const props = defineProps(['count'])
watch(() => props.count, (newVal, oldVal) => {
  console.log('父组件的count变了', newVal, oldVal)
})
</script>

如果子组件需要修改props,而且这个修改是双向的(比如父组件传过来的是一个搜索关键词,子组件的输入框修改后,父组件也要同步更新),可以用defineEmits配合v-model,或者用computed的getter和setter:

// ✅ 正确写法:用computed的getter和setter配合defineEmits
<template>
  <input v-model="childKeyword" placeholder="请输入搜索关键词" />
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps(['keyword'])
const emit = defineEmits(['update:keyword'])
const childKeyword = computed({
  get() {
    return props.keyword
  },
  set(newVal) {
    emit('update:keyword', newVal)
  }
})
</script>

父组件调用的时候用v-model:keyword

<template>
  <ChildComponent v-model:keyword="parentKeyword" />
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const parentKeyword = ref('')
</script>

坑3:immediate用了反而出问题,比如在子组件里用immediate监听props,导致首次渲染时旧值是undefined

在uniapp vue3里,immediate的作用和vue2一样——设置为true的话,watch会在组件挂载时立即执行一次handler函数,此时newVal是初始值,oldVal是undefined,这个本身没问题,但如果在handler函数里依赖了旧值,或者依赖了其他还没初始化完成的数据(比如onMounted里才会获取的接口数据),就会出问题。

举个例子,新手常犯的错误:

// ❌ 错误写法:immediate用了但依赖了还没初始化的接口数据
<template>
  <div>{{ userList }}</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
const keyword = ref('')
const userList = ref([])
// 模拟获取用户列表的接口
const fetchUserList = async (kw) => {
  console.log('正在获取用户列表,关键词:', kw)
  // 这里可以替换成真实的接口请求
  const res = await new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: kw ? kw + '1' : '张三' },
        { id: 2, name: kw ? kw + '2' : '李四' }
      ])
    }, 500)
  })
  userList.value = res
}
// 监听keyword变化,获取用户列表,设置immediate:true,希望组件挂载时就获取一次
watch(keyword, (newVal, oldVal) => {
  fetchUserList(newVal)
}, { immediate: true })
// 但如果这里还有一个onMounted,需要先获取用户的登录状态,然后再获取用户列表,就会出问题——immediate会先执行,此时登录状态还没获取
// const isLogin = ref(false)
// onMounted(async () => {
//   // 模拟获取登录状态
//   const loginRes = await new Promise((resolve) => {
//     setTimeout(() => {
//       resolve(true)
//     }, 300)
//   })
//   isLogin.value = loginRes
// })
// watch(keyword, (newVal, oldVal) => {
//   if (isLogin.value) { // immediate执行时,isLogin还是false,不会获取用户列表
//     fetchUserList(newVal)
//   }
// }, { immediate: true })
</script>

怎么解决这个问题呢?有三个方法: 第一个方法:如果不依赖其他数据,只是希望组件挂载时就执行一次,那immediate没问题; 第二个方法:如果依赖其他数据(比如登录状态、接口返回的配置数据),可以把immediate去掉,然后在所有依赖数据都初始化完成之后,手动调用一次handler函数; 第三个方法:用watchEffect代替watch,watchEffect会自动追踪依赖,当所有依赖都初始化完成之后,会自动执行一次,后面高级用法里会详细讲watch和watchEffect的区别。

坑4:deep和immediate一起用,拿不到旧值,而且可能影响性能

刚才在核心区别里讲过,深度监听对象/数组时,拿不到准确的旧值;如果再加上immediate,首次执行时oldVal就是undefined,后面执行时newVal和oldVal是同一个引用对象,所以如果你的handler函数里必须用到旧值,就不要用deep,或者提前保存旧值。

深度监听也会影响性能——因为vue3需要遍历对象/数组的所有属性(包括嵌套属性)来追踪变化,如果对象/数组很大(比如有几百上千个嵌套属性),性能消耗会很大,所以尽量不要随便用deep,只有在必须监听对象/数组内部所有属性的变化时才用;如果只需要监听某个或某几个属性的变化,用getter函数分别监听,或者用watch的第一个参数传数组,监听多个源,后面高级用法里会讲。

坑5:监听ref数组的push、pop、shift、unshift等方法时,直接传ref数组没问题,但监听slice、concat等不修改原数组的方法时,不会触发

其实这个和vue3的响应式原理有关——ref包装的数组,只有调用修改原数组的方法(比如push、pop、shift、unshift、splice、sort、reverse)时,才会触发响应式更新;如果调用不修改原数组的方法(比如slice、concat、filter、map、reduce),会返回一个新的数组,此时如果直接赋值给ref数组(比如list.value = list.value.slice(0, 2)),也会触发响应式更新;但如果只是调用了方法,没有赋值给ref数组,那ref数组的引用还是原来的,内容也没变化(因为方法没修改原数组),所以watch不会触发,这个其实是正常的,不是坑,但很多新手不知道,以为是watch出问题了。

坑6:在for循环里创建watch,导致内存泄漏

这个不管是vue2还是vue3,不管是uniapp还是普通的web项目,都是新手常犯的错误——比如你有一个列表,每个列表项都有一个开关,你在for循环里给每个开关的ref创建了一个watch,当列表项被删除时,对应的watch没有被手动清除,导致内存泄漏。

怎么解决这个问题呢?在组合式API里,watch会返回一个停止函数,你可以把这个停止函数保存起来,当列表项被删除时,手动调用这个停止函数;或者,如果你是在组件的setup里创建的watch,当组件被卸载时,watch会自动被清除,不需要手动处理;但如果是在循环里、在异步函数里、或者在非组件的setup里创建的watch,最好手动保存停止函数,在不需要的时候手动清除。

举个例子,在循环里创建watch并手动清除:

<template>
  <div v-for="item in list" :key="item.id">
    <input type="checkbox" v-model="item.checked" />
    <span>{{ item.name }}</span>
    <button @click="deleteItem(item.id)">删除</button>
  </div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
const list = reactive([
  { id: 1, name: '张三', checked: false },
  { id: 2, name: '李四', checked: false },
  { id: 3, name: '王五', checked: false }
])
// 用一个对象保存每个列表项的停止函数,key是item.id
const stopWatchMap = reactive({})
// 初始化列表时,给每个列表项创建watch
list.forEach(item => {
  stopWatchMap[item.id] = watch(() => item.checked, (newVal, oldVal) => {
    console.log(`列表项${item.id}的开关状态变了:${oldVal} -> ${newVal}`)
  })
})
// 删除列表项时,先调用对应的停止函数,再删除列表项和停止函数
const deleteItem = (id) => {
  if (stopWatchMap[id]) {
    stopWatchMap[id]() // 手动清除watch
    delete stopWatchMap[id]
  }
  const index = list.findIndex(item => item.id === id)
  if (index !== -1) {
    list.splice(index, 1)
  }
}
</script>

坑7:监听computed的返回值时,以为computed内部的所有依赖变化都会触发watch,但其实只有computed的返回值变化时才会触发

这个其实是computed的特性,不是watch的锅——computed是懒加载的,只有当它的依赖变化,而且它的返回值也变化时,才会重新计算;所以如果computed的依赖变化了,但返回值没变化,computed不会重新计算,watch也不会触发,这个是正常的,而且能节省性能,大家可以放心用。

举个例子:

<script setup>
import { ref, computed, watch } from 'vue'
const count = ref(0)
// 这个computed只有当count是偶数时,返回值才会变化
const evenCount = computed(() => {
  console.log('evenCount重新计算了')
  return count.value % 2 === 0 ? count.value : count.value - 1
})
watch(evenCount, (newVal, oldVal) => {
  console.log('evenCount变了', newVal, oldVal)
})
count.value = 1 // evenCount的依赖变了,但返回值还是0(1-1),所以evenCount不会重新计算,watch也不会触发
count.value = 2 // evenCount的依赖变了,返回值变成2,所以evenCount重新计算,watch触发
</script>

运行上面的代码,控制台会输出:

evenCount重新计算了
evenCount变了 0 undefined
// 当count.value = 1时,没有输出
evenCount重新计算了
evenCount变了 2 0

坑8:在uniapp的H5端没问题,但在小程序端watch监听失败

这个是uniapp的平台兼容性问题吗?其实大部分情况下不是,而是你用了一些uniapp在小程序端不支持的vue3特性,或者是你的代码逻辑有问题,比如在小程序端,不能直接监听documentwindow等浏览器特有的对象(这个不管是vue2还是vue3都一样);或者你用了watch的第一个参数传箭头函数,但箭头函数里用到了this(在组合式setup里,this是undefined,所以没问题,但如果你是在选项式API里传箭头函数,this就不是组件实例了,可能会有问题)。

如果真的遇到了H5端没问题但小程序端有问题的情况,可以先检查一下你的代码有没有用到浏览器特有的对象或API,有没有用到uniapp官方文档里明确说不支持的vue3特性;如果都没有,可以尝试把watch改成watchEffect,或者把组合式API改成选项式API(虽然不推荐,但有时候能解决兼容性问题);如果还是不行,可以去uniapp的官方社区提问,记得带上你的代码片段和平台版本号。

高级进阶:uniapp vue3 watch的5个骚操作

讲完新手必踩的坑,接下来聊老手常用的5个骚操作,这些操作能让你的代码更简洁、更高效、更易维护:

骚操作1:监听多个响应式源,用数组传参

在uniapp vue3组合式API里,watch的第一个参数可以传一个数组,数组里可以放ref、reactive、computed的返回值,或者返回响应式数据的getter函数;当数组里的任意一个源变化时,handler函数都会执行,此时newVal和oldVal也是数组,顺序和第一个参数的数组顺序一致。

举个例子,监听搜索关键词和分页页码的变化,当任意一个变化时,重新获取用户列表:

<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const pageNum = ref(1)
const fetchUserList = (kw, pn) => {
  console.log('正在获取用户列表,关键词:', kw, '页码:', pn)
  // 这里可以替换成真实的接口请求
}
// 监听多个源
watch([keyword, pageNum], ([newKeyword, newPageNum], [oldKeyword, oldPageNum]) => {
  fetchUserList(newKeyword, newPageNum)
}, { immediate: true })
</script>

骚操作2:深度监听但只追踪部分属性,用getter函数返回包含这些属性的对象

刚才在新手必踩的坑里讲过,深度监听会影响性能,所以如果只需要监听对象的部分属性的变化,可以用getter函数返回一个包含这些属性的新对象,然后对这个新对象进行深度监听——这样vue3只会追踪新对象里的属性的变化,不会追踪原对象里的其他属性的变化,能节省性能。

举个例子,监听用户对象的name和age属性的变化,不监听其他属性:

<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
  name: '张三',
  age: 18,
  gender: '男',
  address: {
    province: '广东',
    city: '深圳'
  }
})
// 用getter函数返回包含name和age的新对象,然后深度监听
watch(() => ({ name: user.name, age: user.age }), (newVal, oldVal) => {
  console.log('name或age变了', newVal, oldVal)
}, { deep: true, immediate: true })
user.gender = '女' // ❌ 没在getter函数里,不触发
user.address.city = '广州' // ❌ 没在getter函数里,不触发
user.name = '李四' // ✅ 在getter函数里,触发
user.age = 19 // ✅ 在getter函数里,触发
</script>

骚操作3:提前保存旧值,解决深度监听时拿不到准确旧值的问题

刚才在核心区别里讲过,深度监听对象/数组时,拿不到准确的旧值;如果必须拿到旧值,可以用computed或者watchEffect的副作用来提前保存,这里推荐用watchEffect的副作用,因为更简洁。

举个例子,用watchEffect提前保存用户对象的旧值:

<script setup>
import { reactive, watch, watchEffect } from 'vue'
const user = reactive({ name: '张三', age: 18 })
let oldUser = null
// 用watchEffect提前保存旧值
watchEffect(() => {
  // 这里必须访问user的所有需要监听的属性,才能触发响应式追踪
  oldUser = JSON.parse(JSON.stringify(user)) // 深拷贝一份,避免引用传递导致旧值被修改
})
// 深度监听user,此时可以用oldUser作为旧值
watch(user, (newVal) => {
  console.log('user变了,新值:', newVal, '旧值:', oldUser)
  // 注意:这里的oldUser是上一次watchEffect保存的,执行完watch之后,watchEffect会再次执行,更新oldUser
}, { deep: true })
user.name = '李四'
user.age = 19
</script>

运行上面的代码,控制台会输出:

// watchEffect首次执行,保存oldUser为{name: '张三', age: 18}
user变了,新值: {name: '李四', age: 18} 旧值: {name: '张三', age: 18}
// watchEffect再次执行,保存oldUser为{name: '李四', age: 18}
user变了,新值: {name: '李四', age: 19} 旧值: {name: '李四', age: 18}
// watchEffect再次执行,保存oldUser为{name: '李四', age: 19}

这里要注意两点:第一,必须深拷贝旧值,否则JS里的引用传递会导致旧值被修改;第二,watchEffect里必须访问user的所有需要监听的属性,才能触发响应式追踪——如果user是一个很大的对象,访问所有属性可能会影响性能,所以这个方法只适用于对象不大的情况;如果对象很大,还是尽量不要用深度监听,用getter函数分别监听需要的属性。

骚操作4:封装一个响应式的本地存储hook,监听本地数据的变化

刚才在新手必踩的坑里讲过,uni.getStorageSync读取的是静态数据,不会自动更新,所以可以封装一个响应式的本地存储hook,把uni.setStorageSync和uni.removeStorageSync的操作都和ref绑定起来,这样监听这个hook的返回值就可以了。

这里给大家分享一个我自己常用的响应式本地存储hook:

// src/hooks/useStorage.js
import { ref, watch } from 'vue'
/**
 * 响应式本地存储hook
 * @param {string} key 本地存储的key
 * @param {any} defaultValue 初始默认值
 * @returns {Ref} 响应式的本地存储数据
 */
export const useStorage = (key, defaultValue) => {
  // 先从本地存储里读取数据,如果没有,就用默认值
  const storedValue = uni.getStorageSync(key)
  const data = ref(storedValue !== '' ? storedValue : defaultValue)
  // 监听data的变化,自动同步到本地存储
  watch(data, (newVal) => {
    if (newVal === null || newVal === undefined) {
      uni.removeStorageSync(key)
    } else {
      uni.setStorageSync(key, newVal)
    }
  }, { deep: true })
  return data
}

在组件里使用这个hook:

<script setup>
import { useStorage } from '@/hooks/useStorage'
// 使用响应式本地存储hook,监听userInfo的变化
const userInfo = useStorage('userInfo', { name: '', age: 0 })
// 修改userInfo,会自动同步到本地存储
const login = () => {
  userInfo.value = { name: '张三', age: 18 }
}
// 清除userInfo,会自动从本地存储里删除
const logout = () => {
  userInfo.value = null
}
</script>

这个hook不仅可以监听本地数据的变化,还可以自动同步到本地存储,非常方便;而且支持深度监听对象/数组,支持清除本地存储,大家可以根据自己的需求修改。

骚操作5:用watchEffect代替watch,自动追踪依赖,不需要手动指定监听源

watchEffect是vue3新增的一个API,它和watch的区别主要有三点: 第一,watchEffect不需要手动指定监听源,它会自动追踪回调函数里用到的所有响应式数据; 第二,watchEffect会在组件挂载时立即执行一次(相当于watch的immediate: true); 第三,watchEffect拿不到旧值,只能拿到新值; 第四,watchEffect更适合用来做副作用(比如修改DOM、发送网络请求、记录日志等),而watch更适合用来做数据转换、或者当数据变化时执行特定的逻辑(需要用到旧值的情况)。

举个例子,用watchEffect代替之前的搜索关键词和分页页码的watch:

<script setup>
import { ref, watchEffect } from 'vue'
const keyword = ref('')
const pageNum = ref(1)
const fetchUserList = (kw, pn) => {
  console.log('正在获取用户列表,关键词:', kw, '页码:', pn)
  // 这里可以替换成真实的接口请求
}
// 用watchEffect自动追踪keyword和pageNum的变化
watchEffect(() => {
  fetchUserList(keyword.value, pageNum.value)
})
</script>

这个代码比之前的watch更简洁,不需要手动指定监听源,也不需要设置immediate: true,非常适合用来做副作用。

这里补充一句:watchEffect也有停止函数,用法和watch一样;而且watchEffect也可以接受第二个参数,用来配置 flush 选项(可选值有'pre'、'post'、'sync',默认是'pre',表示在组件更新前执行;'post'表示在组件更新后执行;'sync'表示同步执行),这个配置选项watch也有,大家可以根据自己的需求选择。

uniapp vue3 watch和computed、watchEffect的选择原则

给大家总结一下uniapp vue3里watch、computed、watchEffect的选择原则,这个是很多新手和进阶用户都搞不清楚的:

  1. 什么时候用computed? 当你需要根据一个或多个响应式数据计算出一个新的值,而且这个新的值需要被缓存(只有当依赖变化时才会重新计算),并且不需要做副作用时,用computed。
  2. 什么时候用watch? 当你需要监听一个或多个响应式数据的变化,并且需要用到旧值,或者需要执行特定的逻辑(比如数据转换、发送网络请求、修改其他数据等),并且不需要自动追踪依赖时,用watch。
  3. 什么时候用watchEffect? 当你需要根据一个或多个响应式数据的变化做副作用(比如修改DOM、发送网络请求、记录日志等),并且不需要用到旧值,并且希望自动追踪依赖时,用watchEffect。

还要注意uniapp的平台兼容性问题,尽量不要用浏览器特有的对象或API,尽量用uniapp官方提供的API;还要注意性能问题,尽量不要随便用deep,尽量用getter函数分别监听需要的属性,尽量用computed缓存计算结果。

好啦,今天的uniapp vue3 watch指南就讲到这里,希望能帮到大家;如果大家还有其他问题,欢迎在评论区留言讨论。

版权声明

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

热门