5.Computed and watch【监控变化】
# computed
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象【默认传入的是get函数的对象】
传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态
const count = ref(1)
/*不支持修改【只读的】 */
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误!
/*【可更改的】 */
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
},
})
plusOne.value = 1
console.log(count.value) // 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# watchEffect
立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数
computed
与watchEffect
区别:
- computed计算属性可通过setup return,再模板中使用,watchEffect不能;
- computed可以使用多个,并且对多个属性进行不同的响应计算,watchEffect会存在副作用
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 打印出 0
setTimeout(() => {
count.value++
// -> 打印出 1
}, 100)
2
3
4
5
6
7
8
9
# 停止观察
当在组件的setup()函数或生命周期钩子期间调用watchEffect时,监视程序会链接到组件的生命周期,并在卸载组件时自动停止 一般情况下watchEffect返回可以stop 操作,停止监听程序
const stop = watchEffect(() => {
/* ... */
})
// 停止监听程序
stop()
2
3
4
5
6
# 副作用(函数式编程)
一个带有副作用的函数不仅只是简单的返回一个值,还干了一些其他的事情,比如:
- 修改一个变量
- 直接修改数据结构
- 设置一个对象的成员
- 抛出一个异常或以一个错误终止
- 打印到终端或读取用户的输入
- 读取或写入一个文件
- 在屏幕上绘画
buyCoffee的例子(p3):函数只不过是需要返回一杯咖啡,可是却对费用进行了持久化操作(产生副作用),我们可以在buyCoffee方法返回咖啡时也把费用作为值一并返回,将费用这条记录交给其他程序来做持久化,以此来去除副作用 ====》通过把这些副作用推到程序的外层,来转换任何带有副作用的函数(纯的内核和一层很薄的外围来处理副作用)
如果一个函数内外有依赖于外部变量或者环境时,常常我们称之为其有副作用,如果我们仅通过函数签名不打开内部代码检查并不能知道该函数在干什么,作为一个独立函数我们期望有明确的输入和输出,副作用是bug的发源地,作为程序员开发者应尽量少的开发有副作用的函数或方法,副作用也使得方法通用性下降不适合扩展和可重用性。
# 清除副作用
watchEffect函数都是副作用
在一些时候监听函数将执行异步副作用【一个响应式依赖被修改了,会做其他事情】,这些响应需要在其失效时清除(例如在效果完成前状态改变)。effect函数接收一个onInvalidate 函数作入参, 用来注册清理失效时的回调。这个 invalidation函数 在什么时候会被调用:
监听函数重新被执行的时候【响应式依赖的数据被修改】
监听停止的时候(如果watchEffect在setup()或者生命周期函数中被使用的时候组件会被卸载)【停止观察】
watchEffect(onInvalidate => {
/*这是个异步操作*/
const token = performAsyncOperation(id.value)//id依赖
onInvalidate(() => {
// id被修改了或者监听停止了会触发token.cancel()事件【这块区域的代码】.
// 这里是异步事件的话,前面的peding的异步操作无效【这里的异步事件只执行一次】
token.cancel()/*异步操作*/
console.log('onInvalidate')
})
})
2
3
4
5
6
7
8
9
10
# 副作用刷新时机
注意setup()将在组件挂载前调用,因此如果想要在watchEffect中使用 DOM (或者组件),请在挂载的钩子中声明watchEffect
onMounted(() => {
watchEffect(() => {
// access the DOM or template refs
})
})
2
3
4
5
还可以为watchEffect传入额外的对象作为参数。 比如通过设置flush来设置watchEffect是异步执行还是在组件更新前执行
// 同步运行
watchEffect(
() => {
/* ... */
},
{
flush: 'sync'
}
)
// 组件更新前执行
watchEffect(
() => {
/* ... */
},
{
flush: 'pre'
}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 侦听器调试【响应式调试用的】
onTrack(追踪依赖时调用)和onTrigger(依赖改变触发了watchEffect的方法时触发)参数可以用来调试watchEffect的行为
- 当一个 reactive 对象属性或一个 ref 作为依赖被追踪时,将调用 onTrack【调用次数为被追踪的数量】
- 依赖项变更会导致重新追踪依赖,从而onTrack被调用【调用次数为被追踪的数量】
- 依赖项变更导致副作用被触发时,将调用 onTrigger
这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger 语句来检查依赖关系:【onTrack 和 onTrigger 仅在开发模式下生效】
watchEffect(
() => {
/* 副作用的内容 */
},
{
onTrigger(e) {
/*副作用依赖修改*/
debugger
},
onTrack(e) {
/*副作用依赖修改*/
debugger
},
}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# watch
watch API 完全等效于 2.x watch 中相应的选项。watch 需要侦听特定的数据源,并在回调函数中执行副作用【默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调】
watch允许我们:
- 懒执行副作用
- 更明确哪些状态的改变会触发侦听器重新运行副作用
- 访问侦听状态变化前后的值
# 侦听单个数据源
侦听器的数据源可以是一个拥有返回值的 getter 函数,也可以是 ref:
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# 侦听多个数据源
// watcher 也可以使用数组来同时侦听多个源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
2
3
4
5
# 与 watchEffect 共享的行为
watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机 和 侦听器调试 等方面行为一致
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar],onInvalidate) => {
/* ... */
onInvalidate(() => {...})
},
{
onTrigger(e) {
/*副作用依赖修改*/
debugger
},
onTrack(e) {
/*副作用依赖修改*/
debugger
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# watchEffect 与 watch 有什么不同?
- 第一点我们可以从示例代码中看到 watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当* 这些属性变更的时候,这个回调都会执行,而 watch 只能监听指定的属性而做出变更(v3开始可以同时指定多个)。
- 第二点就是 watch 可以获取到新值与旧值(更新前的值),而 watchEffect 是拿不到的。
- 第三点是 watchEffect 如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而 watch 不需要,因为他一开始就指定了依赖。 从他们的不同点可以看出,他们的优缺点。并且可以在业务需求面前做出正确的选择。
完整例子:
import { reactive, watch, toRefs, computed,watchEffect } from 'vue'
export default {
setup () {
const state = reactive({
count: 0,
doubleCount: computed(() => {
return state.count * 2
}),
a: 1,
watchCount: 0,
watchCount1: 1
})
watchEffect(() => {
console.log('watchEffect', state.count, state.a)
}, {
onTrack() {
console.log('onTrack调用') // 当反应性属性或ref作为依赖项被跟踪时
},
onTrigger() {
console.log('ontrigger调用') // 当观察程序回调由依赖项的变异触发时
}
})
watch(() => {
return [state.watchCount, state.watchCount1]
}, (val, prev) => {
console.log(val, prev)
})
setTimeout(() => {
state.watchCount++
}, 1000)
const addRef = () => {
state.count++
}
return {
// 将代理对象转换为纯对象。并对其每个key做包装,包装为ref
...toRefs(state),
addRef,
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40