普通视图

发现新文章,点击刷新页面。
昨天 — 2025年10月14日首页

Vue 3 组合式函数(Composables)全面解析:从原理到实战

作者 90后晨仔
2025年10月13日 22:11

一、前言

当 Vue 3 发布时,组合式 API(Composition API) 带来了一个革命性的变化:

我们不再需要依赖 data、methods、computed 这些分散的选项,而是能用函数的方式,灵活组织逻辑。

这套函数化逻辑复用方案,就叫做 组合式函数(Composables)

简单来说:

  • Options API 更像是“配置式”;

  • Composition API 则让我们“像写逻辑一样组织组件”。

组合式函数(Composables) ,就是在这个新体系下,用于封装和复用有状态逻辑的函数。


二、什么是组合式函数?

先来看一句官方定义:

“组合式函数是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。”

也就是说,它不仅可以处理计算逻辑、请求接口、事件监听,还能和组件生命周期绑定,并且是响应式的。

按照惯例,我们命名时一般以 use 开头

// useXxx 组合式函数命名惯例
export function useMouse() { ... }
export function useFetch() { ... }
export function useEventListener() { ... }

三、基础示例:从组件逻辑到组合式函数

假设我们要做一个“鼠标追踪器”,实时显示鼠标位置。

如果直接写在组件里,可能是这样 👇

<!-- MouseComponent.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

// 定义响应式状态
const x = ref(0)
const y = ref(0)

// 事件处理函数:更新坐标
function update(e) {
  x.value = e.pageX
  y.value = e.pageY
}

// 生命周期绑定
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>
  鼠标坐标:{{ x }}, {{ y }}
</template>

很好,但如果我们多个页面都要复用这个逻辑呢?

那就应该把它抽出来!


四、封装成组合式函数

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 约定:组合式函数以 use 开头
export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  // 内部逻辑:跟踪鼠标移动
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 生命周期钩子
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 返回需要暴露的状态
  return { x, y }
}

使用起来非常简单:

<!-- MouseComponent.vue -->
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>

<template>
  鼠标坐标:{{ x }}, {{ y }}
</template>

✅ 这样写的好处是:

  • 组件逻辑更清晰;
  • 多处可复用;
  • 生命周期自动关联;
  • 每个组件都拥有独立的状态(互不干扰)。

五、进阶封装:useEventListener

假如我们还想监听滚动、键盘等事件,可以进一步抽象出一个事件监听函数 👇

// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

接着 useMouse 就能进一步简化:

// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  useEventListener(window, 'mousemove', (e) => {
    x.value = e.pageX
    y.value = e.pageY
  })

  return { x, y }
}

💡 这样我们不仅复用了逻辑,还建立了逻辑的“组合关系” ——

组合式函数可以嵌套调用另一个组合式函数


六、异步场景:useFetch 示例

除了事件逻辑,我们常常需要封装“异步请求逻辑”,比如:

// fetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err))

  return { data, error }
}

使用方式:

<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('https://api.example.com/posts')
</script>

<template>
  <div v-if="error">❌ 出错:{{ error.message }}</div>
  <div v-else-if="data">✅ 数据:{{ data }}</div>
  <div v-else>⏳ 加载中...</div>
</template>

七、响应式参数:动态请求的 useFetch

上面 useFetch 只会执行一次,

但如果我们希望在 URL 改变时自动重新请求呢?

就可以用 watchEffect() + toValue():

// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    data.value = null
    error.value = null

    fetch(toValue(url)) // 兼容 ref / getter / 普通字符串
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  watchEffect(() => {
    fetchData() // url 改变时会自动重新执行
  })

  return { data, error }
}

使用示例:

<script setup>
import { ref } from 'vue'
import { useFetch } from './fetch.js'

const postId = ref(1)
const { data, error } = useFetch(() => `/api/posts/${postId.value}`)

// 模拟切换文章
function nextPost() {
  postId.value++
}
</script>

<template>
  <button @click="nextPost">下一篇</button>
  <div v-if="error">❌ 出错:{{ error.message }}</div>
  <div v-else-if="data">📰 文章:{{ data.title }}</div>
  <div v-else>⏳ 加载中...</div>
</template>

✅ 这就让你的 useFetch 成为了真正“响应式的请求函数”。


