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 快速参考:简写模式

如果你只需要在 mountedupdated 时执行相同的行为,可以采用简写形式:

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 会触发指令的 beforeUpdateupdated 钩子。我们可以在这些钩子中对比 binding.valuebinding.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严禁 从外部通过指令操作,应使用组件暴露的 MethodsExpose

三、插件

插件是 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
相关推荐
陶甜也2 小时前
3D智慧城市:blender建模、骨骼、动画、VUE、threeJs引入渲染,飞行视角,涟漪、人物行走
前端·3d·vue·blender·threejs·模型
患得患失9492 小时前
【前端websocket】企业级功能清单
前端·websocket·网络协议
落魄江湖行2 小时前
基础篇四 Nuxt4 全局样式与 CSS 模块
前端·css·typescript·nuxt4
禅思院2 小时前
前端性能优化:从"术"到"道"的完整修炼指南
前端·架构·前端框架
叫我一声阿雷吧2 小时前
JS 入门通关手册(43):async/await 原理与异常处理(实战 + 面试,彻底搞懂)
javascript·异常处理·promise·前端面试·async/await·generator·异步编程
架构师老Y3 小时前
003、Python Web框架深度对比:Django vs Flask vs FastAPI
前端·python·django
小陈工6 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
xiaotao13110 小时前
第九章:Vite API 参考手册
前端·vite·前端打包
午安~婉10 小时前
Electron桌面应用聊天(续)
前端·javascript·electron