百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

vue3 watch 最佳实践(vue的$watch)

wxin55 2024-10-31 15:40 9 浏览 0 评论

vue3 watch 最佳实践 项目中在监听数据变化,有时候不生效,又去翻阅文档,watch 和 watchEffect 如何选择才比较好? 这些问题,都挺关键的,现在总结一下比较好的做法。 watch watch 的第一个参数是监听的数据源,有四种形式:

  1. ref:refcomputed;
  2. reactive;
  3. getter: 返回一值的函数;
  4. 以上三种组成的数组

监听 ref 和 shallowRef

<script setup lang="ts">
  const a = ref(0)
  const b = ref(10)
  watch(a, (a, oldA) => {
    console.log(`watch(a),a:${a},oldA:${oldA}`)
  })
  // 监听不到 ? ts 也给出提示
  watch(a.value, (a, oldA) => {
    console.log(`watch(a),a:${a},oldA:${oldA}`)
  })

  watch([a, b], ([a, b]) => {
    console.log(`watch([a, b] ,a:${a},b:${b}`)
  })

  const sum = computed(() => {
    return a.value + b.value
  })

  watch(sum, sum => {
    console.log(`watch(sum) ,sum:${sum}`)
  })

  watch([a, () => b.value + 1000], ([a, sum]) => {
    console.log(`watch([a, () => b.value + 1000] a:${a} sum:${sum}`)
  })

  function changeA() {
    a.value += 1
    b.value += 10
  }

  const numList = shallowRef([1, 2, 3])

  function changeNumList() {
    // numList.value.push(10, 20, 30)  
    // NOTE shallowRef 的数组,使用 push pop 等方法修改 无法监测到 ?
    numList.value = [...numList.value, 10, 20, 30]
  }

  watch(numList, numList => {
    console.log(numList, `watch(numList), numList: ${numList}`)
  })

  function changeNumList2() {
    numList.value = ['10', '20', '30']
  }
  const shallowA = shallowRef({
    a: {
      b: {
        c: 10,
      },
    },
    d: 100,
  })
  watch(shallowA, val => {
    console.log('watch(shallowA): ', val)
  })
  watch(
    () => shallowA.value.a,
    val => {
      console.log('watch(() => shallowA.value.a): ', val)
    }
  )

  function changeShallowA() {
    shallowA.value = {
      // @ts-ignore
      e: 1000,
    }
  }
</script>

<template>
  <div>
    <h2>watch ref</h2>
    <p>a:{{ a }}</p>
    <p>b:{{ b }}</p>
    <p>sum:{{ sum }}</p>
    <button @click="changeA">changeA</button>
    <h2>watch shallowRef</h2>
    <p>shallowA:{{ shallowA }}</p>
    <button @click="changeShallowA">changeShallowA</button>
    <h2>watch array</h2>
    <p>numList:{{ numList }}</p>
    <button @click="changeNumList">changeNumList</button>
    <button @click="changeNumList2">changeNumList2</button>
  </div>
</template>

最佳实践

  1. 监听 ref: watch(ref) 或者 watch(computedRef)
  2. 监听 shallowRef:watch(ref) 或者 watch(computedRef)
  3. 关于 ref和 shallowRef 的修改,使用xxx.value = newValue,即直接重置,可有效降低心智负担。

比如上面的数组添加,numList 是一个 shallowRef ,使用数组方法 numList.value.push(10, 20, 30) 添加元素,无法监测到 numList 的改变 。使用 pushpop 等改变数组类型的 ref,是能监听到的。 修改数组,推荐都这样改:

numList.value = [...numList.value, 10, 20, 30] // 直接重置数组 shallowRef 也是如此

修改对象某个属性:

shallowA.value = {
  ...shallowA.value,
  a: {
    b: 'this is a.b '
  },
  // @ts-ignore
  e: 1000,
}

关于 reactive 和 shallowReactive 的监听

  • 监听整个 reactive,直接写,watch(person),不用添加 {deep: true},是默认深度监听的;
  • 使用函数返回整个 reactive:watch(()=>({..person})), 默认浅层监听的,监听深层属性,添加{deep:true}
  • 监听 reactive 的单个属性,使用函数返回,监听多个属性,使用数组:watch(person.age) watch([()=>person.age, ()=>person.name])