八、组合式函数的使用规范

项目 推荐做法 原因
🧩 命名 useXxx() 一目了然,符合惯例
📦 返回值 返回多个 ref,不要直接返回 reactive 对象 防止解构时丢失响应性
🔁 生命周期 必须在 Vue 需要绑定当前组件实例
⚙️ 参数 建议使用 toValue() 规范化输入 兼容 ref、getter、普通值
🧹 清理 要在 onUnmounted() 清理副作用 避免内存泄漏

九、与其他模式的比较

模式 优点 缺点
Mixins 逻辑复用简单 来源不清晰、命名冲突、隐式依赖
无渲染组件 (Renderless) 可复用逻辑 + UI 会额外创建组件实例,性能差
组合式函数 (Composables) 无实例开销、逻辑清晰、依赖显式 不直接提供模板复用

✅ 结论:

纯逻辑复用 → 用组合式函数

逻辑 + UI 复用 → 用无渲染组件


十、总结

概念 说明
组合式函数 利用 Vue 组合式 API 封装可复用逻辑的函数
核心特性 可使用 ref / reactive / 生命周期钩子 / watch
优势 灵活组合、逻辑清晰、性能优秀、类型友好
常见应用 请求封装、事件监听、滚动追踪、权限控制、表单管理等
开发建议 命名统一、输入规范化、注意生命周期上下文

✨ 最后

Composables 就像是 Vue 世界里的「逻辑积木」——

你可以自由拼接、拆解、组合它们,构建出任何复杂的交互逻辑。

如果你曾觉得逻辑在组件里越堆越乱,

那是时候开始用 组合式函数 让代码“呼吸”了。

Vue 3 中 Provide / Inject 在异步时不起作用原因分析(二)?

作者 90后晨仔
2025年10月13日 21:33

本文是继续上一篇文章《Vue 3 中 Provide / Inject 在异步时不起作用原因分析(一)》

在线查看示例(需要科学上网)

示例源码下载地址:分析demo

🧩 一、核心原理(简单讲人话)

在 Vue3 中:

  • provide 是父组件提供一个依赖值

  • inject 是子组件接收这个依赖值

  • 默认情况下,provide 提供的是一个「普通的引用值」,而不是响应式的。

👉 这意味着:

如果你在父组件中 later(异步)修改了 provide 的值,而这个值不是响应式对象,那么子组件不会自动更新。


🧠 二、最简单示例:静态 provide(不响应)

<!-- App.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <button @click="changeName">修改名字</button>
    <Child />
  </div>
</template>

<script setup>
import { provide } from 'vue'
import Child from './Child.vue'

let username = '小明'

// 向子组件提供 username
provide('username', username)

function changeName() {
  username = '小红'
  console.log('父组件修改了 username =', username)
}
</script>

<!-- Child.vue -->
<template>
  <div>
    <h3>子组件</h3>
    <p>用户名:{{ username }}</p>
  </div>
</template>

<script setup>
import { inject } from 'vue'
const username = inject('username')
</script>

🧩 运行结果:

  • 初始显示:用户名:小明

  • 点击“修改名字”按钮后,子组件界面不会更新

📖 原因:

因为 provide('username', username) 提供的是普通字符串,不具备响应式特性。


✅ 三、扩展版:让 provide 变成响应式的(推荐写法)

要让子组件能「自动响应父组件异步变化」,只需要用 ref 或 reactive 包装即可。

<!-- App.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <button @click="changeName">异步修改名字(2秒后)</button>
    <Child />
  </div>
</template>

<script setup>
import { ref, provide } from 'vue'
import Child from './Child.vue'

const username = ref('小明')

// ✅ 提供响应式的值
provide('username', username)

function changeName() {
  setTimeout(() => {
    username.value = '小红'
    console.log('父组件异步修改 username = 小红')
  }, 2000)
}
</script>

<!-- Child.vue -->
<template>
  <div>
    <h3>子组件</h3>
    <p>用户名:{{ username }}</p>
  </div>
</template>

<script setup>
import { inject } from 'vue'
const username = inject('username') // 自动响应
</script>

🧩 运行结果:

  • 初始显示:用户名:小明

  • 点击按钮后 2 秒 → 自动更新为:用户名:小红

✅ 因为我们注入的是 ref,Vue3 会自动处理 .value 的响应式绑定。


