起因
vue3 在watch的时候,究竟用箭头函数还是直接对象就可以,什么时候需要用deep。每次写的时候都需要试试是否生效。
分析
例子:props分别传递对象和基础值
// app.vue
<template>
<button @click="changA">changA</button>
<button @click="changB">changB</button>
<HelloWorld :b="b" :a="a"/>
</template>
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import { ref } from 'vue'
const b = ref('1')
const a = ref([{c: 1}, {c: 2}, {c: 3}])
const changA = () => {
a.value[0].c = 4
}
const changB = () => {
b.value = '2'
}
</script>
<template>
<div>{{ props.b }}</div>
</template>
<script setup>
import { watch } from 'vue';
/* eslint-disable */
const props = defineProps({
a: Array,
b: String
});
watch(() => props.a, (val) => {
console.log('a changed 1');
}) // 没触发
watch(props.a, (val) => {
console.log('a changed 2')
}) // 触发
watch(() => props.b, () => {
console.log('b changed');
}) // 触发
</script>
通过perfomance记录,我们观察一下变更值时整体运行
我们可以看出值改变的时候会触发组件patch更新,从而更新了props(updateProps, setfullprops), 最后就把job给运行了。(这部分属于双向绑定的收集执行,晚点会再出一篇文章专门讲)
graph LR
patch --> updateConponent --> updateProps --> trigger执行函数
源码解析
回到问题我们什么时候需要用函数,什么时候不用函数。
import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_HelloWorld = _resolveComponent("HelloWorld")
return (_openBlock(), _createBlock(_component_HelloWorld, {
b: _ctx.b,
a: _ctx.a
}, null, 8 /* PROPS */, ["b", "a"]))
}
这是app.vue helloword解析出来的样子,我们可以看到 a,b的传递都是直接把值赋予的,这时候a为proxy对象, b就是1。那么为什么watch b能生效呢,因为在initProps的时候会用shallowReactive裹一层。
// runtime-core/src/componentProps.ts
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: number, // result of bitwise flag comparison
isSSR = false
) {
...
if (isStateful) {
// stateful
instance.props = isSSR ? props : shallowReactive(props)
} else {
...
再结合watch代码
// runtime-core/src/apiWatch.ts
function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
...
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
getter = () => source
deep = true
} else if (isArray(source)) {
...
} else if (isFunction(source)) {
...
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
...
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
...
}
我们可以看出如果直接是watch(props.b),等于 watch(1)不符合任意条件所以会报错,而函数的写法命中isFunction,则会触发收集(effect.run),因为props是shallowReactive。 反观如果props.a 使用函数写法,则失去了deep的自动补充(原本命中 isReactive),所以不会有traverse的深度递归,需要自己手动补充才可以维持。
结论
- 传递基础值的时候需要用到函数去包裹执行收集,也就是b的情况。
- 为对象的时候直接传递即可,如果使用函数会失去deep的观察,要自己视情况补充deep。