这篇笔记主要介绍Vue父子组件生命周期,并解决实际遇到的项目问题。先看下Vue父子组件渲染顺序和销毁顺序。
这个时间图使用mermaid-js (如果section 名称相同,加了空格来偷偷的区分)
timeline
title Vue父子组件渲染顺序
section 父组件 渲染
beforeCreate : 父组件实例 被初始化 之前调用
created : 父组件实例被创建 之后调用,但还未 挂载到DOM
beforeMount : 在模板编译成虚拟 DOM后,在挂载 之前调用
section 子组件 渲染
beforeCreate : 子组件实例 被初始化 之前调用
created : 子组件实例被创建 之后调用,但还未 挂载到DOM
beforeMount : 子组件模板编译成 虚拟 DOM后, 在挂载之前调用
mounted: 子组件挂载到 DOM 后调用
section 父组件 渲染
mounted : 父组件挂载到 DOM 后调用
timeline
title Vue父子组件销毁顺序
section 父组件 销毁
beforeDestroy: 父组件销毁之前调用
section 子组件 销毁
beforeDestroy: 子组件销毁之前调用
destroyed: 子组件销毁后调用
section 父组件 销毁
destroyed: 父组件销毁后调用
实际遇到的问题
项目需求,需要给表格展开行按钮 > 加提示,来告诉用户这是可以点击展开或隐藏的
表格渲染代码(element-plus)
<template>
<el-table :data="tableData" :border="parentBorder" style="width: 100%">
<el-table-column type="expand">
<template #default="props">
<div m="4">
<p m="t-0 b-2">State: {{ props.row.state }}</p>
<p m="t-0 b-2">City: {{ props.row.city }}</p>
<p m="t-0 b-2">Address: {{ props.row.address }}</p>
<p m="t-0 b-2">Zip: {{ props.row.zip }}</p>
<h3>Family</h3>
<el-table :data="props.row.family" :border="childBorder">
<el-table-column label="Name" prop="name" />
<el-table-column label="State" prop="state" />
<el-table-column label="City" prop="city" />
<el-table-column label="Address" prop="address" />
<el-table-column label="Zip" prop="zip" />
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="Date" prop="date" />
<el-table-column label="Name" prop="name" />
</el-table>
</template>
解决方案1 加个el-tooltip (有难度 破坏原有布局不可行)
当时就觉得很简单,给那一列加个el-tooltip就可以了,看了下代码那一列是el-table封装的,当使用type=expand,
el-table使用h函数渲染了ArrowRight右箭头icon, 封装好了,咱们没地方加el-tooltip, 只能不使用type="expand", 自己加icon实现,
<template>
<el-table :data="tableData" :border="parentBorder" style="width: 100%"
ref="tableRef"
row-key="date"
>
<el-table-column>
<template #default="props">
<el-tooltip content="点击展开/收起" placement="top">
<el-button
@click="toggleRowExpansion(props.row)"
primary
type="text"
>{{ props.row.expanded ? '^' : '>' }}</el-button>
</el-tooltip>
<div m="4" v-if="props.row.expanded">
<p m="t-0 b-2">State: {{ props.row.state }}</p>
<p m="t-0 b-2">City: {{ props.row.city }}</p>
<p m="t-0 b-2">Address: {{ props.row.address }}</p>
<p m="t-0 b-2">Zip: {{ props.row.zip }}</p>
<h3>Family</h3>
<el-table :data="props.row.family" :border="childBorder">
<el-table-column label="Name" prop="name" />
<el-table-column label="State" prop="state" />
<el-table-column label="City" prop="city" />
<el-table-column label="Address" prop="address" />
<el-table-column label="Zip" prop="zip" />
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="Date" prop="date" />
<el-table-column label="Name" prop="name" />
</el-table>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const parentBorder = ref(false)
const childBorder = ref(false)
// 表格引用
const tableRef = ref();
// 切换行展开状态
const toggleRowExpansion = (row) => {
row.expanded = !row.expanded
};
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
family: [
{
name: 'Jerry',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
{
name: 'Spike',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
{
name: 'Tyke',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
],
},
]
</script>
但是呢,这样就只占据了一列的宽度, 展开的时候,不像
type="expand", 单独一行
留了一个问题:如果不用type="expand",自己实现展开,怎么让它单独一行
解决方案2 加上title属性 (有延迟 体验不好)
看下代码
<template>
<div style="padding: 30px">
子组件1: 表格
<el-table
:data="tableData"
:border="parentBorder"
style="width: 100%"
@expand-change="expandChange()"
>
<el-table-column type="expand">
<template #default="props">
<div m="4">
<p m="t-0 b-2">State: {{ props.row.state }}</p>
<p m="t-0 b-2">City: {{ props.row.city }}</p>
<p m="t-0 b-2">Address: {{ props.row.address }}</p>
<p m="t-0 b-2">Zip: {{ props.row.zip }}</p>
<h3>Family</h3>
<el-table :data="props.row.family" :border="childBorder">
<el-table-column label="Name" prop="name" />
<el-table-column label="State" prop="state" />
<el-table-column label="City" prop="city" />
<el-table-column label="Address" prop="address" />
<el-table-column label="Zip" prop="zip" />
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="Date" prop="date" />
<el-table-column label="Name" prop="name" />
</el-table>
<!-- <hello-world msg=" " /> -->
</div>
</template>
<script lang="ts" setup>
import 'tippy.js/dist/tippy.css'
import { ref, nextTick, onMounted } from 'vue'
import HelloWorld from '../components/HelloWorld.vue'
const parentBorder = ref(false)
const childBorder = ref(false)
const expandChange = () => {
}
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
family: [
{
name: 'Jerry',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
{
name: 'Spike',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
{
name: 'Tyke',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
],
},
{
date: '2016-05-02',
name: 'Tom',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
family: [
{
name: 'Jerry',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
{
name: 'Spike',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
{
name: 'Tyke',
state: 'California',
city: 'San Francisco',
address: '3650 21st St, San Francisco',
zip: 'CA 94114',
},
],
},
]
const showTitle = event => {
event.target.setAttribute('title', '点击展开/隐藏')
}
const hideTitle = event => {
event.target.removeAttribute('title')
}
onMounted(() => {
nextTick(() => {
const expandIcons = document.querySelectorAll('.el-table__expand-icon')
console.log('nextTick', expandIcons)
expandIcons.forEach((icon, iconIndex) => {
if (iconIndex === 0) {
icon.addEventListener('mouseover', showTitle)
icon.addEventListener('mouseleave', hideTitle)
}
})
})
setTimeout(() => {
const expandIcons = document.querySelectorAll('.el-table__expand-icon')
console.log('setTimeout', expandIcons)
expandIcons.forEach((icon, iconIndex) => {
if (iconIndex === 1) {
icon.addEventListener('mouseover', showTitle)
icon.addEventListener('mouseleave', hideTitle)
}
})
}, 1000)
})
</script>
title属性有延时,效果不好, 动图第一条数据使用的是nextTick获取不到dom, 所以没有提示,第二条数据是nextTick获取的dom, 加的提示
可以看到,在onMounted里面,并不可以获取到.el-table__expand-icon, 只有加上延时才可以,使用nextTick都获取不到,前面我们说父组件onMounted挂载的时候,子组件已经挂载了,
但是如果子组件很复杂的话,父组件挂载时候,子组件里面的不一定都渲染完了
解决方案3 利用css伪元素,没有延时,样式很难调,放弃了
看下代码,加上伪元素样式
<style>
.el-table__expand-icon {
position: fixed; /* Establishes a positioning context for the tooltip */
display: inline-block;
cursor: pointer;
}
/* Tooltip styling */
.el-table__expand-icon::after {
content: '点击展开/隐藏'; /* attr(data-tooltip); Tooltip content */
position: absolute; /* Fixed positioning */
left: 50%; /* Center horizontally */
top: 100%; /* Position below the tooltip element */
transform: translateX(-20%) translateY(-200%); /* Center horizontally and offset vertically */
background-color: rgba(0, 0, 0, 0.7); /* Tooltip background */
color: white;
padding: 8px;
border-radius: 4px;
white-space: nowrap;
visibility: hidden; /* Start hidden */
opacity: 0; /* Fully transparent */
transition:
opacity 0.2s ease-in-out,
visibility 0.2s ease-in-out;
z-index: 10;
font-size: 12px;
pointer-events: none; /* Ensures the tooltip doesn't block interaction */
visibility: hidden; /* Make visible */
opacity: 0; /* Make opaque */
}
/* Show the tooltip on hover */
.el-table__expand-icon:hover::after {
visibility: visible; /* Make visible */
opacity: 1; /* Make opaque */
}
</style>

没有延时,但是icon样式没有居中对齐,要调,点击后,鼠标移上去有影子样式,影响体验。那就尝试第4种方案
解决方案4 利用tippy.js
看下代码
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue'
import tippy from 'tippy.js'
import 'tippy.js/dist/tippy.css'
import { ref, nextTick } from 'vue'
import HelloWorld from '../components/HelloWorld.vue'
const parentBorder = ref(false)
const childBorder = ref(false)
const tableData = [] // 数据省略 不占篇幅
const observer = new MutationObserver(() => {
const expandIcons = document.querySelectorAll('.el-table__expand-icon')
if (expandIcons.length) {
tippy('.el-table__expand-icon', {
content: '点击展开/隐藏',
})
}
})
onMounted(() => {
// Observe changes in the table element
const tableElement = document.querySelector('.el-table')
if (tableElement) {
observer.observe(tableElement, { childList: true, subtree: true })
}
tippy('.button', {
content: '我是简单子组件',
})
})
onUnmounted(() => {
// 断开 MutationObserver 的观察
observer.disconnect()
})
搞了半小时(GIF只要一上传提示保存中,没法上传GIF了,可是延时,静态图片体现不出来,只好用视频了)

没有延时,样式也对,完美。
看图有个子组件2, 那个是测试,如果是简单子组件,在父组件里onMounted是可以获取dom的
在解决方案2中,我们说了setTimeout来获取子组件的dom,可是不想用定时器,看到上面代码里我们用了web一个API, MutationObserver来监听 DOM 变动,
定义回调函数、创建观察实例
const observer = new MutationObserver(() => {
const expandIcons = document.querySelectorAll('.el-table__expand-icon')
if (expandIcons.length) {
tippy('.el-table__expand-icon', {
content: '点击展开/隐藏',
})
}
})
选择目标节点、设置观察选项、开始观察
const tableElement = document.querySelector('.el-table')
if (tableElement) {
observer.observe(tableElement, { childList: true, subtree: true })
}
停止观察
onUnmounted(() => {
// 断开 MutationObserver 的观察
observer.disconnect()
})
总结下这篇笔记
-
vue父子组件生命周期渲染顺序,当子组件渲染逻辑比较复杂的时候,在父组件onMounted里是拿不到子组件dom的
-
element-plus el-table中的type="expand"的渲染逻辑,源代码里是用
return [h(ArrowRight)]来渲染 -
css的title有延时,伪元素没有
-
web里APIMutationObserver的使用