响应式:computed & watch

计算属性 computed

具有缓存性,计算属性是基于它们的反应依赖关系缓存的,计算属性只在相关响应式依赖发生改变时它们才会重新求值

作用: 防止变量污染

使用场景: 对原数据进行改造输出,例如 数据格式化(添加符号、大小写转换)、数据过滤、顺序重排……

注意:

  • 这也同样意味着下面的计算属性将不再更新,因为 Date.now () 不是响应式依赖:

    computed: {
      now() {
        return Date.now()
      }
    }
    
  • 不要在 computed 中做异步请求或者更改 DOM

函数写法

<script setup>
import { ref, computed } from "vue";

const firstName = ref("");
const lastName = ref("");

// 只读,不允许修改值
const name = computed(() => {
  return fitstName + lastName;
});
</script>

getter/setter

计算属性默认只有 getter,不过在需要时你也可以提供一个 setter

<script setup>
import { ref, computed } from "vue";

const firstName = ref("");
const lastName = ref("");

const name = computed({
  get() {
    return firstName.value + lastName.value;
  },
  set(newVal) {
    const names = newVal.split(" "); // ['www', 'abc']
    firstName.value = names[0];
    lastName.value = names[names.length - 1];
  },
});

setTimeout(() => {
  firstName.value = "www";
}, 1500);
setTimeout(() => {
  lastName.value = "王微微";
}, 3000);
setTimeout(() => {
  name.value = "www abc";
}, 5000);

</script>

侦听器 watch

watch 观察和响应 Vue 实例上的数据变动,理论上 computed 计算属性能实现的 watch 侦听器也能实现

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

注意:

  1. watch 默认先侦听后组件更新
  2. 异步侦听需要自己停止侦听,它不会随组件销毁/卸载时自动停止侦听;为避免内存泄露应该使用完毕后主动停止侦听

watch

watch(source, callback, options) 追踪指定的数据源

  • source:监听变化的数据
    • 一个函数,返回一个值
    • 一个 ref
    • 一个响应式对象
    • …或是由以上类型的值组成的数组
  • callback:监听回调函数
    • value 新值
    • oldValue 旧值
    • onCleanup 注册副作用清理的回调函数
  • options: 选项
// 侦听单个来源
function watch<T>(
  source: WatchSource<T>,
  callback: WatchCallback<T>,
  options?: WatchOptions
): StopHandle

// 侦听多个来源
function watch<T>(
  sources: WatchSource<T>[],
  callback: WatchCallback<T[]>,
  options?: WatchOptions
): StopHandle

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
  once?: boolean // 默认:false (3.4+)
}
source
直接侦听一个 ref
<script setup>
import { ref, watch } from "vue";

const x = ref(0)
watch(x, (newVal, oldVal) => {});
</script>
getter 函数(复杂表达式)
<script setup>
import { ref, watch } from "vue";

const x = ref(0)
const y = ref(0)


// 表达式 `x.value + y.value` 每次得出一个不同的结果时,处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)
</script>
侦听多个来源
<script setup>
  import { reactive, watch } from 'vue'

  const detail = reactive({
    firstName: 'a',
    lastName: '1',
  })

  setTimeout(() => {
    detail.firstName = 'ww'
  }, 500)

  // 多个来源 组成的数组
  watch([() => detail.firstName, () => detail.lastName], (newVal, oldVal) => {
    console.log(newVal) // ['ww', '1']
    console.log(oldVal) // ['a', '1']
  })
</script>
直接侦听一个 reactive 对象

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

<script setup>
  import { reactive, watch } from 'vue'
  const obj = reactive({ count: 0 })

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

  setTimeout(() => {
    obj.count++
  }, 2000)
</script>

注意:

  • 直接监听 reactive 对象,开启和不开启deep效果一样
    watch(obj, (newValue, oldValue) => {}, {
      deep: true
    })
    
  • obj 被替换时不会触发 watch 直接监听对象
    obj = { count: 2 }
    
getter 函数 监听 reactive 对象

相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

<script setup>
  import { reactive, watch } from 'vue'
  const obj = reactive({ count: 0 })

  watch(
    () => obj,
    (newValue, oldValue) => {
      // 仅当 obj 被替换时触发
      console.log(newValue.count, oldValue.count) // 1 1
    },
  )

  setTimeout(() => {
    console.log('我更改了')
    obj.count++ // obj.count 修改 不会触发 watch 使用 getter函数 监听obj
  }, 2000)
</script>

你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:

当使用 watch 选项 getter函数 侦听对象/数组时,只有在对象/数组被替换时才会触发回调。换句话说,在对象/数组 属性改变时 watch 回调将不再被触发。要想在对象/数组 属性改变时触发 watch 回调,必须指定 deep 选项

来自:v3 迁移指南

watch(
  () => obj,
  (newValue, oldValue) => {
    // 注意:
    // 修改 obj 属性触发的监听,`newValue` 此处和 `oldValue` 是相等的
    // 因为它们的引用指向同一个。Vue 不会保留变更之前值的副本。
    // 除非 obj 整个被替换了
  },
  // 为了发现对象内部值的变化,可以在选项参数中指定 `deep: true`。这个选项同样适用于监听数组变更
  // 这样 obj.count 修改 也能监听到
  { deep: true } 
)
callback
onCleanup 清除副作用