❌ 四、错误示例:异步 provide 失效的情况(常见坑)

有时新手会这么写:

<!-- App.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <button @click="loadData">异步加载 provide 值</button>
    <Child />
  </div>
</template>

<script setup>
import { provide, ref } from 'vue'
import Child from './Child.vue'

let user = null

function loadData() {
  setTimeout(() => {
    user = { name: '异步用户' }
    provide('user', user) // ❌ 错误!在 setup 外部、异步中调用 provide 无效
    console.log('异步 provide 完成')
  }, 2000)
}

provide('user', user)
</script>

<!-- Child.vue -->
<template>
  <div>
    <p>子组件:{{ user }}</p>
  </div>
</template>

<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>

🧩 现象:

  • 初始显示:子组件:null

  • 点击“异步加载”后,依然不变!

📖 原因:

provide 只能在组件 setup() 执行时建立依赖关系,

异步调用 provide() 没有效果,Vue 根本不会重新建立依赖注入。


🔍 五、正确的异步写法总结

场景 错误示例 正确写法
父组件 setup 后再异步修改 普通变量 ✅ 使用 ref 或 reactive
异步中重新调用 provide() ❌ 无效 ✅ 一次 provide 响应式引用即可
想实时共享对象状态 ❌ 普通对象 ✅ 用 reactive() 或 Pinia

🧱 六、总结

类型 响应式 子组件会更新? 推荐
provide('a', 普通变量) ❌ 否 ❌ 否
provide('a', ref()) ✅ 是 ✅ 是
provide('a', reactive()) ✅ 是 ✅ 是
异步重新调用 provide() ❌ 无效 ❌ 否

Vue 3 中 Provide / Inject 在异步时不起作用原因分析(一)?

作者 90后晨仔
2025年10月13日 21:29

一、先搞清楚:Provide / Inject 是什么机制

provide 和 inject 是 Vue 组件之间 祖孙通信的一种机制

它允许上层组件提供数据,而下层组件直接获取,不需要层层 props 传递。

简单关系图:

App.vue (provide)
   └── ChildA.vue
         └── ChildB.vue (inject)

App 通过 provide 提供,ChildB 直接拿到。

在 Vue 3 中:

// 父组件
import { provide } from 'vue'

setup() {
  provide('theme', 'dark')
}
// 孙组件
import { inject } from 'vue'

setup() {
  const theme = inject('theme')
  console.log(theme) // 'dark'
}

这本质上是 Vue 在「组件初始化时」建立的一种依赖注入映射关系(依赖树)


二、误区:为什么“异步”时会失效?

很多人说“在异步组件里 inject 不到值”,其实问题出在「加载时机」上。

❌ 错误理解:

以为 inject 是“运行时全局取值”,随时都能拿到。

✅ 实际原理:

inject() 的查找是在 组件创建阶段(setup 执行时) 完成的。

也就是说:

只有当父组件已经被挂载并执行了 provide() 后,子组件在 setup 时才能拿到。

如果异步加载的子组件在 provide 之前被初始化,或者在懒加载时「上下文丢失」,那它当然拿不到值。


三、可复现测试案例(你可以直接复制运行)

我们写一个最常见的「异步子组件注入」示例。

你可以用 Vite 新建项目,然后建这三个文件:


🟢App.vue(父组件)

<template>
  <div>
    <h2>父组件</h2>
    <p>当前主题:{{ theme }}</p>
    <button @click="loadAsync">加载异步子组件</button>

    <!-- 当点击后才加载 -->
    <component :is="childComp" />
  </div>
</template>

<script setup>
import { ref, provide, defineAsyncComponent } from 'vue'

// 1️⃣ 提供一个响应式值
const theme = ref('🌙 暗黑模式')
provide('theme', theme)

// 2️⃣ 模拟异步组件加载
const childComp = ref(null)
function loadAsync() {
  // 模拟异步加载组件(1 秒后返回)
  const AsyncChild = defineAsyncComponent(() =>
    new Promise(resolve => {
      setTimeout(() => resolve(import('./Child.vue')), 1000)
    })
  )
  childComp.value = AsyncChild
}
</script>

🟡Child.vue(中间组件)

<template>
  <div class="child">
    <h3>中间组件</h3>
    <GrandChild />
  </div>
