Vue 3 中 defineExpose 的行为【defineExpose暴露ref变量】详解:自动解包、响应性与实际使用

Vue 3 中 defineExpose 的行为详解:自动解包、响应性与实际使用

引言

在做组件封装时,遇到一个现象,子组件里 ref变量,通过defineExpose暴露给父组件,在父组件使用时,不需要添加.value进行解包,可以直接使用,于是就有了疑问:

  1. defineExpose暴露ref变量,父组件使用变量的方式
  2. 在父组件里,暴漏的子组件的变量是否还具有响应式

问了多个AI,回答的一塌糊涂,完全混淆,AI自己也不确定

通过详细的代码测试和分析,揭示 defineExpose 的实际行为机制,特别是关于 ref 的自动解包和响应性问题。

defineExpose 的基本作用

在 Vue 3 的 <script setup> 语法中,组件的内部状态默认是封闭的,外部无法通过模板引用直接访问。defineExpose 允许组件显式地暴露特定的属性,使父组件能够通过模板引用访问这些属性。

自动解包机制

实际行为

经过实际测试(基于 Vue 3.4+),defineExpose 暴露的 ref 在父组件中会被自动解包。这意味着父组件获取到的是解包后的值,而不是 ref 对象本身。

vue 复制代码
<!-- 子组件 Child.vue -->
<script setup>
import { ref, defineExpose } from 'vue';

const count = ref(0);
const formState = ref({ asset_no: '123' });

defineExpose({ count, formState });
</script>

<!-- 父组件 Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';

const childRef = ref();

onMounted(() => {
  // ✅ 正确:直接访问解包后的值
  console.log(childRef.value?.count); // 0
  console.log(childRef.value?.formState?.asset_no); // '123'
  
  // ❌ 错误:尝试访问 .value 会导致 undefined
  console.log(childRef.value?.count.value); // undefined
  console.log(childRef.value?.formState.value?.asset_no); // 报错
});
</script>

响应性保持

尽管自动解包,但响应性依然保持。父组件可以正确响应子组件内部状态的变化。

vue 复制代码
<!-- 子组件 -->
<script setup>
import { ref, defineExpose, onMounted } from 'vue';

const count = ref(0);
defineExpose({ count });

onMounted(() => {
  setInterval(() => {
    count.value++;
  }, 1000);
});
</script>

<!-- 父组件 -->
<script setup>
import { ref, watch } from 'vue';
import Child from './Child.vue';

const childRef = ref();

watch(
  () => childRef.value?.count,
  (newVal) => {
    console.log('count changed:', newVal); // 每秒输出递增的值
  }
);
</script>

小结

在 Vue 3.3+ 中:

defineExpose会自动解包 ref - 父组件得到的是解包后的值

父组件中直接访问属性,无需 .value:

✅ formInstance.value?.formState.asset_no

❌ formInstance.value?.formState.value.asset_no(会报错)

响应性依然保持 - 可以正确响应子组件内部的变化

在模板中 - 同样直接访问,无需额外处理

上面的已经能解决问题了,下面的扩展,仅供参考

模板与脚本的差异

模板中的行为

在模板中,Vue 自动处理了 ref 的解包,因此访问方式与 <script setup> 中一致:

vue 复制代码
<template>
  <!-- 模板中直接访问,无需 .value -->
  <div>Count: {{ childRef?.count }}</div>
  <div>Asset No: {{ childRef?.formState?.asset_no }}</div>
</template>

脚本中的行为

<script setup> 中,同样直接访问解包后的值:

vue 复制代码
<script setup>
// 在脚本中同样直接访问
const currentCount = childRef.value?.count;
const assetNo = childRef.value?.formState?.asset_no;
</script>

深度解包与复杂结构

嵌套对象的处理

当暴露的对象包含嵌套的 ref 时,解包行为是浅层的:

vue 复制代码
<!-- 子组件 -->
<script setup>
import { ref, defineExpose } from 'vue';

const user = ref({
  name: 'John',
  profile: ref({ age: 30 }) // 嵌套的 ref
});

defineExpose({ user });

// 测试访问
onMounted(() => {
  console.log('子组件内 user.value.profile:', user.value.profile);
  // 输出: RefImpl { value: 30 }
});
</script>

<!-- 父组件 -->
<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';

const childRef = ref();

