不使用ref()时如何让孙组件渲染与父组件保持同步?
我的孙组件依赖父组件onUpdated钩子中计算的data2,但孙组件的渲染总是比父组件慢一步。以下是简化后的代码:
App.vue
<script setup lang="ts"> import { ref } from 'vue' import Child from './Child.vue' const msg = ref(1) </script> <template> <button @click="++msg">increase</button> <h1>Me: {{ msg }}</h1> <Child :data = msg></Child> </template>
Child.vue
<script setup lang="ts"> import { onUpdated } from 'vue' import GrandChild from './grandchild.vue' const props = defineProps<{ data : number }>() let data2; function complexFunction(data) { setTimeout(()=>data2 = props.data,200); } onUpdated(() => { complexFunction(props.data); }) </script> <template> <h2>Child: {{ data }}</h2> <GrandChild :data = data2 ></GrandChild> </template>
GrandChild.vue
<script setup lang="ts"> const props = defineProps<{ data : number }>() </script> <template> <h3>Grandchild: {{ data }}</h3> </template>
实际场景说明
实际中complexFunction会用Leaflet.js这类库计算data2(是复杂对象而非简单数字),示例里用setTimeout模拟异步计算过程。
如果给data2用ref(),简化示例里孙组件能正常更新,但实际场景中complexFunction会导致onUpdated递归调用,引发栈溢出,所以想找不用ref()的同步更新方案。
现在有个部分解决方案,但只能每两次点击后正确更新孙组件。我需要实现的效果是:
Me: 9 Child: 9 Grandchild: 9
而不是:
Me: 9 Child: 9 Grandchild: 8
方案1:手动触发组件更新(不用ref)
因为data2是非响应式变量,修改后需要手动通知Vue更新视图。可以在计算完成后,借助nextTick和组件实例触发更新:
修改Child.vue:
<script setup lang="ts"> import { onUpdated, nextTick, getCurrentInstance } from 'vue' import GrandChild from './grandchild.vue' const props = defineProps<{ data : number }>() let data2; const instance = getCurrentInstance() function complexFunction(data) { setTimeout(()=>{ data2 = props.data; // 手动触发组件重新渲染 nextTick(() => { instance?.update() }) },200); } onUpdated(() => { complexFunction(props.data); }) </script> <template> <h2>Child: {{ data }}</h2> <GrandChild :data = data2 ></GrandChild> </template>
原理:修改非响应式的data2后,手动调用组件的update方法,强制Vue重新渲染,孙组件就能拿到最新的data2值。
方案2:用watch替代onUpdated避免递归(兼容ref)
之前用ref()导致递归,是因为onUpdated会在组件每次更新(包括孙组件更新触发的父组件更新)时执行。换成watch精准监听props.data的变化,就能避免递归:
修改Child.vue:
<script setup lang="ts"> import { ref, watch } from 'vue' import GrandChild from './grandchild.vue' const props = defineProps<{ data : number }>() const data2 = ref<number | undefined>() function complexFunction(data) { setTimeout(()=>{ data2.value = props.data; },200); } // 仅在props.data变化时执行计算 watch(() => props.data, (newVal) => { complexFunction(newVal) }) </script> <template> <h2>Child: {{ data }}</h2> <GrandChild :data = data2 ></GrandChild> </template>
原理:watch只会在props.data的实际值改变时触发,不会因为组件的其他更新重复执行,从根源避免了递归调用的问题。
方案3:用markRaw处理第三方库实例(针对复杂对象)
如果data2是Leaflet实例这类不需要响应式追踪的复杂对象,用markRaw包装后,既保留响应式更新能力,又不会触发不必要的响应式监听,避免递归:
<script setup lang="ts"> import { ref, watch, markRaw } from 'vue' import GrandChild from './grandchild.vue' const props = defineProps<{ data : number }>() const data2 = ref<any>() function complexFunction(data) { setTimeout(()=>{ // 替换为实际的第三方库实例创建逻辑 const libInstance = {}; data2.value = markRaw(libInstance); },200); } watch(() => props.data, (newVal) => { complexFunction(newVal) }) </script>
内容的提问来源于stack exchange,提问作者quilkin