</template>

<script setup>
import GrandChild from './GrandChild.vue'
</script>

<style scoped>
.child {
  border: 1px solid #aaa;
  margin: 8px;
  padding: 8px;
}
</style>

🔵GrandChild.vue(孙组件)

<template>
  <div class="grand">
    <h4>孙组件</h4>
    <p>从 provide 注入的主题:{{ theme }}</p>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 1️⃣ 注入父级 provide 的数据
const theme = inject('theme', '默认主题')

// 2️⃣ 打印验证
console.log('孙组件注入的 theme 值是:', theme)
</script>

<style scoped>
.grand {
  border: 1px dashed #666;
  margin-top: 8px;
  padding: 6px;
}
</style>

✅ 运行结果验证:

1️⃣ 页面初始只显示父组件。

2️⃣ 点击「加载异步子组件」。

3️⃣ 一秒后加载完成,控制台输出:

孙组件注入的 theme 值是:RefImpl {value: '🌙 暗黑模式'}

页面上显示:

从 provide 注入的主题:🌙 暗黑模式

👉 说明:即使是 异步组件,也能正确拿到 provide 的值。


四、那为什么有时真的“不起作用”?

有三种常见原因:

原因 说明 解决方案
1️⃣ 在 setup 外使用 inject() Vue 只能在组件初始化(setup 阶段)内建立依赖 一定要在 setup() 中调用
2️⃣ 异步组件创建时父组件上下文丢失 如果异步加载组件时没有挂在已有的上下文中(比如 createApp 动态 mount) 保证异步组件是作为「现有组件树」的子节点被渲染
3️⃣ SSR 场景中 hydration 时机问题 如果在服务器端渲染中,provide 未在客户端同步恢复 SSR 需保证 provide/inject 在同一上下文实例中执行

五、底层原理小科普(可选理解)

Vue 内部维护了一棵「依赖注入树」,

每个组件实例在初始化时会记录自己的 provides 对象:

instance.provides = Object.create(parent.provides)

所以当 inject('theme') 时,它会:

  1. 向上查找父组件的 provides;

  2. 找到对应 key;

  3. 返回对应的值(引用)。

这就是为什么:

  • 父子必须在「同一组件树上下文」中;
  • 异步不会破坏注入关系(除非脱离这棵树)。

✅ 总结重点

概念 说明
Provide / Inject 用于祖孙通信的依赖注入机制
异步组件能否注入? ✅ 能,只要仍在同一组件树中
什么时候会失效? 父未先 provide、或异步 mount 独立实例
验证方法 使用 defineAsyncComponent 懒加载组件
推荐做法 始终在 setup 内使用 provide/inject

Vue 异步组件(defineAsyncComponent)全指南:写给新手的小白实战笔记

作者 90后晨仔
2025年10月13日 21:18

在现代前端应用中,性能优化几乎是每个开发者都要面对的课题。

尤其是使用 Vue 构建大型单页应用(SPA)时,首屏加载慢、包体积大 成了常见的痛点。

这时,“异步组件”就登场了。

它能让你把页面拆成小块按需加载,只在用户真正需要时才下载对应的模块,显著减少首屏压力。

这篇文章是写给 刚入门 Vue 3 的开发者 的异步组件实战指南,

我会用简单的语言、可运行的代码和图景化的思维带你彻底搞懂——

defineAsyncComponent 到底做了什么、怎么用、有哪些坑。


一、为什么需要异步组件

🚀 核心动机:提升首屏速度,减少无用资源加载。

想象一个后台系统,首屏只展示“仪表盘”,但你的 bundle 里却打包了“用户管理”、“统计分析”、“设置中心”……

即使用户一天都没点进去,这些模块也会白白加载。

异步组件正是用来解决这种浪费的:

  • 不会被打进主包
  • 只有在组件首次渲染时,才会异步加载真实实现;
  • 这就是所谓的 按需加载 (lazy load)代码分割 (code-splitting)

二、最简单的异步加载:

defineAsyncComponent+import()

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

使用方式完全与普通组件一致:

<template>
  <AsyncComp some-prop="Hello Vue!" />
</template>

