【一文读懂-Vue】对生命周期、组件通信、mixins混入的理解

👉 个人博客主页 👈
📝 一个努力学习的程序猿


更多文章/专栏推荐:
HTML和CSS
JavaScript
Vue
Vue3
React
TypeScript
个人经历+面经+学习路线【内含免费下载初级前端面试题】
前端面试分享
前端学习+方案分享(VitePress、html2canvas+jspdf、vuedraggable、videojs)
前端踩坑日记(ElementUI)
【一文读懂】对闭包的理解及其衍生问题
【一文读懂】对原型链、事件循环、异步操作(Promise、async/await)、递归栈溢出的理解及其衍生问题
【一文读懂】对 Vue 原理的理解-简易版(如何处理模版和指令、虚拟DOM、双向数据绑定+监听、怎么触发生命周期、nextTick简述)


前言

如果对 Vue3 基础用法不太了解,可以参考之前的文章:
https://blog.youkuaiyun.com/qq_45613931/article/details/109471110


1、Vue 生命周期

1.1 基础概念 Vue2

Vue2 实例从创建到销毁的过程中,经历的生命周期钩子函数,主要分为以下几个阶段:

(1)创建阶段:实例初始化与数据准备:beforeCreate,created
(2)挂载阶段:DOM 渲染与挂载:beforeMount,mounted
(3)更新阶段:数据变化与 DOM 更新:beforeUpdate,updated
(4)销毁阶段:组件卸载与资源释放:beforeDestroy,destroyed

为更好的理解,感兴趣的朋友可以查看上一篇文章。通过它,你将能更理解每个生命周期为什么只能做特定的事情:https://blog.youkuaiyun.com/qq_45613931/article/details/146509791(对 Vue 原理的理解:介绍了虚拟 DOM、各生命周期触发的时机、简述了nextTick的原理及作用)

示例 Vue2:

<template>
  <div>
    <h1>{{ message }}</h1>
	<button @click="updateMessage">更新消息</button>
    <child-component ref="child"></child-component>
  </div>
</template>
 
<script>
import ChildComponent from './ChildComponent.vue'
export default {
  name: 'LifecycleExample',
  components: {
    ChildComponent
  },
  data() {
    return {
      message: 'Hello Vue!',
      timer: null,
      data: null
    }
  },
  
  // 1. 创建阶段
  beforeCreate() {
    console.log('beforeCreate: 实例初始化之后,数据观测和事件配置之前被调用')
    console.log('此时无法访问 data 和 methods:', this.message) // undefined
  },
  
  async created() {
    console.log('created: 实例创建完成后被调用。此时未挂载到 DOM,无法对 DOM 做操作')
    console.log('可以访问 data 和 methods:', this.message) // "Hello Vue!"
	// 适合进行异步 API 接口调用
    try {
      this.data = await this.xxx()
    } catch (error) {
      console.error(error)
	}
  },
  
  // 2. 挂载阶段
  beforeMount() {
    console.log('beforeMount: 模版编译完成,虚拟 DOM 挂载开始之前被调用')
    console.log('首次渲染之前最后一次修改数据的机会')
  },
  
  mounted() {
    console.log('mounted: DOM 挂载完成后被调用,此时可以访问 DOM 元素')
    // 适合执行需要 DOM 的操作
	console.log(this.$el.querySelector('h1').textContent)
    this.$nextTick(() => {
      console.log(this.$refs.child.$el)
    })
    
    // 设置定时器
    this.timer = setInterval(() => {
      xxx
	}, 1000)
  },
  
  // 3. 更新阶段
  beforeUpdate() {
	console.log('beforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染之前')
    console.log('更新之前最后一次访问现有 DOM 的机会')
  },
  
  updated() {
    console.log('updated: 虚拟 DOM 重新渲染后调用,此时 DOM 已经更新完成')
	// 避免在此期间更改数据,否则可能导致无限循环
    this.$nextTick(() => {
      // 所有更新完成后的操作
	})
  },
  
  // 4. 销毁阶段
  beforeDestroy() {
    console.log('beforeDestroy: 实例销毁之前调用,此时依然可以访问实例')
	// 适合清理定时器、事件监听器、取消订阅等
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
	}
  },
  
  destroyed() {
    console.log('destroyed: 实例销毁后调用')
    console.log('所有的事件监听器已被移除,所有的子实例也已经被销毁')
  },
  
  methods: {
    updateMessage() {
      this.message = '消息已更新:' + new Date().toLocaleString()
	}
  }
}
</script>