不能直接侦听响应式对象的属性值

const obj = reactive({
  count: 0
})

// 错误,因为 watch() 得到的参数是一个 number ?
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})
// 应该这样监听:提供一个 getter 函数 ?
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

shallowReactive 只是第一层属性是响应式的,添加 {deep:true} ,也监听不到深层次属性。

watch 的第三个参数

watch 的第三个参数,是一个配置对象

watch(reactiveVariable, effectCallback, {
  deep: true, // 监听深层属性
  immediate: true, // 立即执行 effectCallback
  flush: 'post', // vue 组件更新后执行 effectCallback,默认 vue 组件更新之前执行 effectCallback
})

flush?: 'pre' | 'post' | 'sync'; 何时使用 {flush:'post'} ?希望在 effectCallback 中获取到最新的 DOM 时,比如 effectCallback 中涉及 DOM 的操作。 watchEffect 监听器的回调函数使用到的变量依赖源完全相同是很常见的,比如这样:

watch(todoId, async (newTodoId) => {
  // totoId.value 和 newTodoId 是相同的
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${newTodoId}`
  )
  data.value = await response.json()
}, {
  immediate: true
})

todoId 在监听器中被使用了两次:一次是作为依赖源,另一次是在回调中,可使用watchEffect简化:

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

watchEffect 立即执行的特点相当于立即执行的 watch

watchEffect 不会深度监听

const person = reactive({
  name: 'jack',
  age: '',
  deep: {
    city: 'GuiYang',
  },
})
console.log('----->', 'zqj log')
watchEffect(() => {
  const refPerson = toRefs(person)
  console.log(refPerson, 'zqj log toRefs')
})

watchEffect(
  () => {
    const refPerson = toRefs(person)
    console.log(refPerson, 'zqj log toRefs  deep: true')
  }, {
    deep: true,
  }
)

watchEffect(() => {
  console.log(person, 'zqj log')
})

watchEffect(
  () => {
    console.log(person, 'zqj log deep: true')
  }, {
    deep: true,
  }
)

watchEffect(() => {
  const city = person.deep.city
  console.log(city, 'zqj log')
})

setTimeout(() => {
  person.deep.city = 'Beijing'
}, 2000)

setTimeout(() => {
  person.deep = {
    city: 'ChengDu',
  }
}, 4000)

4 个 watchEffect 的回调都会立即执行,2 秒后,只有第三个 watchEffect 的回调执行。

注意,watchEffect 不支持深度监听,需要深度监听,使用 toRefs

2 秒后,只有

watchEffect(() => {
  const city = person.deep.city
  console.log(city, 'zqj log')
})

的回调执行。

4 秒后,执行的回调有:

watchEffect(() => {
  const refPerson = toRefs(person)
  console.log(refPerson, 'zqj log toRefs')
})
watchEffect(
  () => {
    const refPerson = toRefs(person)
    console.log(refPerson, 'zqj log toRefs  deep: true')
  }, {
    deep: true,
  }
)

watchEffect 的第二个参数 {flush:'pre'|'post'|'sync'} , 用来改变回调执行时机 ,默认是 {flush:'pre'} ,vue 组件更新前执行。 watchPostEffect(callback)watchEffect(callback,{post:true}) 的别名。 使用 watchEffect 的最佳实践

  1. 对于依赖项多:有多个依赖项的侦听器来说,使用 watchEffect,可以消除手动维护依赖列表的负担。
  2. 深层监听: 监听一个嵌套数据结构中的几个属性,watchEffect 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
  3. 立即执行:希望监听器回调立即执行时使用。
  4. 惰性执行副作用和希望获取旧值时,不要使用。
  5. 深度监听,使用 toRefs

ref vs reactive 两者对比

reactive

ref

? script 和 template 无差别

? script 中需要使用 .value

? 只支持对象和数组

? 任意类型

? 传递函数会丢失响应式

? 传递函数不会丢失响应式

? 直接返回属性

? 使用 .value 访问属性

? 解构丢失响应式

? 解构丢失响应式

ref 会显示得使用 .value 返回,会让一眼看出是响应式变量,监听也更加方便; 直接使用 ref ,修改时使用 .value = newValue ,更加接近 immutability 特性,心智负担小; 监听变化,ref 比 reactive 心智负担更加小,监听 reactive 的深层次属性,需要添加 {deep:true} , ref 不用。 关于 props 的监听

  1. 都使用箭头函数作为数据源
  2. 在 1 的基础上,监听 对象数组 时,使用深度监听。 关于 1 很好理解,vue 文档也有很好的解释,为什么要使用深度监听呢? 因为只有 箭头函数 + deep 的组合,才能监听到 reactive 和计算属性。
// NOTE obj 是一个计算属性,监听不到  是 reactive 却能监听 ?
watch(props.obj, (val, old) => {
  console.log('props.obj', val, old)
})
// NOTE obj 是 reactive 监听不到 是计算属性能监听  ?
watch(
  () => props.obj,
  (val, old) => {
    console.log('()=>props.obj', val, old)
  }
)
// NOTE obj 是 reactive 或者计算属性 都能监听到 ?
watch(
  () => props.obj,
  (val, old) => {
    console.log('()=>props.obj', val, old)
  }, {
    deep: true,
  }
)

关于这个怪异的行为,vue3 文档没有给出解释。我们都是 监听函数 + deep:true 的组合,这样就能监听到 reactive 和计算属性,减少心智负担。

watch vs watchEffect

相同点

  1. 返回值都是一个用于停止监听的函数;
const postID = ref(1)
const post = ref()

const stopWatchEffect = watchEffect(async () => {
  const [error, data] = await http.get(`http://localhost:3001/posts/${postID.value}`);
  !error && (post.value = data)
})