解释一下背后的机制:

  • import() 会返回一个 Promise;

  • 打包工具(Vite / Webpack)会自动把它拆成独立的 chunk 文件

  • defineAsyncComponent() 会创建一个“外壳组件”,在内部完成加载逻辑;

  • 一旦加载完成,它会自动渲染内部真正的 MyComponent.vue;

  • 所有 props、插槽、事件 都会被自动透传。

简单来说,它是 Vue 帮你封装好的“懒加载包装器”。


三、加载中 & 加载失败状态:更友好的配置写法

网络总是有延迟或失败的时候,Vue 官方提供了更完善的配置:

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Foo.vue'),
  loadingComponent: LoadingComponent, // 加载中占位
  delay: 200,                          // 多少 ms 后显示 loading
  errorComponent: ErrorComponent,      // 失败时的提示
  timeout: 3000                        // 超时视为失败
})

🧠 要点:

  • delay:默认 200ms,如果加载太快就不显示 loading,防止闪烁;
  • timeout:超过指定时间自动触发错误;
  • loadingComponent / errorComponent 都是普通组件,可以是骨架屏或重试按钮;
  • Vue 会自动处理 Promise 的状态变化。

四、SSR 场景下的新玩法:Hydration 策略(Vue 3.5+)

在服务器端渲染(SSR)场景下,HTML 首屏已经输出,但 JS 模块还没激活。

Vue 3.5 开始支持为异步组件设置「延迟激活策略」:

import { defineAsyncComponent, hydrateOnVisible } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Comp.vue'),
  hydrate: hydrateOnVisible({ rootMargin: '100px' })
})

这意味着:

  • 组件只在滚动到可视区时才激活;

  • SSR 首屏照常渲染,但 hydration(激活)被延后;

  • 从而减少初始脚本执行量,提高 TTI(可交互时间)。

其他常见策略:

策略函数 行为
hydrateOnIdle() 浏览器空闲时激活
hydrateOnVisible() 元素进入视口时激活
hydrateOnMediaQuery() 媒体查询匹配时激活
hydrateOnInteraction('click') 用户交互后激活

你甚至可以自定义策略,在合适时机调用 hydrate() 完成手动激活。


五、搭配

使用,构建优雅的异步界面

是 Vue 专门为异步组件设计的辅助标签,它可以集中控制加载状态与回退界面。

<Suspense>
  <template #default>
    <AsyncComp />
  </template>
  <template #fallback>
    <div>正在努力加载中...</div>
  </template>
</Suspense>

的工作原理:

  • 会等待内部所有异步依赖(包括 defineAsyncComponent)加载完成;
  • 如果有 delay 或网络延迟,会自动显示 fallback 内容;
  • 当所有异步都 resolve 后,才一次性切换到真实内容;
  • 适合并行加载多个异步子组件时使用。

六、实战建议与最佳实践

1. 优先按路由懒加载:

const routes = [
  { path: '/admin', component: () => import('./views/Admin.vue') }
]

这能最大化地减少首包体积。

2. 小组件不建议懒加载:

懒加载有 HTTP 开销,过度拆包反而拖慢渲染。

3. 善用 loadingComponent 做骨架屏:

用灰色框或占位元素代替 spinner,更自然。

4. 设置合理 delay / timeout:

避免闪烁,也要能及时处理网络异常。

5. 支持重试:

function retryImport(path, retries = 3, interval = 500) {
  return new Promise((resolve, reject) => {
    const attempt = () => {
      import(path).then(resolve).catch(err => {
        if (retries-- <= 0) reject(err)
        else setTimeout(attempt, interval)
      })
    }
    attempt()
  })
}

const AsyncComp = defineAsyncComponent(() => retryImport('./Foo.vue', 2))

6. SSR 优化:

配合 hydrateOnVisible / hydrateOnIdle 让页面更快可交互。


七、常见陷阱 Q&A

Q1:defineAsyncComponent 会影响 props 或 slot 吗?

👉 不会,Vue 内部会自动透传所有 props / slot。

Q2:可以全局注册异步组件吗?

👉 可以:

app.component('MyComp', defineAsyncComponent(() => import('./MyComp.vue')))

Q3:delay=0 会怎样?

👉 loading 组件会立刻显示,建议保留短延迟防闪烁。

Q4:如何在 errorComponent 里实现重试?

👉 通过 emit 通知父组件重新渲染异步组件实例即可。


八、完整实战示例