在使用过程中,用到最多的就是 createdmountedbeforeDestroy。注意事项:

(1)生命周期钩子必须使用普通函数,不能使用箭头函数。否则内部使用 this 会指向 window 或 undefined,而不是 Vue 实例;

(2)父子组件的生命周期顺序(同步引入):

  • 创建和挂载:
    父组件 beforeCreatecreatedbeforeMount => 所有子组件 beforeCreatecreatedbeforeMount => 所有子组件 mounted => 父组件 mounted
    总结:父组件先创建,然后子组件创建子组件完全挂载后,父组件挂载
  • 更新过程:
    父组件 beforeUpdate =>所有子组件 beforeUpdate =>所有子组件 updated => 父组件updated
    总结:父组件先触发更新钩子子组件更新完成后,父组件才完成更新
  • 销毁过程:
    父组件 beforeDestroy =>所有子组件 beforeDestroy =>所有子组件 destroyed=>父组件destroyed
    总结:父组件先触发销毁钩子。子组件完全销毁后,父组件才完成销毁

(3)在父组件的 mounted 里调用子组件的用法时 this.$refs.child.$el,即使在生命周期层面一定是子组件先 mounted,父组件再 mounted。但实际上,父组件触发 mounted 时,子组件的渲染任务可能还在异步队列中。因此建议在需要访问子组件 DOM 的场景下,使用 nextTick 来确保安全访问。


1.2 基础概念 Vue3

Vue3 与 Vue2 生命周期的区别:

// Vue2 生命周期            Vue3 组合式 API
beforeCreate  ->           setup()
created       ->           setup()
beforeMount   ->           onBeforeMount
mounted       ->           onMounted
beforeUpdate  ->           onBeforeUpdate
updated       ->           onUpdated
beforeDestroy ->           onBeforeUnmount
destroyed     ->           onUnmounted

Vue3 示例:

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">增加计数</button>
  </div>
</template>
 
