VUE:逻辑复用

系列文章目录

VUE3:基础篇官网笔记
VUE3:深入组件官网笔记


文章目录

  • 系列文章目录
  • 一、组合式函数
    • [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. 面试高频问题)

一、组合式函数

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:为什么不推荐在组件上使用自定义指令?

参考回答 :

主要有三个原因:

  1. 不可控性:指令强制绑定在根节点,如果组件内部重构删除了根节点或变成了多根节点,指令会失效并报错。
  2. 破坏封装性:指令通常涉及直接的 DOM 操作,这违反了组件"通过 Props 驱动"的封装原则。
  3. 无法透传 :它不像 $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 钩子中不加判断地执行耗时操作。

  • 后果:由于对象字面量在每次渲染时可能都是"新对象",可能导致不必要的性能开销。

  • 修正 :始终检查值的变化。

    javascript 复制代码
    updated(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 函数接收两个参数:
    1. app: Vue 应用实例(提供 component, directive, provide 等方法)。
    2. 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:如何避免插件注入的全局属性与组件内部属性发生冲突?

参考回答:

  1. 前缀法 :为全局属性添加特殊前缀(如 $i18n_translate 而不是 $t)。
  2. 使用 Symbol :在使用 provide/inject 时,使用 Symbol 作为 Key,可以彻底避免命名冲突。
  3. 谨慎使用 GlobalProperties:Vue 3 官方更推荐通过 `provide/inject
相关推荐
Dream-Y.ocean1 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn2 小时前
性能监控
前端
Highcharts.js2 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn2 小时前
构建工具
前端
FRing033 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷3 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen5 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒5 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫6 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库
jerrywus6 小时前
我用 GPT-6.1-sol 做了台会讲解的 3D 售货机:60 罐饮料,一口没喝上
前端·agent·claude