vue3学习——侦听器

一、侦听器的基本概念

计算属性允许我们声明性地计算衍生值,但在一些场景下,比如状态变化时执行副作用(像更改 DOM、根据异步操作结果修改其他状态),就需要用到侦听器。简单来说,侦听器就像一个 “数据变化监控器”,当它 “观察” 的数据发生变化,就会执行我们预先设定的代码。

二、选项式 API 中的侦听器

在选项式 API 里,我们使用watch选项来创建侦听器。示例代码如下:

export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)',
      loading: false
    }
  },
  watch: {
    // 每当question改变时,这个函数就会执行
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      } finally {
        this.loading = false
      }
    }
  }
}

在上述代码中,watch选项监听question数据的变化。一旦question发生改变,就会执行对应的函数。在函数内部,检查新的question是否包含?,如果包含,则调用getAnswer方法。getAnswer方法用于发起异步请求获取答案,并处理请求过程中的加载状态和错误情况。

watch选项还支持监听对象的嵌套属性,通过用.分隔的路径来设置键,如:

export default {
  watch: {
    // 注意:只能是简单的路径,不支持表达式。
    'some.nested.key'(newValue) {
      //...
    }
  }
}

三、组合式 API 中的侦听器

在组合式 API 中,我们使用watch函数来创建侦听器。示例如下:

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

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

// 可以直接侦听一个ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    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
    } finally {
      loading.value = false
    }
  }
})
</script>

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

这里通过watch函数监听question这个ref,当question变化时,执行回调函数,逻辑和选项式 API 中的类似。

(一)侦听数据源类型

watch函数的第一个参数可以是多种形式的 “数据源”:

  1. 单个 ref:可以直接侦听一个ref,如watch(x, (newX) => { console.log(x is ${newX}) })
  2. getter 函数:通过返回一个值的函数来侦听,如watch(() => x.value + y.value, (sum) => { console.log(sum of x + y is: ${sum}) }) 。
  3. 多个数据源组成的数组:同时侦听多个数据源,如watch([x, () => y.value], ([newX, newY]) => { console.log(x is ${newX} and y is ${newY}) }) 。

注意,不能直接侦听响应式对象的属性值,比如const obj = reactive({ count: 0 }); watch(obj.count, (count) => { console.log(Count is: ${count}) })这样是错误的,需要使用返回该属性的getter函数,即watch(() => obj.count, (count) => { console.log(Count is: ${count}) }) 。

(二)深层侦听器

默认情况下,watch是浅层侦听,即被侦听的属性只有在被赋新值时才会触发回调,嵌套属性的变化不会触发。若要侦听所有嵌套的变更,需要使用深层侦听器。
在选项式 API 中,可以这样设置:

export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // 注意:在嵌套的变更中,只要没有替换对象本身,那么这里的`newValue`和`oldValue`相同
      },
      deep: true
    }
  }
}

在组合式 API 中,直接给watch()传入一个响应式对象,会隐式创建深层侦听器:

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

如果是返回响应式对象的 getter 函数,默认只有在返回不同对象时才会触发回调,若要强制转成深层侦听器,需显式加上deep选项:

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

注意:深度侦听需要遍历被侦听对象的所有嵌套属性,用于大型数据结构时开销较大,要谨慎使用。

(三)即时回调的侦听器

watch默认是懒执行的,仅当数据源变化时才执行回调。但有些场景下,我们希望在创建侦听器时立即执行一遍回调,比如请求初始数据。
在选项式 API 中,可以用包含handler方法和immediate: true选项的对象来声明侦听器:

export default {
  //...
  watch: {
    question: {
      handler(newQuestion) {
        // 在组件实例创建时会立即调用
      },
      // 强制立即执行回调
      immediate: true
    }
  }
  //...
}

在组合式 API 中,通过传入immediate: true选项来强制回调立即执行:

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,且当`source`改变时再次执行
  },
  { immediate: true }
)

(四)一次性侦听器(仅支持 3.4 及以上版本)

如果希望侦听器的回调只在源变化时触发一次,可以使用once: true选项。
在选项式 API 中:

export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // 当`source`变化时,仅触发一次
      },
      once: true
    }
  }
}

在组合式 API 中:

watch(
  source,
  (newValue, oldValue) => {
    // 当`source`变化时,仅触发一次
  },
  { once: true }
)

四、watchEffect()函数

watchEffect()函数可以自动跟踪回调的响应式依赖。例如,用watch函数加载远程资源的代码:

const todoId = ref(1)
const data = ref(null)
watch(
  todoId,
  async () => {
    const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
    data.value = await response.json()
  },
  { immediate: true }
)

使用watchEffect()函数可以简化为:

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

这里回调会立即执行,不需要指定immediate: true,并且会自动追踪todoId.value作为依赖,每当todoId.value变化时,回调会再次执行。对于有多个依赖项的侦听器,使用watchEffect()可以避免手动维护依赖列表;在侦听嵌套数据结构中的几个属性时,watchEffect()可能比深度侦听器更高效,因为它只跟踪回调中使用到的属性。

注意watchEffect仅在其同步执行期间追踪依赖,使用异步回调时,只有在第一个await正常工作前访问到的属性才会被追踪。

(一)watchwatchEffect的区别

watchwatchEffect都能响应式地执行有副作用的回调,但追踪响应式依赖的方式不同:

  • watch只追踪明确侦听的数据源,不会追踪回调中访问到的其他东西,仅在数据源确实改变时才触发回调,能更精确地控制回调函数的触发时机。
  • watchEffect会在副作用发生期间追踪依赖,在同步执行过程中自动追踪所有能访问到的响应式属性,代码更简洁,但响应性依赖关系可能不那么明确。

五、副作用清理

在侦听器执行异步操作(如异步请求)时,如果在请求完成前数据发生变化,可能会导致使用过时数据。这时可以使用清理函数来解决这个问题。
在 Vue 3.5 + 版本中,可以使用onWatcherCleanup()API 来注册清理函数,在侦听器失效并准备重新运行时被调用:

import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
  const controller = new AbortController()
  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // 回调逻辑
  })
  onWatcherCleanup(() => {
    // 终止过期请求
    controller.abort()
  })
})

在 3.5 之前的版本,以及在 Vue 3.5 + 版本中,还可以通过将onCleanup函数作为第三个参数传递给侦听器回调和watchEffect作用函数的第一个参数来实现清理功能:

watch(id, (newId, oldId, onCleanup) => {
  //...
  onCleanup(() => {
    // 清理逻辑
  })
})
watchEffect((onCleanup) => {
  //...
  onCleanup(() => {
    // 清理逻辑
  })
})

六、回调的触发时机

当更改响应式状态时,可能会同时触发 Vue 组件更新和侦听器回调。默认情况下,侦听器回调会在父组件更新(如有)之后、所属组件的 DOM 更新之前被调用,此时在侦听器回调中访问所属组件的 DOM,得到的是更新前的状态。

(一)Post Watchers

如果想在侦听器回调中访问被 Vue 更新之后的所属组件的 DOM,需要指明flush: 'post'选项。
在选项式 API 中:

export default {
  //...
  watch: {
    key: {
      handler() {},
      flush: 'post'
    }
  }
}

在组合式 API 中:

watch(source, callback, {
  flush: 'post'
})
watchEffect(callback, {
  flush: 'post'
})

后置刷新的watchEffect()有个更方便的别名watchPostEffect()

import { watchPostEffect } from 'vue'
watchPostEffect(() => {
  /* 在Vue更新后执行 */
})

(二)同步侦听器

还可以创建同步触发的侦听器,它会在 Vue 进行任何更新之前触发。
在选项式 API 中:

export default {
  //...
  watch: {
    key: {
      handler() {},
      flush: 'sync'
    }
  }
}

同步触发的watchEffect()有个更方便的别名watchSyncEffect()

import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
  /* 在响应式数据变化时同步执行 */
})

注意:同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发,适合监视简单的布尔值,应避免在可能多次同步修改的数据源(如数组)上使用。

七、this.$watch()方法

在组件实例中,还可以使用this.$watch()方法命令式地创建一个侦听器:

export default {
  created() {
    this.$watch('question', (newQuestion) => {
      //...
    })
  }
}

这种方式在特定条件下设置侦听器,或者只侦听响应用户交互的内容时很有用,并且可以提前停止该侦听器。

八、停止侦听器

watch选项或者this.$watch()实例方法声明的侦听器,会在宿主组件卸载时自动停止。但在少数情况下,需要在组件卸载前手动停止侦听器,可以调用this.$watch()API 返回的函数:

const unwatch = this.$watch('foo', callback)
//...当该侦听器不再需要时
unwatch()

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) {
    // 数据加载后执行某些操作...
  }
})

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值