假设用户调触发watch回调执行,且回调内有个请求,但是请求的回调比较慢,在这个期间,用户再次触发watch回调执行。那么就产生了副作用,因为上一个请求还没有结束。在这种情况下,就可以通过 onCleanUp 来清除副作用,也就是在 onCleanup 内取消之前的请求

<script setup lang="ts">
  import { reactive, watch } from 'vue'
  let obj = reactive({ id: 1 })

  let controller = new AbortController()

  watch(obj, async (_val, _oldVal, onCleanup) => {
    console.log(obj.id)

    fetch(`https://jsonplaceholder.typicode.com/todos/${obj.id}`, {
      signal: controller.signal, // 获取 AbortController 的信号对象
    })
      .then(response => {
        console.log(response, '=== response')
      })
      .catch(error => {
        console.log(error, '=== error')
      })

    // 当 `id` 变化时,`cancel` 将被调用,
    // 取消之前的未完成的请求
    onCleanup(() => {
      console.log('==== onCleanup,执行cancel')
      // 取消之前的 Fetch 请求
      controller.abort()

      // 解决新的请求也被中止问题
      controller = new AbortController()
    })
  })

  setTimeout(() => {
    console.log('变化后,触发watch 111111')
    obj.id = 2
  }, 1000)
  setTimeout(() => {
    console.log('变化后,触发watch  22222')
    obj.id = 3
  }, 1002)
</script>

在这里插入图片描述

注意:正常的执行顺序是 自上而下,但是 watch onCleanUp 会比 上面代码更早触发。所以 onCleanUp 写在那个位置不是很重要。

补充:

  watch(obj, async () => {
    console.log(obj.id)

    // 取消之前的 Fetch 请求
    controller.abort()

    // 解决新的请求也被中止问题
    controller = new AbortController()

    fetch(`https://jsonplaceholder.typicode.com/todos/${obj.id}`, {
      signal: controller.signal, // 获取 AbortController 的信号对象
    })
      .then(response => {
        console.log(response, '=== response')
      })
      .catch(error => {
        console.log(error, '=== error')
      })
  })

以上写法虽然结果等同于使用 onCleanup 的结果,但是这样写每次回调触发都会执行如下代码,而使用 onCleanup 第一次触发回调执行并不会触发 onCleanup

   // 取消之前的 Fetch 请求
   controller.abort()

   // 解决新的请求也被中止问题
   controller = new AbortController()

补充2:


<script setup lang="ts">
  import { reactive, watch } from 'vue'
  let obj = reactive({ id: 1 })

  let controller = new AbortController()

  watch(obj, async (_val, _oldVal, onCleanup) => {
    console.log(obj.id)

    fetch(`https://jsonplaceholder.typicode.com/todos/${obj.id}`, {
      signal: controller.signal, // 获取 AbortController 的信号对象
    })
      .then(response => {
        console.log(response, '=== response')
      })
      .catch(error => {
        console.log(error, '=== error')
      })

    // 当 `id` 变化时,`cancel` 将被调用,
    // 取消之前的未完成的请求
    onCleanup(() => {
      console.log('==== onCleanup,执行cancel')
      // 取消之前的 Fetch 请求
      controller.abort()

      // 解决新的请求也被中止问题
      controller = new AbortController()
    })
  })

  setTimeout(() => {
    console.log('变化后,触发watch 111111')
    obj.id = 2
  }, 1000)
  setTimeout(() => {
    console.log('变化后,触发watch  22222')
    obj.id = 3
  }, 3000)
</script>

在这里插入图片描述

options
deep

默认 false,true 深度侦听

immediate

默认 fakse,true 即时回调的侦听器。

注意: 在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

// 这会导致报错
const unwatch = vm.$watch(
  "value",
  function () {
    doSomething();
    unwatch();
  },
  { immediate: true }
);

如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:

let unwatch = null;

unwatch = vm.$watch(
  "value",
  function () {
    doSomething();
    if (unwatch) {
      unwatch();
    }
  },
  { immediate: true }
);
once V3.4+

once: false 每当被侦听源发生变化时,侦听器的回调就会执行。
once: true 回调只在源变化时触发一次

watch(
  source,
  (newValue, oldValue) => {
    // 当 `source` 变化时,仅触发一次
  },
  { once: true }
)
flush 回调触发机制(Vue3新增)

flush 选项可以更好地控制回调的时间。它可以设置为 'pre''post''sync'

  • 'pre':默认值 ,指定的回调应该在渲染前被调用。它允许回调在模板运行前更新了其他值。

  • 'post' :可以用来将回调推迟到渲染之后的。如果回调需要通过 $refs 访问更新的 DOM 或子组件,那么则使用该值。

  • 'sync':一旦值发生了变化,回调将被同步调用。

