返回

Vue 3 子组件 ref 中 prop 未定义问题及解决方案

vue.js

子组件 ref 中 Prop 未定义

在 Vue 3 的组合式 API 中,开发者常常使用 ref 来创建响应式数据。当涉及到子组件接收父组件传递的 prop 时,可能会遇到一个问题:在子组件内部,ref 对象初始化的值引用 prop,结果 prop 的值却变成了 undefined。本文将深入分析这个问题的原因,并提供多种解决方案,以避免未来项目遇到此类问题。

问题分析

子组件接收到父组件传递的 prop 后,尝试在 ref 初始值中使用此 prop,这是引发问题的根源。ref 的初始化过程是同步的,组件实例及其 props 在该时间点可能尚未完全设置或解析完毕, 这通常发生在组件的生命周期钩子开始之前。在此时使用 prop 进行初始值设置时,props 还没有值或者值可能并不是最新的。简单说,ref 在依赖于 props 前就初始化了。

以下述代码为例说明:

<script setup>
import { ref } from 'vue';

const { companyId } = defineProps({
    companyId: {
        type: Number,
        required: true 
    }
});

var form = ref({
    name: null,
    companyId: companyId // 这里 `companyId` 在 `ref` 初始化时可能为 `undefined`
});
</script>

尽管在 setup 钩子中已经声明了 companyId 这个 prop,并且它确实存在于组件实例上,但在上述代码 form 这个 ref 初始化时,这个 prop 值有可能还不是期望的值。当组件渲染时,组件的 render 函数和 setup 会并发执行,这意味着无法确保 defineProps 返回的值何时生效,或者何时同步地绑定到组件上。所以当初始化 ref 时 props 尚未被初始化或正确赋值就导致 form.value.companyIdundefined

解决方案

方案一:使用 onMounted 生命周期钩子

为了确保组件已经正确接收到 prop 并已经完成初始赋值,可以将 ref 的初始化移到 onMounted 钩子中,这个钩子会在组件挂载到 DOM 后执行,此时所有 props 的值都已完成设置。

代码示例:

<script setup>
import { ref, onMounted } from 'vue';

const { companyId } = defineProps({
    companyId: {
        type: Number,
        required: true
    }
});

const form = ref({
  name: null,
  companyId: undefined // 先定义,待 mounted 钩子函数中赋值。
});

onMounted(() => {
    form.value.companyId = companyId; // 在 mounted 后, prop 的值会是有效的
});


async function createNewTerms() {
   console.log(form.value.companyId);
}
</script>

操作步骤:

  1. ref 的初始赋值设为undefined,以便观察更新情况。
  2. 导入onMounted钩子函数。
  3. 将使用prop值的ref数据更新放置到onMounted函数内执行,这样保证此时prop已经绑定到组件实例。
  4. createNewTerms()中验证 prop 的值。

这种方法避免了 ref 在 prop 尚未就绪时就被初始化的错误,保证数据的准确性。

方案二:使用 computed 属性

使用计算属性是一种更优雅的方式来解决这个问题。计算属性会惰性地进行计算,并且只有当它的依赖项发生变化时才会重新计算,可以完美地解决初始化时间差的问题。

代码示例:

<script setup>
import { ref, computed } from 'vue';

const { companyId } = defineProps({
    companyId: {
        type: Number,
        required: true
    }
});

const form = ref({
    name: null
});

const reactiveForm = computed(() => ({
    ...form.value,
    companyId
}));

async function createNewTerms() {
   console.log(reactiveForm.value.companyId);
}
</script>

操作步骤:

  1. 导入computed函数。
  2. 创建一个计算属性,它依赖于原始的form,同时直接引用props值。
  3. createNewTerms()函数中,使用这个计算属性的 .value
    此方法非常优雅,因为它使用了 vue 的响应式系统,并且减少了在不同生命周期之间移动状态。 reactiveForm 将在 companyId 变化时自动更新。

方案三: 使用 watchEffect 监听 props 变化

可以尝试使用 watchEffect 来创建一个响应式的 form 对象。当 props 中的 companyId 值变化时, form 的值也会随之变化。

代码示例:

<script setup>
import { ref, watchEffect } from 'vue';

const { companyId } = defineProps({
  companyId: {
      type: Number,
      required: true,
  }
});


const form = ref({ name: null });

watchEffect(() => {
    form.value = {
        name: form.value.name,
        companyId: companyId,
    };
});


async function createNewTerms() {
   console.log(form.value.companyId);
}
</script>

操作步骤:

  1. 导入watchEffect 函数
  2. 使用 watchEffect 监听依赖的prop
  3. 在回调中赋值form
  4. 验证更新的值
    这种方案虽然能够实现,但在本次场景下并非最佳选择,原因是它创建了一个非必要的数据监听。它更适合用于监视prop值并在改变时执行特定的副作用,这里并不需要副作用,只需要响应式赋值即可,相较而言 computed 属性更加轻量,推荐优先考虑。

安全建议

  • 明确数据类型:defineProps 中,明确 prop 的数据类型可以防止类型不匹配的错误。使用 required: true 来指示必须提供的 props, 提高代码的健壮性。
  • 默认值: 可以使用 default 为 prop 提供一个默认值,在父组件未传递该 prop 时,可以保证代码运行不会报错。

总结

当使用 Vue 3 的组合式 API 和 ref 时,注意在 prop 被正确初始化之后再使用,这避免了因为初始化时机不确定导致的 undefined 问题。onMountedcomputed 属性和 watchEffect 这些方案都可以用来保证prop可用。 使用这些技巧,可以让开发过程更加流畅,并避免了数据错误的风险。 选择最适合场景的方案并配合一些好的代码实践能使代码更加健壮可靠。