<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingSkeleton from './LoadingSkeleton.vue'
import ErrorBox from './ErrorBox.vue'

const AsyncWidget = defineAsyncComponent({
  loader: () => import('./HeavyWidget.vue'),
  loadingComponent: LoadingSkeleton,
  errorComponent: ErrorBox,
  delay: 200,
  timeout: 5000
})
</script>

<template>
  <section class="dashboard">
    <h2>📊 仪表盘</h2>
    <AsyncWidget />
  </section>
</template>

📌 ErrorBox 可加上「重试」按钮,点击后 emit 事件让父组件重新创建 AsyncWidget 实例即可。


九、总结回顾

要点 说明
defineAsyncComponent() 创建懒加载包装组件
import() 触发动态分包
loadingComponent / errorComponent 优化加载与失败体验
SSR Hydration 策略 控制何时激活异步组件
统一处理异步加载状态
实战建议 只懒加载页面级或大型组件,合理延迟与重试

昨天以前首页

掌握Vue的Provide/Inject:解锁跨层级组件通信的新姿势 🔥

作者 90后晨仔
2025年10月10日 21:52

在Vue应用开发中,组件化是我们的核心思想。但当组件层级越来越深,父子组件间的数据传递就会变得异常繁琐:你可能需要将数据从父组件传递到子组件,再传递给孙组件...如此反复,这就是所谓的"prop逐层传递"(Prop Drilling)问题。幸运的是,Vue提供了provide和inject这两个API,可以让我们优雅地实现跨层级组件通信

一、Provide/Inject是什么?🤔

Provide/Inject是Vue提供的一种依赖注入机制,它允许祖先组件作为依赖提供者,向任意深度的子孙组件注入依赖,而无需经过中间组件。

  • provide:在祖先组件中定义,提供数据或方法
  • inject:在子孙组件中使用,注入祖先提供的数据或方法

类比理解:如果把prop传递比作快递中转(每个中转站都要处理),那provide/inject就像直达空投 - 发货点直接空投到收货点,无视中间所有环节!

二、基本使用方式 🚀

1. 组合式API(Vue 3推荐)

<!-- 祖先组件:提供数据 -->
<script setup>
import { ref, provide } from 'vue'

// 提供静态数据
provide('appName', '我的Vue应用')

// 提供响应式数据
const userInfo = ref({
  name: '张三',
  age: 25
})
provide('userInfo', userInfo)

// 提供方法
const updateUser = (newInfo) => {
  userInfo.value = { ...userInfo.value, ...newInfo }
}
provide('updateUser', updateUser)
</script>
<!-- 子孙组件:注入数据 -->
<script setup>
import { inject } from 'vue'

// 注入数据(基础用法)
const appName = inject('appName')

// 注入数据(带默认值)
const userInfo = inject('userInfo', {})

// 注入方法
const updateUser = inject('updateUser', () => {})

// 使用注入的数据和方法
const handleUpdate = () => {
  updateUser({ age: 26 })
}
</script>

<template>
  <div>
    <h1>{{ appName }}</h1>
    <p>用户名:{{ userInfo.name }}</p>
    <button @click="handleUpdate">更新年龄</button>
  </div>
</template>

2. 选项式API(Vue 2/Vue 3兼容)

// 祖先组件
export default {
  data() {
    return {
      appName: '我的Vue应用',
      userInfo: {
        name: '张三',
        age: 25
      }
    }
  },
  provide() {
    return {
      appName: this.appName,
      userInfo: this.userInfo,
      updateUser: this.updateUser
    }
  },
  methods: {
    updateUser(newInfo) {
      this.userInfo = { ...this.userInfo, ...newInfo }
    }
  }
}
// 子孙组件
export default {
  inject: ['appName', 'userInfo', 'updateUser'],
  
  // 或者使用对象形式指定默认值
  inject: {
    appName: { default: '默认应用名' },
    userInfo: { default: () => ({}) },
    updateUser: { default: () => {} }
  },
  
  methods: {
    handleUpdate() {
      this.updateUser({ age: 26 })
    }
  }
}

三、解决响应式数据问题 💫

重要提醒:默认情况下,provide/inject不是响应式的。如果需要响应性,必须使用ref或reactive:

<script setup>
import { ref, reactive, provide, readonly } from 'vue'