onMounted(() => {
  console.log('父组件获取:');
  console.log(childRef.value?.user?.name); // 'John'
  console.log(childRef.value?.user?.profile); // RefImpl 对象
  console.log(childRef.value?.user?.profile?.value); // 30
  
  // 嵌套的 ref 需要手动 .value
  const age = childRef.value?.user?.profile?.value; // 30
});
</script>

响应式对象的处理

对于 reactive 对象,行为有所不同:

vue 复制代码
<!-- 子组件 -->
<script setup>
import { reactive, defineExpose } from 'vue';

const state = reactive({
  count: 0,
  user: { name: 'John' }
});

defineExpose({ state });

// 修改值
setTimeout(() => {
  state.count = 1;
  state.user.name = 'Jane';
}, 1000);
</script>

<!-- 父组件 -->
<script setup>
import { ref, watch } from 'vue';
import Child from './Child.vue';

const childRef = ref();

// ✅ 可以响应变化
watch(
  () => childRef.value?.state?.count,
  (newVal) => {
    console.log('count changed:', newVal); // 1秒后输出 1
  }
);

watch(
  () => childRef.value?.state?.user?.name,
  (newName) => {
    console.log('name changed:', newName); // 1秒后输出 'Jane'
  }
);
</script>

常见错误模式

错误 1:重复使用 .value

javascript 复制代码
// ❌ 错误
const wrong1 = formInstance.value?.formState.value?.asset_no;
const wrong2 = formInstance.value?.formState.value.asset_no;

// ✅ 正确
const correct = formInstance.value?.formState?.asset_no;

错误 2:假设需要手动解包

javascript 复制代码
// ❌ 不必要的解包
const count = childRef.value?.count?.value;

// ✅ 直接访问
const count = childRef.value?.count;

最佳实践

1. 明确暴露的接口

vue 复制代码
<script setup>
import { ref, computed, defineExpose } from 'vue';

// 只暴露必要的属性和方法
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
const increment = () => { count.value++; };

defineExpose({
  count,
  doubleCount,
  increment
});
</script>

2. 使用 TypeScript 增强类型安全

vue 复制代码
<script setup lang="ts">
import { ref, defineExpose } from 'vue';

interface ExposedProps {
  count: number;
  formState: {
    asset_no: string;
  };
}

const count = ref<number>(0);
const formState = ref({ asset_no: '123' });

defineExpose<ExposedProps>({
  count: count.value, // 注意:这里暴露的是值
  formState: formState.value
});
</script>

3. 提供辅助方法

vue 复制代码
<script setup>
import { ref, defineExpose } from 'vue';

const formState = ref({ asset_no: '123', name: 'Test' });

// 提供获取和设置方法
const getFormData = () => ({ ...formState.value });
const setFormData = (data) => {
  Object.assign(formState.value, data);
};

defineExpose({
  getFormData,
  setFormData
  // 可以选择是否暴露原始数据
  // formState: formState.value
});
</script>

响应性陷阱与解决方案

陷阱:直接修改暴露的状态

vue 复制代码
<!-- 父组件 -->
<script setup>
// 不推荐:直接修改子组件状态
const updateData = () => {
  childRef.value.formState.asset_no = 'new value';
};
</script>

解决方案:通过方法控制状态

vue 复制代码
<!-- 子组件 -->
<script setup>
import { ref, defineExpose } from 'vue';

const formState = ref({ asset_no: '123' });

const updateAssetNo = (value) => {
  formState.value.asset_no = value;
};

defineExpose({
  updateAssetNo,
  getFormData: () => ({ ...formState.value })
});
</script>

<!-- 父组件 -->
<script setup>
// 通过方法安全地更新
const updateData = () => {
  childRef.value.updateAssetNo('new value');
};
</script>

与模板引用的配合使用

多个模板引用

vue 复制代码
<template>
  <ChildComponent ref="child1" />
  <ChildComponent ref="child2" />
  <ChildComponent ref="child3" />
</template>

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

const child1 = ref();
const child2 = ref();
const child3 = ref();

onMounted(() => {
  // 分别访问不同实例
  console.log('Child 1:', child1.value?.count);
  console.log('Child 2:', child2.value?.count);
  console.log('Child 3:', child3.value?.count);
});
</script>

动态组件引用

vue 复制代码
<template>
  <component
    :is="currentComponent"
    ref="dynamicComponent"
  />
