index.vue
<template>
<div>
<div>{{ btnText }}</div>
<div>{{ btnText2 }}</div>
</div>
</template>
<script setup lang="ts">
// 脚本内容
import { useConfigHook } from './hooks/useConfig';
import { ComponentInternalInstance, getCurrentInstance, nextTick, ref } from 'vue';
const instance = getCurrentInstance() as ComponentInternalInstance;
const btnText = ref('111');
setTimeout(() => {
btnText.value = '2222';
}, 3000);
const { btnText2 } = useConfigHook(instance);
defineExpose({
btnText,
});
</script>
useConfig.ts
import type { ComponentInternalInstance } from '@vue/runtime-core';
import { computed, watch } from 'vue';
export const useConfigHook = (vm: ComponentInternalInstance) => {
if (!vm) {
throw new Error('vm is required');
}
const btnText2 = computed(() => {
console.log('btnText:', vm.exposed?.btnText?.value)
return `${(vm.exposed?.btnText?.value as string) || ''}` + '33333';
});
watch(
() => btnText2,
() => {
console.log('watch-btnText2-----', btnText2.value);
},
{ immediate: true },
);
return {
btnText2,
};
};
当btnText的值发生变化时,在hooks里通过watch监听的值并没有同步打印,而且btnText2在组件模版中也没有同步展示。难道vm.exposed?.btnText失去了响应式?
但是,当我们把watch注释掉后,发现btnText2又恢复了响应式,在组件模版中同步更新了,为什么watch对btnText2的监听还会影响到btnText2的响应式呢?
// watch(
// () => btnText2,
// () => {
// console.log('watch-btnText2-----', btnText2.value);
// },
// { immediate: true },
// );
这其实是computed的缓存机制造成的。具体来说,就是当computed函数执行时,返回的一个对象,如下
function computed(getter) {
let value;
let dirty = true;
const effectFn = effect(getter, {
lazy: true
});
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
return value;
}
};
return obj;
}
我们先来看当没有watch监听btnText2时,发生了什么
-
当computed执行时,传入的回调getter被包裹进了effect函数中生产了一个effectFn函数,当effectFn函数执行时,会调用getter函数返回值,不过此时effectFn还没执行
-
因为组件template中有btnText2的值,当组件的render函数执行此处时,会去获取btnText2.value的值,这时候就会触发obj的value属性的get函数,此时dirty是true,就会执行这句代码
value = effectFn();,在effectFn内部就会执行getter函数,返回${(vm.exposed?.btnText?.value as string) || ''} + '33333'的值。与此同时,comouted内部也会将render函数加入到依赖列表中。 -
我们在第2步执行getter函数时,调用了vm.exposed?.btnText?.value。
btnText是一个响应式数据,所以当getter函数调用vm.exposed?.btnText?.value时,btnText内部会将此时computed的副作用函数加入到自己的依赖列表中。 -
当btnText的值发生改变时,会依次执行它的依赖列表,当然也包括computed的副作用函数,这个函数会将dirty重新变成true,并通知执行它的依赖列表,render函数重新执行时重新调用btnText2.value, 导致obj的value属性的get函数重新执行,此时dirty为true,所以重新执行effectFn, 重新返回
${(vm.exposed?.btnText?.value as string) || ''} + '33333'的值,页面上的btnText2的值更新
当watch监听btnText2时
- 第一步与上面相同
- 此时先执行的是watch函数,watch会去调用btnText2.value,与上面第2步类型,get函数执行,返回
{(vm.exposed?.btnText?.value as string) || ''} + '33333',并将dirty置为false
注意
const { btnText2 } = useConfigHook(instance);这段代码执行的时候,组件实例还没创建完,所以此时get函数调用vm.exposed?.btnText?.value时computed的副作用函数并没有被加入到btnText的依赖列表中,vm.exposed?.btnText?.value的值为undfined,btnText2.value返回33333
- 同没有watch时的第2步,组件的render函数执行,调用btnText2.value, 调用get函数,但是,此时dirty为false, 所以直接返回了上一步保留的执行结果33333。
- 当btnText值发生变化时,因为computed的副作用函数并不在它的依赖列表中,也就无法通知到btnText2,所以此时btnText2的值不会发生变化
怎么解决这个问题呢,很简单,将watch包在nextTick中就可以了,此时执行watch,get调用的vm.exposed?.btnText.value就是一个响应式数据了,computed的副作用函数就会被加入到btnText的依赖列表中了。所以根据上面的原理,如果在computed执行后,加一行打印 console.log(btnText2.value) 也会导致btnText2失去对btnText的响应式。