系列文章目录
文章目录
- 系列文章目录
- 一、组合式函数
-
- [1.1 什么是"组合式函数"?](#1.1 什么是“组合式函数”?)
- [1.2 鼠标跟踪器示例](#1.2 鼠标跟踪器示例)
- [1.3 接收响应式状态](#1.3 接收响应式状态)
-
- [1.3.1 toValue():参数规范化的利器 (Vue 3.3+)](#1.3.1 toValue():参数规范化的利器 (Vue 3.3+))
- [1.4 约定和最佳实践](#1.4 约定和最佳实践)
-
- [1.4.1 命名:use 开头](#1.4.1 命名:use 开头)
- [1.4.2 输入参数:兼容并蓄 (toValue)](#1.4.2 输入参数:兼容并蓄 (toValue))
- [1.4.3 返回值:解构友好的 ref 对象](#1.4.3 返回值:解构友好的 ref 对象)
- [1.4.4 副作用](#1.4.4 副作用)
- [1.4.5 使用限制](#1.4.5 使用限制)
- 二、自定义指令
-
- [2.1 介绍](#2.1 介绍)
-
- [2.1.1. 定位与差异化](#2.1.1. 定位与差异化)
- [2.1.2. 注册方式](#2.1.2. 注册方式)
- [2.2 自定义指令的使用时机](#2.2 自定义指令的使用时机)
-
- [2.2.1 🔍 为什么要用自定义指令?(核心理由)](#2.2.1 🔍 为什么要用自定义指令?(核心理由))
- [📅 典型应用场景汇总](#📅 典型应用场景汇总)
- [2.3 ⚓ Vue 自定义指令钩子函数 (Custom Directives Hooks)](#2.3 ⚓ Vue 自定义指令钩子函数 (Custom Directives Hooks))
-
- [2.3.1 钩子函数一览表](#2.3.1 钩子函数一览表)
- [2.3.2 钩子参数详解](#2.3.2 钩子参数详解)
- [2.3 快速参考:简写模式](#2.3 快速参考:简写模式)
- [2.4 🚀 Vue 自定义指令进阶:对象参数与组件应用](#2.4 🚀 Vue 自定义指令进阶:对象参数与组件应用)
-
- [2.4.1 对象字面量 (Object Literals)](#2.4.1 对象字面量 (Object Literals))
- [2.4.2 在组件上使用 (不推荐)](#2.4.2 在组件上使用 (不推荐))
- [Q1:如果一个自定义指令需要接收 5 个配置参数,你会如何设计?](#Q1:如果一个自定义指令需要接收 5 个配置参数,你会如何设计?)
- Q2:为什么不推荐在组件上使用自定义指令?
- Q3:自定义指令绑定的值是响应式的吗?
- [❌ 易错点 1:在多根节点组件上绑定指令](#❌ 易错点 1:在多根节点组件上绑定指令)
- [❌ 易错点 2:误以为指令可以像 Attributes 一样透传](#❌ 易错点 2:误以为指令可以像 Attributes 一样透传)
- [❌ 易错点 3:忽略对象字面量的更新对比](#❌ 易错点 3:忽略对象字面量的更新对比)
- 三、插件
-
- [1. 核心知识点总结](#1. 核心知识点总结)
-
- [1.1 插件的本质](#1.1 插件的本质)
- [1.2 插件的常见用途](#1.2 插件的常见用途)
- [1.3 注入方式对比](#1.3 注入方式对比)
- [2. 面试高频问题](#2. 面试高频问题)
-
- [Q1:Vue 插件和组件 (Component) 的区别是什么?](#Q1:Vue 插件和组件 (Component) 的区别是什么?)
- Q2:如何避免插件注入的全局属性与组件内部属性发生冲突?
一、组合式函数
1.1 什么是"组合式函数"?
- "组合式函数"(Composables): 是一个利用 Vue 的 组合式 API 来 封装和复用 有状态逻辑 的函数。
- 它把原本散落在组件各处的、带有响应式状态的逻辑,打包成一个可以到处运行的函数。
- 有状态逻辑负责管理会随时间而变化的状态。
为什么它改变了 Vue 的开发方式?
-
在没有组合式函数之前(Vue 2 时代),复用逻辑主要靠 Mixins。但 Mixins 有三大"剧毒":
-
来源不明: 多个 Mixins 混入后,你不知道 this.x 到底是哪个文件定义的。
-
命名冲突: 两个 Mixins 都定义了 count,直接发生覆盖。
-
隐式耦合: Mixins 之间互相读取对方的数据,维护起来像一团乱麻。
-
-
组合式函数完美解决了这些问题:
-
显式导入: 你能清楚看到数据和方法是从哪个文件导出的。
-
解构重命名: const { x: mouseX } = useMouse(),完全不怕变量名冲突。
-
高度内聚: 相关的逻辑(如监听、数据、清理)都写在一个函数里。
-
命名规范与惯例
- 按照 Vue 社区的约定,所有的组合式函数都应该以 use 开头(如 useMouse, useAuth, useTable)。这能让你在阅读代码时一眼看出:"这是一个带有响应式状态的函数!"
1.2 鼠标跟踪器示例
- 我们可以把逻辑以一个 组合式函数 的形式提取到外部文件中
- 按照惯例,组合式函数名以 "use" 开头
- 组合式函数可以随时更改其状态。
- 一个组合式函数也可以挂靠在所属组件的生命周期上,来启动和卸载副作用
- 和在组件中一样,你也可以在组合式函数中使用所有的组合式 API
- 你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。
- 每一个调用 useMouse() 的组件实例会 创建其独有的 x、y 状态拷贝,因此他们不会互相影响。
1.3 接收响应式状态
1.3.1 toValue():参数规范化的利器 (Vue 3.3+)
这是让组合式函数变得"通杀"的关键。toValue() 能够智能处理三种输入:
-
普通值: 直接返回。
-
Ref: 返回 .value。
-
Getter 函数: 执行函数并返回结果。
自动追踪与重运行
-
配合 watchEffect,组合式函数可以实现自动联动:
-
如果你传入的是一个ref 或 getter ,watchEffect 会在内部调用 toValue 时自动收集这些依赖。
-
一旦依赖变了(比如 props.id 变了),watchEffect 会自动重新触发 fetchData。
javascript
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 这将会重新触发 fetch
url.value = '/new-url'
javascript
// 当 props.id 改变时重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)
javascript
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// reset state before fetching..
data.value = null
error.value = null
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
watchEffect(() => {
fetchData()
})
return { data, error }
}
Q1: toValue() 和 unref() 有什么区别?
-
unref() 只解包 Ref。如果传入的是函数,它会原样返回函数。
-
toValue() 更加强大,它不仅解包 Ref,还会执行 Getter 函数。这使得它非常适合处理 () => props.id 这种输入。
Q2: 为什么要在 watchEffect 内部调用 toValue?
- 为了建立响应式追踪。只有在 Effect 运行期间访问响应式数据,Vue 才能知道这个 Effect 依赖于谁。如果放在外面,watchEffect 就变成了一个死逻辑,不会随参数变化而重跑。
Q3: 异步组合式函数如何处理"竞态问题" (Race Condition)?
- (进阶点)如果请求 A 还没结束,请求 B 就开始了,可能 A 后返回覆盖了 B。
- 解决方案: 在 watchEffect 中使用 onCleanup 回调来忽略过时的请求结果,或者使用 AbortController 取消之前的请求。
| 特性 | 初始版 useFetch |
增强版 useFetch (推荐) |
|---|---|---|
| 参数类型 | 仅限静态字符串 | 字符串、Ref、Getter 函数 |
| 触发频率 | 仅在调用时执行一次 | 随输入参数变化自动重新请求 |
| 内部机制 | 直接执行 fetch 逻辑 |
watchEffect + toValue() 规范化 |
| 灵活性 | 低,仅适合加载静态配置文件 | 极高,适合详情页、搜索列表、动态过滤 |
| 代码简洁度 | 逻辑简单,但不具备响应性 | 封装了自动追踪逻辑,调用方更省心 |
1.4 约定和最佳实践
1.4.1 命名:use 开头
-
规范: 始终使用驼峰命名法并以 use 开头(如 useAuth)。
-
意义: 明确标识该函数具有有状态逻辑,提醒开发者它可能包含 ref、watch 或生命周期钩子。
1.4.2 输入参数:兼容并蓄 (toValue)
-
最佳实践: 无论用户传的是 普通值 、ref 还是 getter 函数,都使用 toValue() 进行规范化。
-
追踪: 如果需要参数变动时自动触发逻辑,务必在 watch 或 watchEffect 中处理
1.4.3 返回值:解构友好的 ref 对象
-
推荐: 返回一个包含多个 ref 的普通对象。
-
理由: 这样用户可以直接解构 (const { x, y } = useMouse())而不会丢失响应性。
-
反例: 如果返回一个 reactive 对象,解构后得到的变量将变成普通值,失去与原状态的联系。
1.4.4 副作用
- 在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
- 如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用。因此可以确保能访问到 DOM。
- 确保在 onUnmounted() 时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted() 中被移除
1.4.5 使用限制
- 组合式函数只能在
<script setup>或 setup() 钩子中被调用。 - 在这些上下文中,它们也只能被 同步 调用。
- 在某些情况下,你也可以在像 onMounted() 这样的生命周期钩子中调用它们。
这些限制很重要,因为这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:
-
将生命周期钩子注册到该组件实例上
-
将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
TIP
<script setup> 是唯一在调用 await 之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。
Q1: 为什么组合式函数推荐返回 ref 对象而不是 reactive 对象?
- 为了支持解构 (Destructuring)。ES6 的解构操作会直接读取对象属性的值。
- 如果是 ref,解构得到的是引用,响应性得以保留;
- 如果是 reactive,解构得到的是纯粹的值(如字符串或数字),响应性会立即断开。
Q2: 组合式函数可以在 setTimeout 里调用吗?
- 不可以。 必须在 setup() 或
Q3: toValue() 相比 unref() 的核心优势是什么?
- 对 Getter 函数 的支持。在处理 () => props.id 这种常见的参数传递方式时,toValue() 会自动执行函数并获取最新值,而 unref() 只能处理 ref。
二、自定义指令
2.1 介绍
2.1.1. 定位与差异化
自定义指令的本质是 DOM 操纵的封装。Vue 推荐的代码复用方案优先级如下:
-
组件 (Components):负责 UI 结构和业务逻辑的单元。
-
组合式函数 (Composables):负责有状态逻辑的复用(数据的响应式处理)。
-
自定义指令 (Directives):负责底层 DOM 访问逻辑的复用(如:自动聚焦、拖拽、图片懒加载)。
2.1.2. 注册方式
-
局部注册 (
<script setup>):必须以 v 开头的驼峰命名(如 vMyDirective),模板中使用 v-my-directive。 -
局部注册 (选项式):在 directives 选项中声明。
-
全局注册:通过 app.directive('name', { ... }),在整个应用中可用。
2.2 自定义指令的使用时机
只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。
2.2.1 🔍 为什么要用自定义指令?(核心理由)
-
超越静态 HTML 属性:
- 正如你提到的 autofocus。
- 原生属性通常只在页面初次加载时生效一次,而 Vue 是单页应用(SPA),组件会被频繁地销毁、重建。
- v-focus 的 mounted 钩子确保了只要组件出现在屏幕上,逻辑就会执行。
-
封装第三方非 Vue 插件:
- 当你集成一个操作 DOM 的原生 JS 库(如 Chart.js、Sortable.js 或某些地图 API)时,指令是极佳的粘合层。
-
横切关注点 (Cross-cutting Concerns):
- 一些与业务数据无关,但需要应用到大量元素上的交互逻辑(如:权限控制、埋点统计、长按手势)。
🚀 典型的使用场景
📅 典型应用场景汇总
| 场景 | 指令示例 | 逻辑说明 |
|---|---|---|
| 自动聚焦 | v-focus |
页面加载或动态插入元素时,自动获取输入焦点。 |
| 权限控制 | v-permission |
根据用户角色,动态从 DOM 中 remove 权限不足的元素。 |
| 防抖/节流 | v-debounce |
限制点击事件频率,防止接口重复调用。 |
| 外部点击 | v-click-outside |
监听元素外的点击动作,常用于关闭下拉框、模态框。 |
| 图片懒加载 | v-lazy |
结合 IntersectionObserver,仅在图片进入视口时加载 src。 |
| 输入限制 | v-number-only |
实时拦截非数字输入,直接操作 el.value。 |
2.3 ⚓ Vue 自定义指令钩子函数 (Custom Directives Hooks)
自定义指令提供了一组生命周期钩子,允许你在元素被创建、插入、更新或卸载时介入并执行底层的 DOM 操作。
2.3.1 钩子函数一览表
| 钩子名称 | 触发时机 | 典型应用场景 |
|---|---|---|
created |
在绑定元素的 attribute 或事件监听器应用之前调用。 | 初始化非 DOM 依赖的数据。 |
beforeMount |
在指令第一次绑定到元素且挂载父组件之前调用。 | 元素挂载前的预处理逻辑。 |
mounted |
最常用。绑定元素的父组件及所有子节点都挂载完成后调用。 | 操作原生 DOM、获取焦点、初始化第三方插件。 |
beforeUpdate |
在绑定元素的父组件更新前调用。 | 在 DOM 更新前读取当前状态(如滚动位置、尺寸)。 |
updated |
在绑定元素的父组件及所有子节点更新后调用。 | 根据响应式数据变化,同步更新 DOM 状态。 |
beforeUnmount |
绑定元素的父组件卸载前调用。 | 卸载前的准备工作(如停止计时器)。 |
unmounted |
绑定元素的父组件卸载后调用。 | 清理副作用(移除事件监听器、销毁插件实例)。 |
2.3.2 钩子参数详解
每个钩子函数都会接收以下参数(注意:除 el 外,其余参数均为 只读):
el: 指令绑定到的 真实 DOM 元素。可以直接修改其样式、属性或监听事件。binding: 一个包含指令信息的对象:value: 传递给指令的值(如v-my="2",值为2)。oldValue: 指令前一个状态的值(仅在更新钩子中可用)。arg: 传递给指令的参数(如v-my:foo,参数为"foo")。modifiers: 修饰符对象(如v-my.lazy,结果为{ lazy: true })。instance: 使用该指令的组件实例。
vnode: 代表当前元素的底层虚拟 DOM 节点。prevVnode: 上一次渲染时的虚拟节点(仅在更新钩子中可用)。
2.3 快速参考:简写模式
如果你只需要在 mounted 和 updated 时执行相同的行为,可以采用简写形式:
javascript
// 全局注册简写
app.directive('color', (el, binding) => {
// 这对应了 mounted 和 updated 两个阶段
el.style.color = binding.value
})
2.4 🚀 Vue 自定义指令进阶:对象参数与组件应用
2.4.1 对象字面量 (Object Literals)
当自定义指令需要多个配置项时,可以向其传递一个 JavaScript 对象字面量。指令不仅支持简单值,还支持任何合法的 JavaScript 表达式。
- 模板语法 :
<div v-demo="{ color: 'white', text: 'hello!' }"></div> - 获取方式 :在钩子函数中通过
binding.value访问该对象。 - 优势:增强了指令的可配置性,避免了定义过多的指令参数(arg)。
2.4.2 在组件上使用 (不推荐)
自定义指令可以像透传 Attributes 一样作用于组件,但存在严格限制:
- 应用规则 :指令始终应用于组件的 根节点 (Root Node)。
- 多根组件限制 :如果组件有多个根节点(Fragments),指令会被忽略并抛出警告。
- 非透传性 :指令不能 通过
v-bind="$attrs"转发给组件内的其他特定元素,这与普通的 HTML 属性不同。
Q1:如果一个自定义指令需要接收 5 个配置参数,你会如何设计?
参考回答 :
我会选择通过对象字面量的形式传递。
- 理由 :使用
v-dir:arg只能传递一个参数名,使用修饰符v-dir.a.b只能传递布尔值。而传递一个对象(如v-dir="{ a: 1, b: 2 ... }")可以一次性传入多种类型的数据,且结构清晰,易于维护。
Q2:为什么不推荐在组件上使用自定义指令?
参考回答 :
主要有三个原因:
- 不可控性:指令强制绑定在根节点,如果组件内部重构删除了根节点或变成了多根节点,指令会失效并报错。
- 破坏封装性:指令通常涉及直接的 DOM 操作,这违反了组件"通过 Props 驱动"的封装原则。
- 无法透传 :它不像
$attrs那样灵活,无法指定绑定到组件内部的某个特定子元素上。
Q3:自定义指令绑定的值是响应式的吗?
参考回答 :
是的。当传递给指令的表达式(如对象中的某个属性)发生变化时,Vue 会触发指令的 beforeUpdate 和 updated 钩子。我们可以在这些钩子中对比 binding.value 和 binding.oldValue 来执行相应的 DOM 更新。
❌ 易错点 1:在多根节点组件上绑定指令
- 错误现象 :在包含多个
<div>或<header>/<main>的组件上使用v-my-directive。 - 后果 :控制台抛出
Runtime directive used on component with non-element root node警告,且指令逻辑完全不执行。 - 修正:确保组件只有一个单一的包裹元素,或者改用 Props 在组件内部控制逻辑。
❌ 易错点 2:误以为指令可以像 Attributes 一样透传
- 错误认知 :认为在组件上写了
v-focus,可以通过v-bind="$attrs"传给内部的<input>。 - 事实 :指令不支持
$attrs。如果想让组件内部的 input 聚焦,应该给组件定义一个focus方法并在外部调用,或者给内部 input 直接绑定指令。
❌ 易错点 3:忽略对象字面量的更新对比
-
错误现象 :在
updated钩子中不加判断地执行耗时操作。 -
后果:由于对象字面量在每次渲染时可能都是"新对象",可能导致不必要的性能开销。
-
修正 :始终检查值的变化。
javascriptupdated(el, binding) { if (binding.value.color !== binding.oldValue.color) { el.style.color = binding.value.color; } }
- 小功能用参数/修饰符 :如
v-focus:immediate.lazy。 - 复杂配置用对象 :如
v-tooltip="{ content: '提示', placement: 'top' }"。 - 操作组件内部 DOM :严禁 从外部通过指令操作,应使用组件暴露的 Methods 或 Expose。
三、插件
插件是 Vue.js 中用于添加全局功能 的工具代码。无论是注册全局组件、添加全局属性,还是集成像 vue-router 这样的功能库,都离不开插件机制。
1. 核心知识点总结
1.1 插件的本质
- 形态 :插件可以是一个拥有
install()方法的对象 ,也可以直接是一个安装函数。 - 安装 :通过
app.use(plugin, options)调用。 - 参数 :
install函数接收两个参数:app: Vue 应用实例(提供component,directive,provide等方法)。options: 用户传入的自定义配置选项。
1.2 插件的常见用途
| 用途 | 实现方式 |
|---|---|
| 全局组件/指令 | app.component() 或 app.directive() |
| 依赖注入 | app.provide() 使资源可被整个应用 inject |
| 全局实例属性 | app.config.globalProperties (如注入 $translate) |
| 综合功能库 | 如 vue-router 同时包含上述所有操作 |
1.3 注入方式对比
- Global Properties : 在模板中直接通过
{``{ $func() }}调用,方便快捷,但易导致命名冲突。 - Provide / Inject: 符合组合式 API 风格,在组件内显式注入,逻辑更清晰,适合分发大型数据或函数。
2. 面试高频问题
Q1:Vue 插件和组件 (Component) 的区别是什么?
参考回答:
- 组件:是应用的基本构建块,负责视图渲染和特定的 UI 逻辑,通常是局部注册或按需使用的。
- 插件:是功能的扩展工具,负责为应用添加全局性的功能。插件通常在应用启动阶段安装一次,并影响整个应用的上下文环境。
Q2:如何避免插件注入的全局属性与组件内部属性发生冲突?
参考回答:
- 前缀法 :为全局属性添加特殊前缀(如
$i18n_translate而不是$t)。 - 使用 Symbol :在使用
provide/inject时,使用Symbol作为 Key,可以彻底避免命名冲突。 - 谨慎使用 GlobalProperties:Vue 3 官方更推荐通过 `provide/inject