<script setup>
import { ref, onMounted, onBeforeMount, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
 
// 响应式数据
const title = ref('生命周期示例')
const count = ref(0)
 
// 方法
const increment = () => {
  count.value++
}
 
// 1. 挂载阶段
onBeforeMount(() => {
  console.log('onBeforeMount: 组件挂载前')
  // 可以访问响应式数据
  console.log('当前计数:', count.value)
})
 
onMounted(async () => {
  console.log('onMounted: 组件挂载完成')
  await nextTick()
  // 可以访问 DOM
  console.log('DOM 元素:', document.querySelector('h1').textContent)
})
 
// 2. 更新阶段
onBeforeUpdate(() => {
  console.log('onBeforeUpdate: 组件更新前')
  // 可以访问更新前的 DOM
  console.log('更新前的计数:', document.querySelector('p').textContent)
})
 
onUpdated(() => {
  console.log('onUpdated: 组件更新后')
  // 可以访问更新后的 DOM
  console.log('更新后的计数:', document.querySelector('p').textContent)
})
 
// 3. 卸载阶段
onBeforeUnmount(() => {
  console.log('onBeforeUnmount: 组件卸载前')
  // 清理工作:移除事件监听、定时器等
})
 
onUnmounted(() => {
  console.log('onUnmounted: 组件已卸载')
})
</script>

整体用法和 Vue2 一致。需要说明的是:

(1)setup 函数替代了 beforeCreate 和 created,内部不能使用 this。

(2)父子组件生命周期的触发顺序和 Vue2 一致。

  • 创建和挂载:
    父组件setuponBeforeMount => 所有子组件setuponBeforeMount => 所有子组件onMounted => 父组件onMounted
    总结:父组件先创建,然后子组件创建;子组件完全挂载后,父组件挂载

  • 更新过程:
    父组件onBeforeUpdate =>所有子组件onBeforeUpdate =>所有子组件onUpdated =>父组件onUpdated
    总结:父组件先触发更新钩子。子组件更新完成后,父组件才完成更新

  • 销毁过程:
    父组件onBeforeUnmount =>所有子组件onBeforeUnmount =>所有子组件onUnmounted =>父组件onUnmounted
    总结:父组件先触发销毁钩子。子组件完全销毁后,父组件才完成销毁


2、Vue 组件间通信

2.1 v-bind + props + $emit

v-bind 是最简单的一种通信方式,示例如下:

<!-- 父组件 -->
<template>
  <child v-model="test" :title.sync="pageTitle" :msg="message" @custom-event="handleEvent"/>
  <!-- .sync 等同于 -->
  <!-- <child :title="pageTitle" @update:title="pageTitle = $event"></child> -->
  <!-- v-model 等同于 -->
  <!-- <child :value="test" @input="test = $event"></child> -->
  <!-- v-bind 完整写法 -->
  <!-- <child v-bind:msg="message" />-->
</template>
<!-- 子组件 -->
<script>
export default {
  props: ['msg', 'title', 'value'],
  methods: {
    updateValue(newValue) {
      this.$emit('input', newValue)
    },
    updateTitle() {
      this.$emit('update:title', newValue)
    },
    sendToParent() {
      this.$emit('custom-event', data)
    }
  }
}
</script>

说明如下:

.sync 修饰符本质上是一个语法糖,会被扩展为上例中的形式,因此子组件才会用 this.$emit('update:title', newValue)

v-model 也算是语法糖,会被扩展为上例中的形式,因此子组件才会用 this.$emit('input', newValue)

③ .sync 和 v-model 的区别:

特性.syncv-model
绑定的 props 名称自定义(如 title)固定为 value
触发的事件名称update:<propName>(如 update:title)固定为 input
使用场景适用于需要双向绑定的任意 props适用于表单控件的绑定

虽然它们使用上可以等价,但最大的区别就在于使用场景,v-model 可以简化双向绑定的操作,特别适合表单控件。如下:

<template>
  <input v-model="username" />
  <!-- 如果不使用v-model -->
  <input :value="username" @input="username = $event.target.value" />
</template>
 
<script>
export default {
  data() {
    return {
      username: ''
    }
  }
}
</script>

④ 关于 props 的使用:

(1) 基本用法:最简单的方式是直接定义 props 为一个数组,列出接收的属性名称 props: ['title', 'content']。缺点明显:这样无法进行类型校验或设置默认值。

(2) 对象语法(指定类型):可以将 props 定义为一个对象,指定每个属性的类型(如果父组件传递的值类型不匹配,Vue 会在开发环境下发出警告)

props: {
  title: String, // 必须是字符串
  count: Number, // 必须是数字
  isActive: Boolean, // 必须是布尔值
  items: Array, // 必须是数组
  user: Object, // 必须是对象
  callback: Function // 必须是函数
}

(3) 设置默认值 + 必填属性:可以通过 default 为 props 设置默认值(如果父组件没有传递对应的 props,子组件会使用默认值);可以通过 required 指定某个 props 是必填的(如果父组件没有传递必填项,Vue 会在开发环境下发出警告)。

数组和对象使用函数进行返回,是为了避免它们在所有组件实例之间共享,导致意外的状态污染。

props: {
  title: {
    type: String,
    default: '默认标题',
    required: true
  },
  count: {
    type: Number,
    default: 0
  },
  isActive: {
    type: Boolean,
    default: false
  },
  items: {
    type: Array,
    default: () => []
  },
  user: {
    type: Object,
    default: () => ({ name: '默认用户' })
  }
}

(4) 自定义类型校验:可以通过 validator 自定义校验规则(如果校验失败,Vue 会在开发环境下发出警告)

props: {
  count: {
    type: Number,
    validator(value) {
      // 只接受大于 0 的数字
      return value > 0
    }
  }
}

(5)需要注意的是:如果类型不匹配,Vue 仅会发出警告,但不会阻止代码运行;props 是响应式的,当父组件传递的值发生变化时,子组件会自动更新。

该通信方式仅适用于父子组件

✅ 优点:

  • 简单直观
  • 符合单向数据流

❌ 缺点:

  • 层级深时需要逐层传递

2.2 $refs / $parent / $children

该用法里,用的比较多的是 $refs。示例:

<!-- 父组件 -->
<template>
  <child ref="childComp"/>
</template>
 
<script>
export default {
  mounted() {
    this.$refs.childComp.childMethod()
    this.$children[0].childMethod()
  }
}
</script>
 
<!-- 子组件 -->
<script>
export default {
  methods: {
    callParent() {
      this.$parent.parentMethod()
    }
  }
}
</script>

需要说明的是:

(1) $refs 只能访问通过 ref 注册的子组件;

(2) $children 只能访问直接子组件,无法访问嵌套的子孙组件;

(3) 不推荐直接通过 $refs$parent$children 修改 data。

该通信方式适用于父子组件

✅ 优点:

  • 访问组件实例内的所有内容
  • 特别适用于父组件调用子组件方法的场景

❌ 缺点:

  • 破坏数据流向(原本应该是:父组件通过props向子组件传递数据;子组件通过$emit向父组件发送事件。现在变成了双向或多向通信)
  • 组件耦合度高(组件之间的依赖关系过于紧密,导致组件无法独立使用或复用 => 组件需要明确知道其他组件的结构和方法,且如果对应方法发生改变,其他组件也可能要配合修改。这种强依赖关系会导致代码的维护成本增加)
  • $children 不保证顺序

2.3 provide / inject

示例:

<!-- 顶层组件 -->
<script>
export default {
  provide() {
    return {
      theme: this.theme
    }
  }
}
</script>
<!-- 子组件 -->
<script>
export default {
  inject: ['theme'],
  mounted() {
    console.log(this.theme)
  }
}
</script>

✅ 优点:

  • 该通信方式可跨多层级通信
  • 适合组件库开发:比如在上述顶层组件提供一些全局配置(如主题、语言、样式等),子组件就可以通过 inject 获取这些配置。这种方式避免了逐层传递 props,使得组件库的使用更加简洁。

❌ 缺点:

  • 难以追踪数据来源:在复杂的组件层级中,子组件无法直接知道数据是从哪个父组件提供而来,这可能导致调试困难。(该问题有解决方法,就是在 provide 新增一个字段,用于标注数据来源)
  • 数据不是响应式(父组件中 provide 数据发生变化,子组件通过 inject 获取的数据不会自动更新)

接下来主要说下响应式的问题:provide / inject 的设计初衷是为了简化跨层级数据传递,而不是为了实现响应式数据共享。如果想解决这个问题,可以使用 Vue 的响应式对象。

<!-- 父组件 -->
<script>
import Vue from 'vue';
 
export default {
  data() {
    return {
      theme: Vue.observable({ value: 'light' })
    }
  },
  provide() {
    return {
      theme: this.theme
    }
  },
  methods: {
    changeTheme() {
      this.theme.value = 'dark';
    }
  }
}
</script>
 
<template>
  <button @click="changeTheme">切换主题</button>
  <child />
</template>
<!-- 子组件 -->
<script>
export default {
  inject: ['theme'],
  mounted() {
    console.log(this.theme.value); // 初始值为 'light'
  }
}
</script>
 
<template>
  <div>当前主题:{{ theme.value }}</div>
</template>

再简单点的,可以直接使用函数,返回父组件的响应式数据:

<!-- 父组件 -->
<script>
export default {
  data() {
    return {
      theme: 'light'
    }
  },
  provide() {
    return {
      theme: () => this.theme // 使用函数返回响应式数据
    }
  },
  methods: {
    changeTheme() {
      this.theme = 'dark';
    }
  }
}
</script>
 
<template>
  <button @click="changeTheme">切换主题</button>
  <child />
</template>
<!-- 子组件 -->
<script>
export default {
  inject: ['theme'],
  computed: {
    currentTheme() {
      return this.theme(); // 调用函数获取最新值
    }
  }
}
</script>
 
<template>
  <div>当前主题:{{ currentTheme }}</div>
</template>

如果是 Vue3.x 那可以直接传递 ref / reactive 响应式对象:

const count = ref(100);
provide('count-key', count);

2.4 事件总线 EventBus

使用方法参考文章:https://zhuanlan.zhihu.com/p/72777951/

博主几乎不用该方式,使用它需要注意:

✅ 优点:

  • 该通信方式支持任意组件间通信
  • 使用简单,代码量少

❌ 缺点:

  • 难以追踪数据来源(和 provide 内的说明一样)
  • 容易造成内存泄漏,至少需要在 beforeDestroy 时手动销毁监听
  • 项目规模大时,事件管理复杂,维护成本高(在小型项目或简单的组件间通信场景下,使用该方式或许是不错的选择

2.5 Vuex(全局状态管理)

使用方法参考文章:https://blog.youkuaiyun.com/qq_45613931/article/details/105903799

  • 组件通过 dispatch 触发 Action。
  • Action 通过 commit 调用 Mutation(可用于处理异步逻辑(如接口请求))。
  • Mutation 同步修改 state。
  • state 的变化会自动更新到组件。

✅ 优点:

  • 集中状态管理:解决了组件间状态共享问题
  • 模块化:支持将状态分割到多个模块中,方便管理
  • 响应式:状态变化会自动更新到组件。
  • 调试工具支持:可以通过 Chrome 插件 Vue.js devtools,对 Vuex 进行调试
  • 适合大型应用

❌ 缺点:

  • 小项目使用成本高,使用 Vuex 增加了代码复杂度。

综上,如果需要集中管理复杂的状态,或多个组件需要频繁共享和更新状态(比如用户信息、系统信息等),使用 Vuex 全局存储是个可以选择的方案

Vuex 除了可以用上文的 EventBus 替代外,还有其他替代方案,在这里简单提一下,感兴趣的朋友可以了解下:

Pinia:Vuex 的轻量化替代方案,支持更简单的 API 和 TypeScript。官网:https://pinia.web3doc.top/

需要额外说明的是:现在这些全局状态管理的功能,如果要用来存储用户信息,其实不算是最佳方案。如 Vuex,它的数据存储在内存中,页面刷新就会导致内存清空,Vuex 的状态也随之丢失。如果真的要存储这类信息,刷新页面也不丢失的全局通信,可以考虑存储在浏览器缓存中(详见第 2.6 节)。


2.6 LocalStorage / SessionStorage / Cookie

该方法勉强算是全局通信的一种方式,适合简单的持久化状态管理,但是不建议滥用。比如在第 2.5 节提到,存储用户信息。当然实际场景里,不建议明文存储,可以考虑加密或者存储用户 Token(标识) 信息。

随后页面在刷新时,通过在缓存中提取 Token,调用接口拿到用户信息。如果没有 Token 则进行登录;如果有 Token 则确认正确性。

使用示例:

// LocalStorage 示例
function localStorageDemo() {
    console.log("=== LocalStorage 示例 ===");
 
    // 存储数据
    localStorage.setItem('username', 'Alice');
    localStorage.setItem('theme', 'dark');
 
    // 读取数据
    const username = localStorage.getItem('username');
    const theme = localStorage.getItem('theme');
    console.log(`LocalStorage - Username: ${username}, Theme: ${theme}`);
 
    // 删除数据
    localStorage.removeItem('username');
    console.log("LocalStorage - After removing 'username':", localStorage.getItem('username'));
 
    // 清空所有数据
    localStorage.clear();
    console.log("LocalStorage - After clearing:", localStorage.getItem('theme'));
}
 
// SessionStorage 示例
function sessionStorageDemo() {
    console.log("=== SessionStorage 示例 ===");
 
    // 存储数据
    sessionStorage.setItem('sessionId', '12345');
    sessionStorage.setItem('isLoggedIn', 'true');
 
    // 读取数据
    const sessionId = sessionStorage.getItem('sessionId');
    const isLoggedIn = sessionStorage.getItem('isLoggedIn');
    console.log(`SessionStorage - Session ID: ${sessionId}, Is Logged In: ${isLoggedIn}`);
 
    // 删除数据
    sessionStorage.removeItem('sessionId');
    console.log("SessionStorage - After removing 'sessionId':", sessionStorage.getItem('sessionId'));
 
    // 清空所有数据
    sessionStorage.clear();
    console.log("SessionStorage - After clearing:", sessionStorage.getItem('isLoggedIn'));
}
 
// Cookie 示例
function cookieDemo() {
    console.log("=== Cookie 示例 ===");
 
    // 设置 Cookie
    document.cookie = "username=Alice; path=/";
    const date = new Date();
    date.setDate(date.getDate() + 7); // 7 天后过期
    document.cookie = `theme=dark; expires=${date.toUTCString()}; path=/`;
 
    // 读取所有 Cookie
    console.log("Cookies:", document.cookie);
 
    // 解析 Cookie
    const getCookie = (name) => {
        const cookies = document.cookie.split('; ');
        const cookie = cookies.find(c => c.startsWith(`${name}=`));
        return cookie ? cookie.split('=')[1] : null;
    };
    console.log("Cookie - Username:", getCookie('username'));
    console.log("Cookie - Theme:", getCookie('theme'));
 
    // 删除 Cookie => 设置过期时间为过去的时间
    document.cookie = "username=Alice; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
    console.log("Cookie - After removing 'username':", getCookie('username'));
}

它们的区别在于:

特性localStoragesessionStorageCookie
生命周期持久化存储,除非手动清除当前标签页有效,关闭标签页后清除可设置过期时间,默认会话级别
存储大小约 5MB约 5MB约 4KB
作用域所有同源标签页当前同源标签页客户端和服务器共享
数据传输不随 HTTP 请求发送不随 HTTP 请求发送每次 HTTP 请求都会自动携带
适用场景长期数据(如用户偏好设置、主题等)临时数据(如表单状态、页面状态)少量数据(如会话标识、用户登录状态)

额外总结:

(1) 不要在 Cookie 中存储敏感信息(如密码),因为 Cookie 会随请求发送,容易被窃取。如果需要存储敏感信息,建议使用 localStorage 或 sessionStorage,并结合 HTTPS 和加密技术。

(2) localStorage 或 sessionStorage 适合存储较大的数据(如 JSON 对象)。不会随 HTTP 请求发送,适合前端使用。


回到之前的问题,如果要存储用户信息:

(1) 通常前端都不会用来存储用户的详细信息,可以和后端约定 Token标识(方案可为:随机生成 + 确保唯一性。短时间不调用接口则过期 - 重新登录),并存储在 cookie 里通过这个标识来代表用户,在需要时让后端来解析

=> 不建议在浏览器上用 localStorage 或 sessionStorage 存储,因为持久化存储 / 会话级别存储 还是容易被 XSS 攻击窃取,且无法自动随请求发送。

(2)因为 cookie 会自动跟随请求发送,所以每次发起请求,后端都可以通过 cookie 来匹配对应用户。如果某页面需要展示用户信息,则用标识去调用接口查询。

(3) 为了 cookie 更安全,还可以配合 HttpOnly、Secure、SameSite 标志使用,防止 JavaScript 访问、XSS 和劫持,防止跨站请求伪造。


2.7 slot插槽

示例:

<template>
  <div>
    <!-- 使用子组件 -->
    <ChildComponent>
      <!-- 默认插槽内容 -->
      <p>这是传递给默认插槽的内容</p>
 
      <!-- 具名插槽内容 -->
      <template v-slot:header>
        <h1>这是头部内容</h1>
      </template>
      <template v-slot:footer>
        <p>这是底部内容</p>
      </template>
 
      <!-- 作用域插槽内容 -->
      <template v-slot:default="slotProps">
        <p>作用域插槽 - 用户名: {{ slotProps.user.name }}</p>
        <p>作用域插槽 - 年龄: {{ slotProps.user.age }}</p>
      </template>
    </ChildComponent>
  </div>
</template>
 
<script>
import ChildComponent from './ChildComponent.vue';
 
export default {
  components: {
    ChildComponent
  }
};
</script>

子组件 ChildComponent.vue:

<template>
  <div>
    <!-- 默认插槽 -->
    <slot></slot>
 
    <!-- 具名插槽 -->
    <slot name="header"></slot>
    <slot name="footer"></slot>
 
    <!-- 作用域插槽 -->
    <slot :user="user"></slot>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      user: { name: '张三', age: 25 }
    };
  }
};
</script>