// 响应式对象
const globalState = reactive({
  theme: 'light',
  language: 'zh-CN'
})

// 响应式基本值
const userCount = ref(0)

// 如果需要保护数据不被随意修改,可以使用readonly
provide('globalState', readonly(globalState))
provide('userCount', userCount)

// 提供修改方法,集中管理状态变更
const setTheme = (theme) => {
  globalState.theme = theme
}
provide('setTheme', setTheme)
</script>

四、实战应用:主题切换功能 🎨

让我们通过一个完整的主题切换案例,看看provide/inject的实际价值:

<!-- ThemeProvider.vue:主题提供者 -->
<template>
  <div :class="`theme-${currentTheme}`">
    <slot />
    <button @click="toggleTheme" class="theme-toggle">
      切换主题:{{ currentTheme === 'light' ? '暗黑' : '明亮' }}
    </button>
  </div>
</template>

<script setup>
import { ref, provide, computed } from 'vue'

const currentTheme = ref('light')

const themeConfig = computed(() => ({
  isLight: currentTheme.value === 'light',
  colors: currentTheme.value === 'light' 
    ? { primary: '#007bff', background: '#ffffff', text: '#333333' }
    : { primary: '#4dabf7', background: '#1a1a1a', text: '#ffffff' }
}))

const toggleTheme = () => {
  currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'
}

// 提供主题相关数据和方法
provide('theme', currentTheme)
provide('themeConfig', themeConfig)
provide('toggleTheme', toggleTheme)
</script>

