文章目录
计算属性 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
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
注意:
- watch 默认先侦听后组件更新
- 异步侦听需要自己停止侦听,它不会随组件销毁/卸载时自动停止侦听;为避免内存泄露应该使用完毕后主动停止侦听
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>
watchPostEffect
:watchEffect
结合 flush: 'post'
(Vue3 新增)
watchPostEffect(callback)
:等组件更新后才触发侦听
watchPostEffect(callback)
等价于 :
<script setup lang="ts">
import { watchEffect } from 'vue'
watchEffect(callback, {
flush: 'post'
})
</script>
watchSyncEffect
:watchEffect
结合 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()
使我们可以:
- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的前一个值和当前值。