✅ 优点:

  • 灵活性高:插槽允许父组件动态地控制子组件的内容,增强了组件的复用性和灵活性
  • 内容分发清晰:通过具名插槽,可以明确地定义内容插入的位置,代码结构更清晰
  • 支持动态数据作用域插槽可以将子组件的数据传递给父组件,支持动态渲染

❌ 缺点:

  • 代码复杂性增加
  • 如果插槽内容过于复杂,可能会对渲染性能产生一定影响

2.8 $attrs / $listeners

$attrs 和 $listeners 是两个特殊的属性,主要用于处理父组件传递给子组件的非绑定属性和事件监听器。示例:

<!-- 父组件 -->
<template>
  <child-a :msg="msg" @custom="handleCustom"/>
</template>
 
<!-- 中间组件 -->
<template>
  <child-b v-bind="$attrs" v-on="$listeners"/>
</template>
 
<!-- 子组件 -->
<script>
export default {
  inheritAttrs: false,
  created() {
    console.log(this.$attrs) // { msg: xxx }
  }
}
</script>

其中:
(1) $attrs 是一个对象,包含了父组件传递给子组件的非绑定属性(即没有在 props 中显式定义的属性)。这些属性会自动添加到子组件的根元素上;

(2) $listeners 是一个对象,包含了父组件传递给子组件的事件监听器(即通过 v-on 绑定的事件)。