注意:

  • 对于 'pre''post',回调使用队列进行缓冲。回调只被添加到队列中一次,即使观察值变化了多次。值的中间变化将被跳过,不会传递给回调。
  • 缓冲回调不仅可以提高性能,还有助于保证数据的一致性。在执行数据更新的代码完成之前,侦听器不会被触发。
  • 'sync' 侦听器应少用,因为它们没有这些好处。
onTrack & onTrigger

调试侦听器的依赖。

watch(source, callback, {
  flush: 'post',
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

unwatch:取消 watch

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

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

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

// 宿主组件卸载时自动停止
watchEffect(() => {})

// ...这个则不会!
// 异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏
let unwatch
setTimeout(() => {
  unwatch = watchEffect(() => {})
}, 100)
  
// ...当该侦听器不再需要时
unwatch && unwatch()
</script>

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

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

watchEffect(() => {
  if (data.value) {
    // 数据加载后执行某些操作...
  }
})

watchEffect:结合 immediate: true(Vue3 新增)

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数

watchEffect(callback, options) :追踪所有响应式数据

watchEffect(callback, options) 等价于 :

<script setup lang="ts">
import { watch } from 'vue'
  
watch(obj, callback, {
  immediate: true
})
</script>

示例:

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

import { ref, watchEffect } from "vue";

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})
callback: onCleanUp 清除副作用

watch 回调触发执行前, 先触发 onCleanUp

<script setup>
  import { reactive, watchEffect } from 'vue'
  let obj = reactive({ count: 0 })

  watchEffect(onCleanUp => {
    console.log(obj.count)
		
    // 注意:正常的执行顺序是 自上而下
    // 但是 onCleanUp  会比 上面代码更早触发
    onCleanUp(() => {
      console.log('=== onCleanUp ') // 触发 watch 监听前 触发 onInvalidate
    })
  })

  setTimeout(() => {
    obj.count++
  }, 1000)
</script>
组件销毁/停止监听 触发 onCleanUp 副作用
<script setup>
  import { reactive, watchEffect } from 'vue'
  let obj = reactive({ count: 0 })

  const stop = watchEffect(() => {
    console.log(obj) // stop 不会触发watch
  })

  setTimeout(() => {
    console.log('==== stop')
    stop()
  }, 2000)
</script>

设置 onCleanUp 清除副作用

<script setup>
  import { reactive, watchEffect } from 'vue'
  let obj = reactive({ count: 0 })

  const stop = watchEffect(onCleanUp => {
    console.log(obj) // stop 不会触发watch

    onCleanUp(() => {
      console.log('=== onCleanUp ') // stop 会触发 watch 里面的 onInvalidate
    })
  })

  setTimeout(() => {
    console.log('==== stop')
    stop()
  }, 2000)
</script>

执行stop函数停止侦听,此时会触发onCleanUp 的回调函数。同样,watchEffect 所在的组件被卸载时会隐式调用stop函数停止侦听,故也能触发onCleanUp 的回调函数。

使用场景:

防抖、退出页面清除一些事件/监听、组件销毁前进行数据保存等

<script setup>
  import { reactive, watchEffect } from 'vue'
  const obj = reactive({ id: 2 })

  const stop = watchEffect(onCleanUp => {
    onCleanUp(async () => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${obj.id}`)
      console.log(response, '=== response') // 只在初始化 和 频繁触发结束后执行
    })
  })

  setTimeout(() => {
    console.log('===stop')
    stop() // 模拟组件销毁
  }, 1000)
</script>
onTrack 和 onTrigger
<script setup>
  import { reactive, watchEffect } from 'vue'
  const obj = reactive({ id: 2 })

  const stop = watchEffect(
    onInvalidate => {
      console.log(obj.id)

      onInvalidate(async () => {
        console.log('=== onInvalidate')
      })
    },
    {
      onTrack() {
        console.log('= onTrack')
      },
      onTrigger() {
        // 数据更改监听,可用于开发调试
        console.log('=== onTrigger')
        // debugger
      },
    },
  )

  setTimeout(() => {
    console.log('== 开始修改了')
    obj.id = 3
  }, 1000)
  setTimeout(() => {
    console.log('===stop')
    stop()
  }, 3000)
</script>

在这里插入图片描述

watchPostEffectwatchEffect 结合 flush: 'post'(Vue3 新增)

watchPostEffect(callback) :等组件更新后才触发侦听

watchPostEffect(callback) 等价于 :

<script setup lang="ts">
import { watchEffect } from 'vue'
  
watchEffect(callback, {
  flush: 'post'
})
</script>

watchSyncEffectwatchEffect 结合 flush: 'sync'(Vue3 新增)

watchSyncEffect(callback) :一旦值发生了变化,回调将被同步调用。

watchSyncEffect(callback) 等价于 :

<script setup lang="ts">
import { watchEffect } from 'vue'
  
watchEffect(callback, {
  flush: 'sync'
})
</script>

watch VS watchEffect

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

  • 懒执行副作用;
  • 更加明确是应该由哪个状态触发侦听器重新执行;
  • 可以访问所侦听状态的前一个值和当前值。
  • 33
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值