setTimeout(() => {
  // 取消监听,在修改 postID 之前
  stopWatchEffect()
}, 2000)

setTimeout(() => {
  // watchEffect 的回调不再执行
  postID.value = 2
}, 3000)

同步的 watch 和 watchEffect,vue 会自动停止监听,异步地执行 watch 和 watchEffect 才需要开发者手动停止。

  1. cleanUp 的回调可清除副作用

用法:

watchEffect(callback)

function callback(cleanUp) => {
  // cleanUp 回调会在 callback 之前执行
  cleanUp(() => {
    // 可以在此清除副作用
  })
  // do some effect
}

watch(deps, callback2)

function callback2(newDeps, old, cleanUp) {
  // cleanUp 回调会在 callback 之前执行
  cleanUp(() => {
    // 可以在此清除副作用
  })
  // do some effect
}

以 watch 为例,看看 cleanUp 如何取消请求:

import http from '@jack/http'

const postID = ref(1)
const post = ref()

watch(postID, callback, {
  immediate: true
})

async function callback(newID, oldID, cleanUp) {
  let lastController
  console.log('callback ')
  cleanUp(() => {
    console.log('cleanUp 回调会在 callback 之前执行 ', '可以在此取消正在进行的请求')
    lastController?.abort()
  })
  lastController = new AbortController()
  const [error, data] = await http.get(`http://localhost:3001/posts/${newID}`, {}, {
    signal: lastController.signal,
  })
  lastController = null;
  !error && (post.value = data)
  const [error, data] = await http.get(`http://localhost:3001/posts/${newID}`, {}, {
    signal: lastController.signal,
  })
  lastController = null;
  !error && (post.value = data)
}
setTimeout(function() {
  postID.value = 2
}, 9)
setTimeout(function() {
  postID.value = 3
}, 10)

在 9 毫秒后修改依赖,postID 为 1 的 http 还没返回,被 cleanUp 的回调取消。 同理,postID 为 3 时,会取消 postID 为 2 的请求。 如果网速极快,也许无法取消 http,可在 chrome 浏览器中开启网络模拟,让网速慢下来。

  1. 都可进行调试

不同点 主要区别是 watchEffect 无法惰性执行副作用和获取旧值。 到底使用哪个呢? 经验法则: 必须使用 watch 的场景:

  1. 获取旧值;
  2. 惰性执行副作用;

可以使用 watchEffect 的常场景:

  1. 依赖源对多 3 个的立即执行
  2. 不需要旧的立即执行
  3. 深层监听比较麻烦的时候;
  4. 依赖源和回调函数用到的变量一样的时候。