✅ 优点:

  • 跨多层级通信
  • 在封装组件时,让子组件动态地、轻松地接收父组件的属性和事件,减少硬编码
  • 无需显式定义所有 props 和事件,代码更简洁

❌ 缺点:

  • 使用 $attrs 和 $listeners 时,会让代码的逻辑变得不够清晰
  • 因为没有 props 类型定义,所以如果父组件传递了错误的属性或事件,可能会导致子组件的行为异常
  • Vue 2.4+ 才支持

2.9 使用场景建议

  1. 父子组件通信:
    • 首选 Props/$emit
    • 需要访问组件实例时用 $refs
  2. 兄弟组件通信:
    • 小项目用 EventBus
    • 大项目用 Vuex
  3. 跨层级通信:
    • 组件库场景用 provide/inject
    • 业务场景用 Vuex
    • 中间层透传用 a t t r s / attrs/ attrs/listeners
  4. 全局状态管理:
    • 中大型项目用 Vuex
    • 小项目用 EventBus
    • 存用户 Token 用 Cookie

3、mixins 混入

在 Vue 中,混入是一种复用组件逻辑的方式。通过定义一个混入对象,可以将其逻辑注入到多个组件中。混入对象可以包含组件的生命周期钩子、数据、方法、计算属性等。示例:

