在Vue2中,子组件使用的是选项式 API ,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
而在Vue3中,子组件使用<script setup>的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露。
一、创建子组件(表格)
在项目目录src/components目录中创建TableNormal.vue组件,并且通过defineExpose对外暴露父组件可以调用的函数。代码如下:
TypeScript
<template>
<div class="table-wrap">
<!-- table-wrap -->
<div class="table-wrap">
<el-table ref="table" border :data="tableData">
<el-table-column type="selection" width="55" fixed></el-table-column>
<el-table-column type="index" label="序号" width="50px" :resizable="false"></el-table-column>
<template v-for="(item, index) in tableColumns">
<el-table-column :prop="item.prop" :label="item.label" :key="'' + index"></el-table-column>
</template.
</el-table>
</div>
<!-- /table-wrap -->
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
defineOptions({
name: "Table-Normal"
})
type TableColumnsType = {
label: string,
prop: string
}
const tableColumns = ref<Array<TableColumnsType>>([]) // 列数据
const tableData = ref<Array<any>>([]) // 行数据
const page = ref(1)
const pageSize = ref(5)
const pageTotal = ref(0)
const onLoadTableColumns = () => {
// 通过Axios API接口获取tableColumns 表格列数据
}
const onLoadTableData = () => {
// 通过Axios API接口获取tableData 表格行数据
}
onMounted(() => {
onLoadTableColumns()
onLoadTableData()
})
// 对外暴露onLoadTableData函数
defineExpose({onLoadTableData})
</script>
二、创建父组件(Department)
在项目src/views目录中,创建Department.vue文件。示例代码如下:
TypeScript
<template>
<div>
<table-normal></table-normal>
<button @click="() => valueChange()">重新加载</button>
</div>
</template>
<script lang="ts" setup>
// 重新加载
const valueChange = () => {
}
</script>
2.1 ref获取组件实例
通过ref获取组件实例后,则可以通过实例对象的变量,去调用子组件中对外暴露的变量或函数了。
TypeScript
<template>
<div>
<table-normal ref="tableNormalRef"></table-normal>
<button @click="() => valueChange()">重新加载</button>
</div>
</template>
<script lang="ts" setup>
import { ref} from "vue";
const tableNormalRef = ref(null)
// 重新加载
const valueChange = () => {
if(tableNormalRef.value && tableNormalRef.value.onLoadTableData)
tableNormalRef.value.onLoadTableData()
}
</script>
三、解决问题 - 类型断言
当父组件Department.vue中,通过ref获取自定义表格组件实例,并且在子组件中已通过defineExpose对外暴露了onLoadTableData函数,所以我们可以通过实例变量直接调用onLoadTableData()函数。
TypeScript
if(tableNormalRef.value && tableNormalRef.value.onLoadTableData)
tableNormalRef.value.onLoadTableData()
3.1 "never"断言
但是调用后,会提示如下截图上的断言提示【错误提示:类型"never"上不存在属性"onLoadTableData"。】。
在TypeScript中,当一个变量被推断为never类型时,这意味着变量从逻辑上不应该存在任何值。所以需要在获取实例时,告诉ref该实例的类型。
由于ref获取自定义组件为DOM元素,所以将其类型指定为HTMLElement,代码如下:
TypeScript
<template>
<div>
<table-normal ref="tableNormalRef"></table-normal>
<button @click="() => valueChange()">重新加载</button>
</div>
</template>
<script lang="ts" setup>
import { ref} from "vue";
const tableNormalRef = ref<HTMLElement | null>(null)
// 重新加载
const valueChange = () => {
if(tableNormalRef.value && tableNormalRef.value.onLoadTableData)
tableNormalRef.value.onLoadTableData()
}
</script>
3.2 onLoadTableData不存在
添加HTMLElement后,never类型断言问题是消失了,但随之而来又有新问题【错误:属性"onLoadTableData"在类型"HTMLElement"上不存在。你是否指的是"onloadeddata"?】
这是由于onLoadTableData为自定义函数,在HTMLElement类型中并不存上在,所以我们需要重新定义一个新接口类型,并继承HTMLElement类型即可。代码如下:
TypeScript
<template>
<div>
<table-normal ref="tableNormalRef"></table-normal>
<button @click="() => valueChange()">重新加载</button>
</div>
</template>
<script lang="ts" setup>
import { ref} from "vue";
// 定义新接口类型
interface LoadTableDataElement extends HTMLElement {
onLoadTableData: () => void
}
const tableNormalRef = ref<LoadTableDataElement | null>(null)
// 重新加载
const valueChange = () => {
if(tableNormalRef.value && tableNormalRef.value.onLoadTableData)
tableNormalRef.value.onLoadTableData()
}
</script>
此时VSCode中波浪线不存在了。
3.3 null类型
在通过ref获取组件实例中,有可能出现组件实例未获取到的情况(即为null类型),所以在valueChange函数中调用前,需判断其是否存在,再调用。否则会报错【错误提示:"tableNormalRef.value"可能为 "null"。】
四、useTemplateRef
模板引用也可以被用在一个子组件上,这种情况下引用中获得的值是组件实例。
4.1 新特性获取组件实例
在前面讲了,使用ref方式获取组件实例,但在3.5版本后,可以通过新特性useTemplateRef来获取模板中的组件实例。我们将上面示例代码稍作修改,代码如下:
TypeScript
<template>
<div>
<table-normal ref="tableNormalRef"></table-normal>
<button @click="() => valueChange()">重新加载</button>
</div>
</template>
<script lang="ts" setup>
import { ref} from "vue";
// 定义新接口类型
interface LoadTableDataElement extends HTMLElement {
onLoadTableData: () => void
}
const tableNormalRef = useTemplateRef('tableNormalRef')
// 重新加载
const valueChange = () => {
if(tableNormalRef.value && tableNormalRef.value.onLoadTableData)
tableNormalRef.value.onLoadTableData()
}
</script>
4.2 类型断言处理
使用useTemplateRef时,也会出现类型断言问题,此时上述代码会提示如下错误【类型"{}"上不存在属性"onLoadTableData"。】。
此时,我们3.2中定义的新接口类型,指定给useTemplateRef即可,代码如下:
TypeScript
<template>
<div>
<table-normal ref="tableNormalRef"></table-normal>
<button @click="() => valueChange()">重新加载</button>
</div>
</template>
<script lang="ts" setup>
import { ref} from "vue";
// 定义新接口类型
interface LoadTableDataElement extends HTMLElement {
onLoadTableData: () => void
}
const tableNormalRef = useTemplateRef<LoadTableDataElement>('tableNormalRef')
// 重新加载
const valueChange = () => {
if(tableNormalRef.value && tableNormalRef.value.onLoadTableData)
tableNormalRef.value.onLoadTableData()
}
</script>
在开发中,当遇到各种问题时,认真阅读错误提示,并根据问题逐一排查,相信大多情况能很快定位到问题,并得到解决的。