Vue3中watch的诸多疑点,看完再也不敢说我懂了!

217 阅读6分钟

前端领秀.png

watch

官方描述:侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

  • 第一个参数是侦听器的
  • 第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。
  • 第三个可选的参数是一个对象,支持以下这些选项:
    • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。在 3.5+ 中,此参数还可以是指示最大遍历深度的数字。参考深层侦听器
    • flush:调整回调函数的刷新时机。参考回调的刷新时机及 watchEffect()
    • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器
    • once:(3.4+) 回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。
1.基本概论了解完了,话不多说,先来看一段Vue的代码:

image.png

<script setup>
import { watch, ref } from 'vue';
const n = ref(0);
function getN(){
    console.log('getN');
    return n.value;
}
watch(
    () => getN(),
    (newVal, oldVal) => {
        console.log('n changed', newVal, oldVal); // 输出:n changed 1, 0
    }
);
setTimeout(() => {
    n.value++;
}, 1000);
</script>

控制台输出:一开始输出一个 getN,1秒后输出getN接着继续输出 n changed 1, 0

image.png

  • watch的第一个参数是一个getN()的函数,这个方法返回一个响应式的值n;第二个参数也是一个函数,当第一个参数的值发生改变之后,就会运行该函数。

  • 但是你有没有发现,getN()返回的n.value是一个原始值,原始值不应该没有响应式吗?为什么能够重新运行呢?回答这个问题之前,有必要了解一下响应式的本质。

  • 响应式的本质是啥?一个函数在运行期间,用到了响应式数据,将来的数据一变化,这个函数重新运行。回到我们上面的代码,watch的第一个参数getN()函数,只要在这个函数的运行期间,注意是运行期间,不管这个函数在运行期间套了多少层,只要用到了响应式数据n, 将来这个n一变化,这个函数就会重新运行。

2.我们改一下,增加一个m:

watch-code-1.png

看下输出结果:

watch-code-1-log.png

  • 啥也没有,为啥?因为vue不是把所有数据拿去关联,而是把特定的数据拿去关联,比如watch的第一入参函数,而第二个则是普通函数,只有在第一个函数关联的响应式数据变化之后才会执行的回调函数。
  • 那第一个参数能不传函数,只给一个n吗?可以的,效果和传入getN()一样,watch内部会自动把n放在函数里面去执行,这就是参数归一化
3.看到这里,你是不是感觉自己好像基本掌握了watch的玩法,那就上难度:

watch-code-2.png

<script setup>
import { watch, reactive } from 'vue';

const state = reactive({
    a: 1,
    b: 2,
    c: 3
})

watch(
    () => {
       console.log(state.a + state.b);
       return state.a + state.b;
    },
    (val) => {
       console.log(val * 2);
    }
)

setTimeout(() => {
    state.a++;
    state.b--;
})
</script>

输出结果: watch-code-2-log.png

  • 你可能会想,为啥不是3和6,延迟1秒之后,state.a和state.b都改变了呀。我们来看第一次运行结构,毫无疑问直接获取响应式数据相加得到3,接下来watch不会做任何事了,那为啥不执行第二个函数中的console.log(val*2),是因为只有当第一个函数再次运行,且返回结果不一样的时候,才会运行第二个函数。除非给watch加入第3个参数,immediate: true, 一开始才会执行第二个参数。
4.继续改一下代码:

watch-code-3.png

<script setup>
import { watch, reactive } from 'vue';

const state = reactive({
    a: 1,
    b: 2,
    c: 4
})

watch(
    () => {
       console.log(state.c);
       return state.a + state.b;
    },
    (val) => {
       console.log(val * 2);
    }
)

setTimeout(() => {
    state.c++;
})
</script>

输出结果: watch-code-3-log.png

  • 第一次运行函数打印c输出4,返回3。关联了state中的a/b/c, c 1秒后重新运行+1,打印5,返回还是3,没有变化,所以不会执行第二个函数。
5.有意思吧,我们继续

watch-code-4.png

<script setup>
import { watch, reactive } from 'vue';

const state = reactive({
    a: {},
    c: 4
})

watch(
    () => {
       console.log(state.c);
       return state.a;
    },
    (val) => {
       console.log("changed");
    }
)

setTimeout(() => {
    state.c++;
})
</script>

输出结果:

watch-code-4-log.png

  • 还是4和5,为啥?因为返回的都是a同一个对象,没区别。
6.继续上难度:

watch-code-5.png

<script setup>
import { watch, reactive } from 'vue';

const state = reactive({
    a: {},
    c: 4
})

watch(
    () => {
       state.c = 5;
       console.log("1");
       return state.a;
    },
    (val) => {
       console.log("2");
    }
)

setTimeout(() => {
    state.c++;
})
</script>

输出结果:

watch-code-5-log.png

  • 你可能会觉得打印两个1,或者死循环打印1,结果为啥只打印了一个1?这就涉及到函数跟数据是怎么关联的,读取到的响应式数据是在函数运行期间发生变化,执行回调函数。这里第一个函数关联的是state.a,而不是state.c, 注意是读取到的响应式数据,state.c是在赋值,只有state.a被读取到了。后面不管你c怎么变,都跟我a没关系,所以只会答应一个1。
7.继续上难度:

watch-code-6.png

<script setup>
import { watch, reactive } from 'vue';

const state = reactive({
    a: {},
    c: 4
})
const c = state.c;

watch(
    () => {
       console.log(c);
       return state.a;
    },
    (val) => {
       console.log('2');
    }
)

setTimeout(() => {
    state.c++;
})
</script>

输出结果:

watch-code-6-log.png

  • 可以看出,这里并没有跟c关联,不是说好的第一个函数里面读取数据发生变化就执行吗?你个老六。为啥?因为c是重新定义的,不是一个响应式数据了。
8.继续上难度:

watch-code-7.png

<script setup>
import { watch, reactive } from 'vue';

const state = reactive({
    a: {},
    c: 4
})
const getC = () => state.c;

watch(
    () => {
       console.log(1);
       getC();
       return state.a;
    },
    (val) => {
       console.log('2');
    }
)

setTimeout(() => {
    state.c++;
})
</script>

输出结果:

watch-code-7-log.png

  • 第一反应,你可能会说,这题我会,单走一个1。结果为啥是2个?再来看看前面的定义:在函数运行期间,不管套了几层函数,读取到了响应式数据,就被关联上了。回到我们的题目,运行期间,读取到了getC()中的state.c响应式数据,return state.a的时候,也关联上了,c在1秒后发生变化,重新运行打印的1。所以输出的结果为两个1。不管是vue2还是vue3,里面的逻辑都是一样的。
9.最后一个问题,先不看答案,脑袋里面想一想:

watch-code-8.png

<script setup>
import { watch, reactive } from 'vue';

const state = reactive({
    a: {},
    c: 4
})

watch(
    () => {
       console.log(state);
       return state.a;
    },
    (val) => {
       console.log('2');
    }
)

setTimeout(() => {
    state.c++;
})
</script>

输出结果: watch-code-8-log.png

  • 你可能会想,打印的是整个响应式数据,函数跟state关联上了,那里面的所有成员是不是都应该被收集到依赖呢?其实不是的,这里一定的要关联的响应式数据中的某个成员,必须是成员才行。所以这里关联的还是state.a, 1秒后state.c发生变化,跟a没关系,所以只打印了state。

最后,你学废了吗?