Vue父子组件生命周期,解决实际遇到的问题

202 阅读5分钟

这篇笔记主要介绍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: 父组件销毁后调用
实际遇到的问题

项目需求,需要给表格展开行按钮 > 加提示,来告诉用户这是可以点击展开或隐藏的

image.png

表格渲染代码(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

image.png

image.png

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>

image.png 但是呢,这样就只占据了一列的宽度, 展开的时候,不像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, 加的提示 Vite-Vue-TS.gif

可以看到,在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>

image.png

没有延时,但是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的

image.png

在解决方案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()
})

总结下这篇笔记

  1. vue父子组件生命周期渲染顺序,当子组件渲染逻辑比较复杂的时候,在父组件onMounted里是拿不到子组件dom的

  2. element-plus el-table中的type="expand"的渲染逻辑,源代码里是用 return [h(ArrowRight)]来渲染

  3. css的title有延时,伪元素没有

  4. web里APIMutationObserver的使用