watchEffect 的副作用是立即执行的。 watch 和 watchEffect 误用 常见的误用:

  1. 在回调函数中修改数据源,容易导致死循环
watch(() => source.value,
  n => {
    // 修改数据源
    source.value = []
    if (!n) return
    // 修改数据源 ...
  })

小结

  1. 优先使用 ref:心智负担小,监听和重置都很方便,一眼能看出是响应式变量,shallowRef 修改数据也是重置;
  2. watchEffect 和 watch 的 cleanUp 回调里可清除副作用;
  3. 惰性执行副作用和获取旧值,必须使用 watch;
  4. 当需要执行深层监听依赖源较多立即执行的副作用,watchEffect 更好;
  5. 监听 props,都使用箭头函数,监听对象和数组时,使用深度监听。

相关推荐

ES6中 Promise的使用场景?(es6promise用法例子)

一、介绍Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码doSomething(f...

JavaScript 对 Promise 并发的处理方法

Promise对象代表一个未来的值,它有三种状态:pending待定,这是Promise的初始状态,它可能成功,也可能失败,前途未卜fulfilled已完成,这是一种成功的状态,此时可以获取...

Promise的九大方法(promise的实例方法)

1、promise.resolv静态方法Promise.resolve(value)可以认为是newPromise方法的语法糖,比如Promise.resolve(42)可以认为是以下代码的语...

360前端一面~面试题解析(360前端开发面试题)

1.组件库按需加载怎么做的,具体打包配了什么-按需加载实现:借助打包工具(如Webpack的require.context或ES模块动态导入),在使用组件时才引入对应的代码。例如在V...

前端面试-Promise 的 finally 怎么实现的?如何在工作中使用?

Promise的finally方法是一个非常有用的工具,它无论Promise是成功(fulfilled)还是失败(rejected)都会执行,且不改变Promise的最终结果。它的实现原...

最简单手写Promise,30行代码理解Promise核心原理和发布订阅模式

看了全网手写Promise的,大部分对于新手还是比较难理解的,其中几个比较难的点:状态还未改变时通过发布订阅模式去收集事件实例化的时候通过调用构造函数里传出来的方法去修改类里面的状态,这个叫Re...

前端分享-Promise可以中途取消啦(promise可以取消吗)

传统Promise就像一台需要手动组装的设备,每次使用都要重新接线。而Promise.withResolvers的出现,相当于给开发者发了一个智能遥控器,可以随时随地控制异步操作。它解决了三大...

手写 Promise(手写输入法 中文)

前言都2020年了,Promise大家肯定都在用了,但是估计很多人对其原理还是一知半解,今天就让我们一起实现一个符合PromiseA+规范的Promise。附PromiseA+规范地址...

什么是 Promise.allSettled()!新手老手都要会?

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的pr...

前端面试-关于Promise解析与高频面试题示范

Promise是啥,直接上图:Promise就是处理异步函数的API,它可以包裹一个异步函数,在异步函数完成时抛出完成状态,让代码结束远古时无限回掉的窘境。配合async/await语法糖,可...

宇宙厂:为什么前端离不开 Promise.withResolvers() ?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发。1.为什么需要Promise.with...

Promise 新增了一个超实用的 API!

在JavaScript的世界里,Promise一直是处理异步操作的神器。而现在,随着ES2025的发布,Promise又迎来了一个超实用的新成员——Promise.try()!这个新方法简...

一次搞懂 Promise 异步处理(promise 异步顺序执行)

PromisePromise就像这个词的表面意识一样,表示一种承诺、许诺,会在后面给出一个结果,成功或者失败。现在已经成为了主流的异步编程的操作方式,写进了标准里面。状态Promise有且仅有...

Promise 核心机制详解(promise机制的实现原理)

一、Promise的核心状态机Promise本质上是一个状态机,其行为由内部状态严格管控。每个Promise实例在创建时处于Pending(等待)状态,此时异步操作尚未完成。当异步操作成功...

javascript——Promise(js实现promise)

1.PromiseES6开始支持,Promise对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。简单说就是处理异步请求的。之所以叫Promise,就是我承诺,如果成功则怎么处理,失败怎...

取消回复欢迎 发表评论: