侦听器 学习笔记

侦听器 学习笔记

一.介绍

计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

在组合式 API 中,我们可以使用 watch()函数在每次响应式状态发生变化时触发回调函数:

<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')

// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.indexOf('?') > -1) {
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    }
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" />
  </p>
  <p>{{ answer }}</p>
</template>

二.watch()

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

先搞段代码感受一下:

<template>
  <div>
    响应式数据:<input v-model="username">
    <hr>
    响应式数据:<input v-model="user.name">
    <hr>
    <button @click="changeName">按钮</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, ref, watch } from "vue";
let username = ref("wsy")
let user = reactive({name: "wsy", age: 111})

const changeName = () => {
  username.value = "改了"
  user.name = "改了"
}

//一个 ref
watch(username, (newValue, oldValue) => {
  console.log("ref新值:" + newValue + " -- ref旧值:" + oldValue)
})
//一个 响应式对象
watch(user, (newValue, oldValue) => {
  console.log("响应式对象新值:" + newValue.name + " -- 响应式对象旧值:" + oldValue.name)
})
//一个 函数返回一个值
watch(()=>user.name, (newValue, oldValue) => {
  console.log("函数返回一个值新值:" + newValue + " -- 函数返回一个值旧值:" + oldValue)
})
//多个数据源
watch([username,()=>user.name],([newValue1,newValue2],[oldValue1,oldValue2]) => {
  console.log("多数据源监听新值:" + newValue1 + " -- 多数据源监听旧值:" + oldValue1)
  console.log("多数据源监听新值:" + newValue2 + " -- 多数据源监听旧值:" + oldValue2)
})

</script>

当点击按钮改变了数据的时候,就会触发watch()的内容。嗯就像你想继承三百亿一样,保证条件,当然有条件!下面来说说游戏规则:

  • 详细信息

    watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

    第一个参数是侦听器的响应式数据源。这个来源可以是以下几种:

    • 一个函数,返回一个值
    • 一个 ref
    • 一个响应式对象
    • ...或是由以上类型的值组成的数组

    第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

    当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

    第三个可选的参数是一个对象,支持以下这些选项:

    • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器
    • flush:调整回调函数的刷新时机。参考回调的刷新时机watchEffect()
    • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器

    watchEffect() 相比,watch() 使我们可以:

    • 懒执行副作用;
    • 更加明确是应该由哪个状态触发侦听器重新执行;
    • 可以访问所侦听状态的前一个值和当前值。

一脸懵逼? 没关系,你们还有我,我还有大满老师~我把人话给搬过来。

2.1 参数

上面一坨说的就是二爷遗嘱上说的继承三百亿的前提条件,下面我代表老金说点容易犯错的地方:

2.1.1 第一个参数

  1. 这个源,必须得是响应式的,那个函数也不例外,你把上面的user改成这样:let user = {name: "wsy", age: 111},就算用函数形式,也不行

  2. 源是响应式对象,注意这个对象:

    //这么搞就是错的,这叫对象的属性,不叫对象
    watch(user.name, (newValue, oldValue) => {
      console.log("新值:" + newValue + " --旧值:" + oldValue)
    })
    

2.1.2 第二个参数

第二个参数是个回调,回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。新值和旧值没啥说的,就拿出来直接用就行了,问题是这个用于注册副作用清理的回调函数

哦这名字,太JB搞事情了API啥时候能说点人话,我也不太懂,不过我从运行的结果来看,大致就是:

第一次进watch的时候不执行这个函数,第二次会先执行一下这个函数。目的可以就是判断一下你上次的操作执行完了没?如果没执行完你想怎么个处理办法。

//实验回调的第三个参数
watch(username, (newValue, oldValue, onCleanup) => {
   onCleanup(()=>{
     console.log("清理之前没执行完的异步操作")
   })
  console.log("新:" + newValue + " -- 旧:" + oldValue)
})
//控制台输出
 新:改了 -- 旧:wsy
 清理之前没执行完的异步操作
 新:改了1 -- 旧:改了
 清理之前没执行完的异步操作
 新:改了12 -- 旧:改了1

2.1.3 第三个参数

第三个参就是个配置对象,其中有个比较骚的deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器

不得不说官网说的既严谨又随意。。严谨到我看不懂,随意到我按照他便面的意思一写就错。

首先:直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——--该回调函数在所有嵌套的变更时都会被触发:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})

obj.count++

Tips:人话:传入一个响应式对象,会默认给你加个deep:true,但是请注意:这个对象只能是reactive的,你换成ref的就不行

相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调,或者显式地加上 deep 选项,强制转成深层侦听器:

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

Tips:人话:监听函数,不给你加deep:true,你自己加去

另外一个值得说一下的是flush的几个值:

  • pre 组件更新之前调用
  • sync 同步执行
  • post 组件更新之后执行

三.watchEffect()

侦听器的回调使用完全相同的响应式状态是很常见的。

let all = ref("初始数据")
let userReactive = reactive({name: "wsy", age: 1, bro: {name: "wds", age: 2 }})

watch(userReactive,()=>{
  all.value = userReactive.age + " 岁"
  console.log(all)
})

我们可以用 watchEffect 函数 来简化上面的代码。watchEffect() 允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:

let all = ref("初始数据")
let userReactive = reactive({name: "wsy", age: 1, bro: {name: "wds", age: 2 }})

watchEffect(()=>{
  all.value = userReactive.age + " 岁"
  console.log(all)
})

这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 userReactive.age 作为依赖(和计算属性类似)。每当变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 userReactive 作为源值。

对于这种只有一个依赖项的例子来说,watchEffect() 的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect() 可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。

四.比较

watchwatchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

五.停止

setup()<script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。

一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子:

<script setup>
import { watchEffect } from 'vue'

// 它会自动停止
watchEffect(() => {})

// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

要手动停止一个侦听器,请调用 watchwatchEffect 返回的函数:

const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()

注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:

// 需要异步请求得到的数据
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // 数据加载后执行某些操作...
  }
})
posted @ 2023-08-13 22:57  抓哇攻城狮  阅读(40)  评论(0)    收藏  举报