Watch:监听Reactive值的变化
我们经常需要在Reactive值发生变化时附加逻辑。
举例:
- Input内容变化后通知搜索服务进行输入提示
- 用户选定的曲目发生变化时通知播放器切换曲目
- 购物车数据变化后通知服务端记录
在`Reactive` 模型中,每个Reactive值发生变化,都可能会触发另一组行为。再比如:
- 用户的切换导致显示头像的切换
- 购买数量的变化触发重新计算营销卡券使用
因此,Reactive值,需要一个监听机制, 这就是`watch` 和`watchEffect` 。
Side Effect(副作用)、Side Effect Invalidate(副作用失效)
在理解`watch` 和`watchEffect` 前,需要理解两个基础概念:
- Side Effect
- Side Effect Invalidation
副作用(Side Effect)
简单理解:副作用是计算之外的逻辑。
Vue、React这类渲染引擎,根据属性、状态计算视图。
view = f(props, state)
计算视图之外的,就是副作用(Effect)。
例如:
function SomeComponent(a, b) {
window.location.href = '...' // 副作用
const c = ref(0) // 副作用
watchEffect(..) // 副作用
return <div>{a + b + c.value}</div>
}
- 当reactive值更新的时候,触发vue组件更新可以看作一个副作用。
- 当用户打开页面,发送请求REST API,可以看作副作用
**React/Vue内部是很纯的计算逻辑,所有【人机交互】,都是副作用。**
**划重点:简单的,你可以将副作用理解成纯计算背后产生的效果。**
举例:
- 当你点击菜单的时候,切换了`currentIndex` (一个整数), 右侧显示内容变化是因为副作用。
- 当你删除文件的时候,你仅仅按下了del键,文件真实在磁盘上被删除是你操作的del键的副作用。
`watch` 和`watchEffect` 监听Reactive值变化(ref, reactive),本质是监听它们产生的副作用。从设计上,`ref` 和`reactive` 是值,但是它们会触发`track` 和`trigger` 两个过程,`trigger` 还会触发重绘——这些都是副作用。
副作用失效(Side Effect Invalidation)问题
设想你点击一个导航菜单,切换页面, 有可能会出现这种情况:
- 页面A还没打开,你已经点击了打开页面B的按钮
【点击打开A】和【点击打开B】是两次计算,比如我们这样:
const currentIndex = ref("")
// 点击打开A
currentIndex.value = "A"
// 点击打开B
currentIndex.value = "B"
但是这两次计算产生的副作用——打开页面A和B,可能只有B完成——因为用户点太快了,页面A还没加载完,就切换了。
像这样,一个副作用没有执行完,下一个副作用已经到来了,上一次的副作用我们称为一个失效的副作用(Invalidate)。
这样看, `watch` 和`watchEffect` 要处理所有的副作用,还需要处理副作用失效的问题。
watchEffect
在实现UI的过程中,我们经常需要添加一些副作用,这个时候,我们需要了解UI内部状态的变化,比如:
- 何时一个reactive的值发生变化?
- 何时页面发生渲染(前、后)?
我们需要在发生上述行为的时候,添加副作用。
这时,可以用`watchEffect` 。当:
- `reactive` 值发生变化
- 界面的初次渲染
`watchEffect` 中注册的副作用会执行。
准确说,当reactive值被追踪 (track)的时候,watchEffect中的回调函数会执行;当watchEffect的依赖发生变化的时候,watchEffect中的回调函数会重复执行。
- watchEffect仅处理跟踪到的依赖
- 没有依赖的watchEffect可以当成一定会执行一次的效果处理发送请求等逻辑
因此`watchEffect` 在`track` 和`trigger` 阶段都会执行。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
副作用失效的处理
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
另一个例子:
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {
/* ... */
}) // we register cleanup function before Promise resolves
data.value = await fetchData(props.id)
})
watchEffect的执行时机(不推荐)
`effect` 有3种执行时机:
- pre(默认):effect在render之前执行
- post:将effect推迟到更新后执行
- sync : 当值变更时立刻执行
这3种时机可以用options.flush配置,具体参考示例。
Vue用一个队列来存储所有的effect(callback),当值变化多次的时候,只会有1次effect被执行,因为effect只会被写入队列一次。
Watch
watch和watchEffect是一类东西,底层实现一致,本质都是对变更的追踪(vue2 Watcher)。
实际使用的过程中,watch提供对单个reactive值的追踪,语义更明确(推荐用watch)。
function watch<T>(
source: WatcherSource<T>,
callback: (
value: T,
oldValue: T,
onInvalidate: InvalidateCbRegistrator
) => void,
options?: WatchOptions
): StopHandle
watch提供对观察源(WatcherSource)的监控,当观察源发生变化的时候,触发`callback` 函数。
观察源的定义:
type WatcherSource<T> = Ref<T> | (() => T)
1
export const WatcheExample01 = defineComponent({
setup : () => {
const c = ref(0)
watch(c, (newVal, old) => {
console.log(`c changed from ${old} to ${newVal}`)
})
setTimeout(() => {
c.value ++
}, 1000)
return () => {
return <div>{c.value}</div>
}
}
})
2、监听多个
export const WatcheExample02 = defineComponent({
setup : () => {
const c = ref(0)
const d = ref(0)
const m = ref([1,2,3,4,5])
// m.value.push(1)
watch(m, () => {
})
watch([c, d], (arrVals, oldValues) => {
console.log(arrVals, oldValues)
})
setTimeout(() => {
c.value ++
d.value = 10
}, 1000)
return () => {
return <div>{c.value + d.value}</div>
}
}
})
3、监听函数
export const WatcheExample03 = defineComponent({
setup : () => {
const c = ref(0)
// const state = reactive({...})
watch(() => c.value, (newVal, old) => {
console.log(`c changed from ${old} to ${newVal}`)
})
setTimeout(() => {
c.value ++
}, 1000)
return () => {
return <div>{c.value}</div>
}
}
})
从类型上,Reactive也是一种Ref。
我们可以观察一个函数,或者一个`Ref` 。举个例子(可以直接观察Ref或者一个返回值的函数)
// watching a getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// directly watching a ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
也可以同时观察多个对象:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
当然,watch有重载版的定义:
function watch<T extends WatcherSource<unknown>[]>(
sources: T
callback: (
values: MapSources<T>,
oldValues: MapSources<T>,
onInvalidate: InvalidateCbRegistrator
) => void,
options? : WatchOptions
): StopHandle
type WatcherSource<T> = Ref<T> | (() => T)
type MapSources<T> = {
[K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}
对于Watch Option,这里有这样的定义:
interface WatchOptions extends WatchEffectOptions {
immediate?: boolean // default: false
deep?: boolean
}
如果我们需要在初始化后,直接执行`watch` 中的回调函数,可以使用`immediate=true`。当我们需要深度监听一个对象,例如一个数组,可以带上`deep=true` 。
1