<style>
.theme-light { background: #f5f5f5; color: #333; }
.theme-dark { background: #333; color: #fff; }
.theme-toggle {
  padding: 10px 20px;
  margin: 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}
</style>
<!-- DeeplyNestedComponent.vue:深层嵌套的子孙组件 -->
<template>
  <div class="component" :style="{
    backgroundColor: themeConfig.colors.background,
    color: themeConfig.colors.text
  }">
    <h3>深层嵌套组件</h3>
    <p>当前主题:{{ theme }}</p>
    <button 
      @click="toggleTheme"
      :style="{ backgroundColor: themeConfig.colors.primary }"
    >
      从这里也能切换主题!
    </button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 直接注入主题相关数据,无需中间组件传递
const theme = inject('theme')
const themeConfig = inject('themeConfig')
const toggleTheme = inject('toggleTheme')
</script>

这个案例的亮点:无论组件嵌套多深,都可以直接访问和修改主题,完全跳过了中间组件

五、最佳实践与注意事项 📝

1. 合理使用场景

场景 推荐程度 说明
全局配置(主题、语言) ✅ 强烈推荐 避免层层传递
用户登录信息 ✅ 推荐 多处需要用户数据
表单上下文 ✅ 推荐 复杂表单字段管理
简单父子通信 ❌ 不推荐 使用props更直观
全局状态管理 ⚠️ 谨慎使用 复杂场景用Pinia/Vuex

2. 类型安全(TypeScript)

// keys.ts - 定义注入键名
import type { InjectionKey } from 'vue'

export interface UserInfo {
  name: string
  age: number
}

export const userInfoKey = Symbol() as InjectionKey<UserInfo>
export const themeKey = Symbol() as InjectionKey<string>

// 提供者组件
provide(userInfoKey, { name: '张三', age: 25 })

// 注入者组件
const userInfo = inject(userInfoKey)

3. 避免的陷阱

  • 不要滥用:只在真正需要跨层级通信时使用
  • 保持响应性:记得使用ref/reactive包装数据
  • 明确数据流:在大型项目中,过度使用会使数据流难以追踪
  • 提供修改方法:避免直接在注入组件修改数据,通过提供的方法修改

六、与Vuex/Pinia的对比 🤼

特性 Provide/Inject Vuex/Pinia
学习成本
类型安全 需要额外配置 优秀
调试工具 有限 强大
适用规模 中小型应用/组件库 中大型应用
测试难度 简单 中等

选择建议:组件库开发和中型应用用provide/inject,大型复杂应用用Pinia/Vuex。

七、总结 💎

Provide/Inject是Vue中一个强大的特性,它让我们能够:

  • ✈️ 实现跨层级组件通信,跳过中间环节
  • 🎯 减少props传递,简化组件接口
  • 🔧 提高组件复用性,降低耦合度
  • 💪 灵活处理全局数据,无需引入状态管理

记住:就像任何强大的工具一样,provide/inject需要谨慎使用。在正确的场景下使用它,能让你的Vue应用更加优雅和可维护!

希望这篇指南能帮助你掌握provide/inject,如果有任何问题,欢迎在评论区讨论!🚀

Vue中为什么要有 Provide / Inject?

作者 90后晨仔
2025年10月10日 21:49

provide 和 inject 是 Vue 提供的一对“依赖注入(Dependency Injection) ”机制。

它的作用是:

👉 在跨层级组件之间传递数据

无需通过 props 一层层传递。

简单来说:

  • provide:由上层组件(祖先组件)提供数据。

  • inject:由下层组件(后代组件)接收数据。

适合场景:

当一个数据需要被很多深层子组件使用时(比如主题色、语言、配置对象等),使用 provide/inject 可以避免“多层 props 传递的麻烦”。


🧩 二、基本用法

(1)在祖先组件中提供数据

// App.vue
import { provide } from 'vue'

export default {
  setup() {
    provide('theme', 'dark')
  }
}

(2)在任意后代组件中注入数据

// Child.vue
import { inject } from 'vue'

export default {
  setup() {
    const theme = inject('theme')
    console.log(theme) // 输出: 'dark'
  }
}

🔁 三、响应式数据的注入

provide 默认不会保持响应式

如果你希望数据变化能被子组件自动更新,需要使用 ref 或 reactive:

// 父组件
import { ref, provide } from 'vue'

export default {
  setup() {
    const theme = ref('light')
    provide('theme', theme)
    return { theme }
  }
}
// 子组件
import { inject } from 'vue'

export default {
  setup() {
    const theme = inject('theme')
    return { theme }
  }
}

✅ 这样当父组件修改 theme.value 时,子组件会自动响应更新。


🧠 四、默认值的使用

如果注入的 key 在上层没有提供,inject 会返回 undefined。

为了安全,可以给它设置一个默认值:

const theme = inject('theme', 'light')

或者传入一个函数返回默认值:

const theme = inject('theme', () => 'light')

🧮 五、在选项式 API 中使用

对于使用 data、methods 的老写法,也能用:

export default {
  provide() {
    return {
      theme: 'dark'
    }
  },
  inject: ['theme']
}

⚙️ 六、进阶:修改注入的数据

子组件如果想修改祖先组件提供的数据,要注意:

  • 如果父组件提供的是普通值(非响应式),子组件改不了。

  • 如果父组件提供的是 ref 或 reactive 对象,子组件可以修改。

例如:

// 父组件
const user = reactive({ name: 'Tom' })
provide('user', user)

// 子组件
const user = inject('user')
user.name = 'Jerry' // ✅ 可以修改

🧩 七、和 Props 的区别

对比项 Props Provide / Inject
用途 父 → 子通信 祖先 → 任意后代
层级 只能相邻组件 可跨多层级
响应式 默认响应式 需手动包裹 ref / reactive
使用场景 一般父子通信 全局配置、依赖共享(如主题、国际化)

🌈 八、实际应用场景举例

  1. 主题系统(Theme)

    顶层组件提供主题信息,子组件统一读取。

  2. 表单组件库

    Form 组件通过 provide 向所有子 Input 组件共享表单上下文。

  3. 国际化(i18n)

    顶层提供语言配置,任意组件可注入读取当前语言。


🧾 九、总结一句话

provide / inject 是 Vue 组件间的“依赖注入系统”,用于跨层级共享数据。 它的核心思想是“祖先提供,后代注入”,适合场景是避免层层传 props,提高组件复用性。

xcode 16 删除 Provisioning Profiles 文件的有效路径

作者 90后晨仔
2025年9月27日 14:14

最近遇到一个问题需要删除指定的Provisioning Profiles文件的时候发现网上搜索的结果给的一些在终端删除的路径不对。原来查看了xcode目录才发现这个目录已经变的和之前不一样了。所以记录一下吧!

Snip20250927_1.png

xcode 16及以上的Provisioning Profiles路径已经变成如下地址了: ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles

如果需要在终端快速找到Provisioning Profiles文件的位置就用上边的吧!

❌
❌