Vue 3.x 响应式:核心

发布于 2024-07-25 08:02:42 字数 7397 浏览 8 评论 0

watch()

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

type WatchCallback<T> = (
  value: T,
  oldValue: T,
  onCleanup: (cleanupFn: () => void) => void
) => void

type WatchSource<T> =
  | Ref<T> // ref
  | (() => T) // getter
  | T extends object
  ? T
  : never // 响应式对象

interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // 默认:false
  deep?: boolean // 默认:false
  flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

// 侦听单个来源
function watch<T>(
  source: WatchSource<T>,
  callback: WatchCallback<T>,
  options?: WatchOptions
): StopHandle

// 侦听多个来源
function watch<T>(
  sources: WatchSource<T>[],
  callback: WatchCallback<T[]>,
  options?: WatchOptions
): StopHandle
  1. 参数一: watch() 的侦听源可以是以下几种:
    • 一个函数,返回一个值
    • 一个 ref
    • 一个响应式对象(reactive)
    • ...或是由以上类型的值组成的数组
  2. 参数二:这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。

    该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

  3. 参数三(可选):配置选项
    • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考 深层侦听器
    • flush:调整回调函数的刷新时机(默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态)。参考 回调的刷新时机watchEffect()
    • onTrack / onTrigger:调试侦听器的依赖。参考 调试侦听器

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

  • 懒执行副作用;
  • 更加明确是应该由哪个状态触发侦听器重新执行;
  • 可以访问所侦听状态的前一个值和当前值。
// 1. 侦听一个 getter 函数
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {}
)

// 2. 侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {})

// 3. 侦听多个来源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {})

当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果你想让回调在深层级变更时也能触发,你需要使用 { deep: true } 强制侦听器进入深层级模式。在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。

const state = reactive({ count: 0 })
watch(
  () => state,
  (newValue, oldValue) => {
    // newValue === oldValue
    // 在嵌套的属性变更时触发
  	// 注意:newValue 此处和 oldValue 是相等的
  	// 因为它们是同一个对象!
  },
  { deep: true }
)
// 当直接侦听一个响应式对象时,侦听器会自动启用深层模式
const state = reactive({ count: 0 })
watch(state, () => {
  /* 深层级变更状态所触发的回调 */
})

// 一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  }
)

// 显式地加上 deep 选项,强制转成深层侦听器
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了
  },
  { deep: true }
)

// watch() 和 watchEffect() 享有相同的刷新时机和调试选项:
watch(source, callback, {
  flush: 'post',
  onTrack(e) {
    debugger
  }
})

watchEffect()

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

type OnCleanup = (cleanupFn: () => void) => void

interface WatchEffectOptions {
  flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

type StopHandle = () => void

function watchEffect(
  effect: (onCleanup: OnCleanup) => void,
  options?: WatchEffectOptions
): StopHandle
  • 第一个参数:就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。

    清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求。

  • 第二个参数:用来调整副作用的刷新时机或调试副作用的依赖。默认情况下,侦听器将在组件渲染之前执行。

    设置 flush: 'post' 将会使侦听器延迟到组件渲染之后再执行。详见 回调的触发时机

    在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置 flush: 'sync' 来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。

  • 返回值:是一个用来停止该副作用的函数。
const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 输出 0

count.value++
// -> 输出 1
// 回调会立即执行。在执行期间,它会自动追踪 url.value 作为依赖(和计算属性的行为类似)。
// 每当 url.value 变化时,回调会再次执行。
watchEffect(async () => {
  const response = await fetch(url.value)
  data.value = await response.json()
})

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

副作用清除:

watchEffect(async (onCleanup) => {
  const { response, cancel } = doAsyncWork(id.value)
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前
  // 未完成的请求
  onCleanup(cancel)
  data.value = await response
})

选项:

watchEffect(() => {}, {
  flush: 'post',
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

停止侦听器:

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

watchPostEffect()

watchEffect() 使用 flush: 'post' 选项时的别名。

watchSyncEffect()

watchEffect() 使用 flush: 'sync' 选项时的别名。

watch vs. watchEffect

它们之间的主要区别是追踪响应式依赖的方式

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

ら栖息

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

13886483628

文章 0 评论 0

流年已逝

文章 0 评论 0

℡寂寞咖啡

文章 0 评论 0

笑看君怀她人

文章 0 评论 0

wkeithbarry

文章 0 评论 0

素手挽清风

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文