</template>

<script setup>
import { ref, watch } from 'vue';
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';

const currentComponent = ref(ComponentA);
const dynamicComponent = ref();

// 监听组件变化
watch(currentComponent, () => {
  // 组件切换时重新访问
  console.log('Dynamic component ref:', dynamicComponent.value);
});
</script>

与其它暴露方式的对比

defineExpose vs. 事件

特性 defineExpose 事件 (emit)
数据流 父→子(读取) 子→父(通知)
响应性 双向 单向
耦合度 较高 较低
类型安全 较好 较好
适用场景 父组件需要直接访问子组件状态 子组件需要通知父组件

defineExpose vs. provide/inject

特性 defineExpose provide/inject
作用范围 直接父子 跨层级
灵活性 较低 较高
维护性 较简单 较复杂
适用场景 紧密耦合的组件 松散耦合的组件树

实际应用示例

表单组件示例

vue 复制代码
<!-- 子组件 ProForm.vue -->
<script setup>
import { ref, defineExpose } from 'vue';

const formState = ref({
  asset_no: '',
  name: '',
  date: null
});

// 验证方法
const validate = () => {
  if (!formState.value.asset_no) {
    return { valid: false, message: '资产编号不能为空' };
  }
  return { valid: true, message: '' };
};

// 重置方法
const reset = () => {
  formState.value = { asset_no: '', name: '', date: null };
};

defineExpose({
  formState: formState.value, // 自动解包
  validate,
  reset
});
</script>

父组件使用

vue 复制代码
<template>
  <ProForm ref="proForm" />
  <button @click="handleSubmit">提交</button>
  <button @click="handleReset">重置</button>
</template>

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

const proForm = ref();

const handleSubmit = () => {
  const result = proForm.value.validate();
  if (result.valid) {
    console.log('表单数据:', proForm.value.formState);
    // 提交逻辑...
  } else {
    alert(result.message);
  }
};

const handleReset = () => {
  proForm.value.reset();
};
</script>

总结

Vue 3 中 defineExpose 的行为可以总结如下:

  1. 自动解包:暴露的 ref 在父组件中会被自动解包,父组件直接获取到的是解包后的值。
  2. 响应性保持:尽管自动解包,但响应性依然保持,父组件可以响应子组件内部状态的变化。
  3. 访问方式
    • 模板中:直接访问,如 {``{ childRef?.formState?.asset_no }}
    • 脚本中:直接访问,如 childRef.value?.formState?.asset_no
  4. 版本一致性:从 Vue 3.3+ 开始,这一行为已经稳定,但在使用时仍需注意版本差异。

理解 defineExpose 的正确行为,可以帮助开发者更高效地编写 Vue 3 组件,避免常见的访问错误和响应性问题。在实际开发中,建议根据具体场景选择合适的组件通信方式,defineExpose 适用于需要直接访问子组件状态的场景,但应注意控制暴露的范围,保持组件的封装性。

附录 vue官方文档

defineExpose()

使用 <script setup> 的组件是默认关闭的------即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。

可以通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性:

.vue 复制代码
<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>

当父组件通过模板引用的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref 会和在普通实例中一样被自动解包)

相关推荐
奔跑的呱呱牛2 小时前
generate-route-vue基于文件系统的 Vue Router 动态路由生成工具
前端·javascript·vue.js
sp42a2 小时前
在 NativeScript-Vue 中实现流畅的共享元素转场动画
vue.js·nativescript·app 开发
柳杉2 小时前
从动漫水面到赛博飞船:这位开发者的Three.js作品太惊艳了
前端·javascript·数据可视化
Greg_Zhong2 小时前
前端基础知识实践总结,每日更新一点...
前端·前端基础·每日学习归类
We་ct3 小时前
LeetCode 148. 排序链表:归并排序详解
前端·数据结构·算法·leetcode·链表·typescript·排序算法
TON_G-T3 小时前
day.js和 Moment.js
开发语言·javascript·ecmascript
IT_陈寒3 小时前
JavaScript开发者必看:5个让你的代码性能翻倍的隐藏技巧
前端·人工智能·后端
Irene19913 小时前
JavaScript 中 this 指向总结和箭头函数的作用域说明(附:call / apply / bind 对比总结)
javascript·this·箭头函数