创建一个混入文件 mixins/loggerMixin.js:

export const loggerMixin = {
  data() {
    return {
      loggerMessage: '页面加载完成'
    };
  },
  methods: {
    formatDate(date) {
      const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
      return new Intl.DateTimeFormat('zh-CN', options).format(date);
    }
  },
  created() {
    console.log(this.loggerMessage);
  }
};

在组件中使用混入 MyComponent.vue:

<template>
  <div>
    <h1>混入示例</h1>
    <p>当前日期:{{ formattedDate }}</p>
  </div>
</template>
 
<script>
import { loggerMixin } from '../mixins/loggerMixin';
 
export default {
  mixins: [loggerMixin],
  data() {
    return {
      currentDate: new Date()
    };
  },
  computed: {
    formattedDate() {
      return this.formatDate(this.currentDate);
    }
  }
};
</script>

需要注意,Vue 在初始化组件时,会将组件的选项与混入的选项进行合并(合并的方式在上一篇原理里介绍过,其实就是简单的 Options Merge)。不同的选项类型有不同的合并策略:

  • 数据(data):
    如果组件和混入都定义了 data,Vue 会合并它们,组件的 data 优先级更高
  • 生命周期钩子
    如果组件和混入都定义了同一个生命周期钩子(如 created),它们会被合并成一个数组,按顺序依次执行
  • 方法、计算属性、侦听器
    如果组件和混入定义了同名的方法或计算属性,组件的定义会覆盖混入的定义

✅ 优点

  • 逻辑复用:可以将通用的逻辑抽离到混入中,减少代码重复。
  • 模块化:通过混入可以将功能模块化,便于维护和扩展。
  • 灵活性:可以在多个组件中使用同一个混入,灵活注入逻辑。

❌ 缺点

  • 命名冲突:如果混入和组件中定义了同名的属性或方法,可能会导致覆盖或意外行为。
  • 调试困难:混入的逻辑可能会分散在多个文件中,调试时不容易定位问题。
  • 可读性降低:当项目中使用了大量混入时,组件的逻辑来源可能变得不清晰。

不过在 Vue2 中,这属于不得已而为之。在 Vue3 中,推荐 使用组合式 API(Composition API) 来替代混入,提供更清晰的逻辑组织方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端Jerry_Zheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值