vue3 watch 多个写法区别分析

114 阅读2分钟

起因

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记录,我们观察一下变更值时整体运行

WeChatWorkScreenshot_eb582c1c-8894-4fee-aa8e-00bab3883a20.png 我们可以看出值改变的时候会触发组件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的深度递归,需要自己手动补充才可以维持。

结论

  1. 传递基础值的时候需要用到函数去包裹执行收集,也就是b的情况。
  2. 为对象的时候直接传递即可,如果使用函数会失去deep的观察,要自己视情况补充deep。

reference

  1. cn.vuejs.org/guide/essen…