<?xml version="1.0" encoding="utf-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"><channel><title>code前端网</title><link>https://www.codeqd.com/</link><description>学技术，来code前端开发网</description><item><title>Vue3官方文档明明用reactive，早期提到的observable去哪了？它俩到底有没有区别？</title><link>https://www.codeqd.com/post/20260821986.html</link><description>&lt;p&gt;这个问题最近在前端技术群里刷到好多次——要么是刚学Vue3的人翻到旧教程卡壳，要么是维护老Vue3RC版项目的伙伴疑惑为啥换了名，今天咱们就好好掰扯清楚，顺便给大家理理日常用这俩（或者说现在只用这一个）的正确姿势。&lt;/p&gt;
&lt;h2&gt;从诞生到消失：Vue3 observable的前世今生&lt;/h2&gt;
&lt;p&gt;Vue3在2019年10月到2020年7月这段RC和Beta阶段，确实把用于创建响应式对象的核心API命名为observable，当时的设计思路和Vue2的Vue.observable一致——都是做全局/非组件的响应式数据管理，但Vue3用了Proxy,性能和能力都强太多了。&lt;/p&gt;
&lt;p&gt;那为啥正式版Vue3（3.0.0-rc.14之后到现在的所有稳定版）直接把observable换成reactive了？主要是两个原因：一是和当时官方在推的Composition API里“reactivity（响应式）”核心概念对齐，名字更直白，新手一看就知道是搞响应式对象的；二是避免和RxJS里最常用的observable概念混淆——毕竟前端开发里，很多时候用observable默认指的是RxJS的可观察对象,Vue3官方不想给用户添乱。&lt;/p&gt;
&lt;p&gt;不过这里要划个小重点：&lt;strong&gt;虽然文档和推荐写法里没了observable，但Vue3核心包里还留着它的兼容代码&lt;/strong&gt;！不信你可以在Vue3项目里直接import { observable } from &#039;vue&#039;，用起来和reactive一模一样，但从长远来看，大家最好还是用reactive，兼容代码说不定哪天就会被彻底砍掉（不过目前3.x系列官方没说要删，4.x的话就得等消息了）。&lt;/p&gt;
&lt;h2&gt;reactive和（Vue3里的）observable的本质：都是Proxy封装的壳&lt;/h2&gt;
&lt;p&gt;刚才说了，它俩兼容代码都留着，用起来一模一样，那本质肯定没啥区别，咱们可以去扒一扒Vue3的早期RC源码和现在的3.4源码对比下——早期RC版里，reactive直接就是调用了observable函数，现在的3.4源码里，observable函数直接返回了reactive的调用结果,相当于只是换了个壳子。&lt;/p&gt;
&lt;p&gt;那咱们再说说这个“壳子”里装的核心东西：Proxy，这是Vue3和Vue2响应式原理最大的不同，Vue2的Vue.observable用的是Object.defineProperty，只能监听对象已有的属性读写，新增属性、删除属性、数组下标直接赋值、数组长度修改这些操作都得额外处理，比如用Vue.set、Vue.delete,不然界面不会更新。&lt;/p&gt;
&lt;p&gt;Vue3的reactive（也就是observable）用了ES6的Proxy，直接代理整个对象，不管是已有属性、新增属性、删除属性，还是数组的各种操作，都能监听到，完全不需要额外的API辅助，开发体验提升了不止一点点，不过这里要注意几个Proxy的局限性：
第一，它只能代理对象、数组、Map、Set这些引用类型，不能代理基本数据类型；
第二，如果代理的是嵌套很深的对象，Vue3会采用“懒代理”的方式——只有当你访问到嵌套对象的时候，才会把嵌套对象也变成响应式的；
第三，它不能代理某些特殊的原生对象，比如Date、RegExp这些，如果你要让这些对象的变化触发界面更新，得换个方式处理,比如用ref包裹或者手动监听。&lt;/p&gt;
&lt;h2&gt;那日常开发里，我们应该怎么用reactive？&lt;/h2&gt;
&lt;p&gt;既然observable只是兼容写法，那咱们日常开发就只用reactive就行啦，接下来给大家分享几个实用的小技巧,避免踩坑。&lt;/p&gt;
&lt;h3&gt;什么时候用reactive，什么时候用ref？&lt;/h3&gt;
&lt;p&gt;很多初学者刚学Vue3的时候，会分不清reactive和ref的使用场景,其实很简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你要创建的是引用类型的数据，比如对象、数组、Map、Set，优先用reactive，因为访问属性不需要加.value,写起来更顺手；&lt;/li&gt;
&lt;li&gt;如果你要创建的是基本数据类型的数据，比如数字、字符串、布尔值、null、undefined，必须用ref,因为Proxy代理不了基本类型；&lt;/li&gt;
&lt;li&gt;如果你要创建的是引用类型的数据，但可能会把整个数据换掉，比如一开始是个空对象，后来会赋值成{ a: 1 }，这时候也建议用ref，因为用reactive的话,直接赋值整个对象会丢失响应式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre class=&quot;brush:js;toolbar:false&quot;&gt;import { reactive, ref } from &amp;#39;vue&amp;#39;
// 引用类型，一般用reactive
const user = reactive({
  name: &amp;#39;张三&amp;#39;,
  age: 18,
  hobbies: [&amp;#39;读书&amp;#39;, &amp;#39;跑步&amp;#39;]
})
// 基本类型，必须用ref
const count = ref(0)
// 可能会换掉整个引用的，建议用ref
const currentComponent = ref(null) // 这是ref的另一个常用场景：绑定DOM元素
const dynamicData = ref({})
// 后来换掉整个对象，响应式还在
dynamicData.value = { type: &amp;#39;article&amp;#39;, title: &amp;#39;Vue3 observable详解&amp;#39; }&lt;/pre&gt;
&lt;h3&gt;怎么正确解构reactive对象？&lt;/h3&gt;
&lt;p&gt;刚才说了，reactive只能代理引用类型，如果你直接用ES6的解构赋值去解构reactive对象，解构出来的基本类型属性会丢失响应式,这也是初学者最容易踩的坑之一。&lt;/p&gt;
&lt;p&gt;那怎么解决这个问题呢？有两个常用的方法：
第一个方法是用toRefs把reactive对象的所有属性都转成ref，然后再解构，这样解构出来的属性还是响应式的，不过访问的时候要加.value（如果是在template里用的话，不需要加，Vue3会自动帮你解包）；
第二个方法是用reactive对象本身，不要解构,或者只在不需要响应式的地方解构。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre class=&quot;brush:js;toolbar:false&quot;&gt;import { reactive, toRefs } from &amp;#39;vue&amp;#39;
const user = reactive({
  name: &amp;#39;张三&amp;#39;,
  age: 18
})
// 直接解构，name和age都会丢失响应式
const { name, age } = user
// 用toRefs转换后再解构，name和age还是响应式的
const { name: refName, age: refAge } = toRefs(user)
// 修改refName.value，user.name也会变
refName.value = &amp;#39;李四&amp;#39;&lt;/pre&gt;
&lt;h3&gt;怎么创建全局的响应式数据？&lt;/h3&gt;
&lt;p&gt;在Vue2里，我们可以用Vue.observable创建全局的响应式数据，然后在各个组件里引用，在Vue3里，我们可以用reactive或者ref创建全局的响应式数据，不过更推荐的是用Vue官方提供的Pinia状态管理库，因为Pinia更适合大型项目，有更好的类型支持、更好的调试体验、更容易维护。&lt;/p&gt;
&lt;p&gt;不过如果是小型项目，或者只是想临时做个简单的全局响应式数据管理，用reactive或者ref也是完全可以的，比如我们可以创建一个store.js文件：&lt;/p&gt;
&lt;pre class=&quot;brush:js;toolbar:false&quot;&gt;import { reactive } from &amp;#39;vue&amp;#39;
export const globalStore = reactive({
  count: 0,
  increment() {
    this.count++
  },
  decrement() {
    this.count--
  }
})&lt;/pre&gt;
&lt;p&gt;然后在各个组件里直接import引用就行，修改globalStore的属性或者调用它的方法,所有引用的组件界面都会更新。&lt;/p&gt;
&lt;p&gt;今天咱们聊了Vue3 observable的前世今生，知道了它其实就是reactive的兼容写法，本质上没有任何区别；也聊了reactive的核心原理是Proxy，以及Proxy的几个局限性；还聊了日常开发里用reactive的实用小技巧，包括什么时候用reactive什么时候用ref、怎么正确解构reactive对象、怎么创建全局的响应式数据。&lt;/p&gt;
&lt;p&gt;最后再提醒大家一句：&lt;strong&gt;虽然Vue3核心包里还留着observable的兼容代码，但从长远来看，大家最好还是用reactive&lt;/strong&gt;,避免以后升级Vue版本的时候出现问题。&lt;/p&gt;</description><pubDate>Tue, 04 Aug 2026 20:02:24 +0800</pubDate></item><item><title>Vue3 props怎么用更靠谱顺手？新手必懂的避坑进阶攻略</title><link>https://www.codeqd.com/post/20260821985.html</link><description>&lt;p&gt;最近帮团队带了几个刚转Vue3的前端实习生,发现他们一开始都容易把Vue2的props习惯直接套过来，要么踩了单向数据流的坑，要么浪费了Vue3新增的特性，要么写出来的代码维护起来特别麻烦，其实只要摸透Vue3 props的底层逻辑和新增用法，完全可以做到既规范又高效，今天就把我整理的实操经验和避坑点分享给大家，不管是刚入门还是想升级代码质量的开发者都能用得上。&lt;/p&gt;
&lt;h2&gt;新手入门第一步：明确Vue3 props的4个核心规则&lt;/h2&gt;
&lt;p&gt;很多人一开始用Vue3 props的时候出错，根本原因就是没搞清楚它的底层设计，Vue官方团队之所以这么设计，就是为了让数据流更清晰，组件更独立可复用。
第一个核心规则是&lt;strong&gt;严格的单向数据流&lt;/strong&gt;——只能父组件通过props传值给子组件，子组件不能直接修改接收到的props，实习生最常犯的错就是直接在子组件里用v-model绑props，或者直接写&lt;code&gt;this.title = &#039;新标题&#039;&lt;/code&gt;这种代码（虽然Vue3用setup不用this，但直接改ref绑定的props属性值也是不行的），如果子组件真的需要修改数据怎么办？有两种合法途径：要么给子组件传一个触发父组件更新的自定义事件，比如子组件emit(&#039;updateTitle&#039;, &#039;新值&#039;)，父组件那边用&lt;code&gt;@updateTitle=&quot;val =&amp;gt; title = val&quot;&lt;/code&gt;或者直接用v-model的语法糖；要么在子组件内部把props转成一个本地的响应式副本，比如用&lt;code&gt;const localTitle = ref(props.title)&lt;/code&gt;，不过这种方法要注意如果父组件的props值变了，localTitle会不会同步更新，需要的话可以用watch或者computed。
第二个核心规则是&lt;strong&gt;props声明越规范越安全&lt;/strong&gt;——Vue3虽然允许直接用字符串数组声明props，比如&lt;code&gt;props: [&#039;title&#039;, &#039;count&#039;]&lt;/code&gt;，但强烈不推荐这么做，没有类型校验、默认值和是否必填的声明，不仅开发时IDE没法给你提示，运行时也容易出现类型错误，比如传了一个字符串给需要数字的props，导致计算逻辑全错，Vue3支持用对象声明props，可以设置type、required、default、validator这些属性，完全能满足复杂场景的需求。
第三个核心规则是&lt;strong&gt;props是响应式的，但不是深度响应式的？不对，是要分情况&lt;/strong&gt;——在Vue3里，如果你给子组件传的是一个普通的引用类型（比如对象、数组）的字面量，父组件直接修改这个引用类型的内部属性，子组件的props是会自动更新的，因为Vue3的响应式系统是基于Proxy的，默认会深度监听对象和数组的变化；但如果父组件是直接替换这个引用类型的整个值，子组件的props当然也会更新，不过这里有个新手容易忽略的点：如果父组件传的是一个computed属性的结果，或者是通过props传过来再给子组件传的，响应式逻辑会不会正常？其实只要父组件那边的数据源是响应式的，子组件接收到的props就会是响应式的，不用额外做什么处理。
第四个核心规则是&lt;strong&gt;Boolean类型的props有特殊处理&lt;/strong&gt;——比如父组件给子组件传了&lt;code&gt;&amp;lt;MyButton disabled /&amp;gt;&lt;/code&gt;，那子组件接收到的disabled就是true；如果父组件传了&lt;code&gt;&amp;lt;MyButton disabled=&quot;false&quot; /&amp;gt;&lt;/code&gt;，那接收到的还是true，因为字符串“false”在Boolean类型校验下会被转换成true，如果要传false给Boolean类型的props，要么不传这个属性，要么传&lt;code&gt;disabled=&quot;false&quot;&lt;/code&gt;，要么在props声明里给default设为true/false以外的值？不对，Boolean类型的default只能是true或者false，哦对，必须用v-bind绑定才能传动态的布尔值，包括false。&lt;/p&gt;
&lt;h2&gt;避坑进阶篇：解决新手最常遇到的6个Vue3 props问题&lt;/h2&gt;
&lt;p&gt;刚才说了核心规则,现在来聊聊实际开发中最容易踩的坑，以及怎么用Vue3的新增特性优化props的使用。&lt;/p&gt;
&lt;h3&gt;第一个坑：直接修改引用类型的props&lt;/h3&gt;
&lt;p&gt;刚才提到过直接修改props的字面量（不管是基本类型还是引用类型的替换）会报错，但直接修改引用类型的内部属性不会报错，可这依然是违反单向数据流的！为什么呢？因为如果多个子组件共用同一个父组件传下来的对象，其中一个子组件修改了内部属性，其他子组件都会受影响，数据流就乱了，调试起来特别麻烦。
那合法的处理方式有哪些？刚才说了两种，这里再补充一种——如果子组件修改的是对象的某个具体属性，父组件又不想每次都写自定义事件，可以给子组件传这个属性的单独引用，或者用Vue3新增的&lt;strong&gt;v-model绑定多个属性&lt;/strong&gt;的特性，比如父组件可以写&lt;code&gt;&amp;lt;UserForm v-model:name=&quot;user.name&quot; v-model:age=&quot;user.age&quot; /&amp;gt;&lt;/code&gt;，子组件那边只需要分别emit(&#039;update:name&#039;, &#039;新名字&#039;)和emit(&#039;update:age&#039;, 20)就行，比单独写多个自定义事件简洁多了。&lt;/p&gt;
&lt;h3&gt;第二个坑：设置引用类型props的default值时出错&lt;/h3&gt;
&lt;p&gt;很多新手会这么写引用类型的默认值：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;props: {
  userInfo: {
    type: Object,
    default: { name: &amp;#39;未知&amp;#39;, age: 18 }
  }
}&lt;/pre&gt;
&lt;p&gt;这样写看起来没问题,但实际上如果有多个地方用到这个子组件，而且都没有传userInfo这个props，那所有子组件的userInfo都会指向同一个对象！因为JavaScript里的对象是引用传递的，default如果直接写对象字面量，会在组件定义的时候就创建好这个对象，然后所有实例都共用它。
正确的写法是把default写成一个返回对象/数组的函数：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;props: {
  userInfo: {
    type: Object,
    default: () =&amp;gt; ({ name: &amp;#39;未知&amp;#39;, age: 18 })
  },
  tags: {
    type: Array,
    default: () =&amp;gt; []
  }
}&lt;/pre&gt;
&lt;p&gt;这样每个子组件实例都会有一个独立的默认对象/数组，不会互相影响。&lt;/p&gt;
&lt;h3&gt;第三个坑：不知道怎么在setup里解构props而不丢失响应式&lt;/h3&gt;
&lt;p&gt;在Vue3的setup语法糖或者setup函数里,很多人为了方便会直接解构props，&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// 错误示范
const props = defineProps([&amp;#39;title&amp;#39;, &amp;#39;count&amp;#39;])
const { title, count } = props
// 这里如果父组件修改了title或者count，解构出来的title和count不会更新！&lt;/pre&gt;
&lt;p&gt;为什么会这样？因为解构出来的基本类型变量是值传递的，引用类型虽然是引用传递，但如果父组件替换了整个引用类型，解构出来的变量还是指向原来的地址。
那怎么在setup里既能解构props，又能保持响应式？Vue3提供了一个&lt;strong&gt;toRefs&lt;/strong&gt;函数，专门用来解决这个问题：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// 正确示范
const props = defineProps([&amp;#39;title&amp;#39;, &amp;#39;count&amp;#39;])
const { title, count } = toRefs(props)和count都是ref，父组件修改的话会自动更新
// 注意：如果props里的某个属性是可选的，而且父组件没有传，toRefs会把它转换成一个值为undefined的ref，不会报错&lt;/pre&gt;
&lt;p&gt;不过这里有个小细节：如果props里的某个属性是引用类型，而且你只需要修改它的内部属性（虽然不推荐直接改，但有时候为了快速开发可能会这么做），直接用props.userInfo就行，不用toRefs也不用toRef，因为它本身就是响应式的。&lt;/p&gt;
&lt;h3&gt;第四个坑：不知道怎么用PropType泛型给复杂的props类型做校验&lt;/h3&gt;
&lt;p&gt;在Vue2里,给复杂的类型（比如接口类型、联合类型）做校验只能用validator函数，&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// Vue2的写法
props: {
  status: {
    type: String,
    validator: (val) =&amp;gt; [&amp;#39;pending&amp;#39;, &amp;#39;success&amp;#39;, &amp;#39;error&amp;#39;].includes(val)
  },
  userList: {
    type: Array,
    validator: (val) =&amp;gt; val.every(item =&amp;gt; item.id &amp;amp;&amp;amp; item.name)
  }
}&lt;/pre&gt;
&lt;p&gt;这种写法虽然能满足需求,但类型不够明确，IDE没法给你很好的提示，Vue3提供了&lt;strong&gt;PropType&lt;/strong&gt;泛型，可以配合TypeScript给复杂的类型做类型声明和运行时校验：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// Vue3 + TypeScript的写法
import type { PropType } from &amp;#39;vue&amp;#39;
interface User {
  id: number
  name: string
  age?: number
}
defineProps({
  status: {
    type: String as PropType&amp;lt;&amp;#39;pending&amp;#39; | &amp;#39;success&amp;#39; | &amp;#39;error&amp;#39;&amp;gt;,
    required: true
  },
  userList: {
    type: Array as PropType&amp;lt;User[]&amp;gt;,
    default: () =&amp;gt; []
  }
})&lt;/pre&gt;
&lt;p&gt;这样写不仅有运行时的类型校验（比如如果status传了&#039;loading&#039;，控制台会报错），还有TypeScript的编译时类型检查，IDE也会给你自动补全status的可选值和userList里每个item的属性，开发效率和代码安全性都提高了很多。&lt;/p&gt;
&lt;h3&gt;第五个坑：父组件传值时用了v-bind=&quot;obj&quot;但不知道怎么过滤不需要的属性&lt;/h3&gt;
&lt;p&gt;在Vue3里,如果你给子组件传了一个对象，而且用了v-bind=&quot;obj&quot;的语法糖，比如&lt;code&gt;&amp;lt;MyComponent v-bind=&quot;userInfo&quot; /&amp;gt;&lt;/code&gt;，那userInfo里的所有属性都会被当成props或者attrs传给子组件，如果子组件里已经用defineProps声明了需要的属性，那剩下的属性会被自动放到attrs里，不会报错，但有时候我们想明确告诉子组件哪些属性是必须的，哪些是可选的，或者不想让某些属性被当成attrs传给子组件的根元素，怎么办？
Vue3提供了&lt;strong&gt;defineProps&lt;/strong&gt;和&lt;strong&gt;defineAttrs&lt;/strong&gt;两个编译器宏（不需要import就能直接用），可以配合使用来过滤不需要的属性，如果你想让某些属性即使被声明了props，也不要被当成props处理，或者想手动指定哪些属性放到attrs里，可以用&lt;strong&gt;inheritAttrs&lt;/strong&gt;选项，把它设为false，然后在子组件的根元素或者其他元素上手动绑定attrs：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// Vue3 setup语法糖的写法
const props = defineProps([&amp;#39;title&amp;#39;, &amp;#39;count&amp;#39;])
const attrs = defineAttrs()
// inheritAttrs默认是true，如果要设为false，需要在script setup外面单独写
// 或者用 &amp;lt;script setup inherit-attrs=&amp;quot;false&amp;quot;&amp;gt; 这种写法（Vue3.3+支持）&lt;/pre&gt;
&lt;h3&gt;第六个坑：不知道怎么用Vue3.3+新增的defineProps简化写法&lt;/h3&gt;
&lt;p&gt;Vue3.3版本更新了很多实用的特性，其中就包括defineProps的简化写法，在Vue3.3之前，如果你想在setup语法糖里用TypeScript给defineProps做类型声明，有两种方式：一种是刚才说的对象声明+PropType泛型，另一种是类型参数声明，&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// Vue3.3之前的类型参数声明写法
interface Props { string
  count: number
  userInfo?: User
}
const props = defineProps&amp;lt;Props&amp;gt;()
// 这种写法虽然类型更清晰，但没法直接设置默认值，需要用withDefaults编译器宏
const props = withDefaults(defineProps&amp;lt;Props&amp;gt;(), { &amp;#39;默认标题&amp;#39;,
  count: 0,
  userInfo: () =&amp;gt; ({ id: 0, name: &amp;#39;未知&amp;#39; })
})&lt;/pre&gt;
&lt;p&gt;Vue3.3之后，类型参数声明也支持直接设置默认值了，而且是更简洁的写法：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// Vue3.3+的简化写法
interface User {
  id: number
  name: string
  age?: number
}
const props = defineProps&amp;lt;{ string
  count: number
  userInfo?: User
}&amp;gt;()
// 或者单独提取接口
interface Props { string
  count: number
  userInfo?: User
}
const props = defineProps&amp;lt;Props&amp;gt;()
// 如果需要设置默认值，还是要用withDefaults，但可以用更简洁的解构赋值写法
const { title = &amp;#39;默认标题&amp;#39;, count = 0, userInfo = () =&amp;gt; ({ id: 0, name: &amp;#39;未知&amp;#39; }) } = withDefaults(defineProps&amp;lt;Props&amp;gt;(), {})
// 哦不对，刚才的解构赋值写法不太对，withDefaults返回的是一个响应式对象，不能直接解构赋值基本类型的默认值，除非用toRefs包裹：
const props = withDefaults(defineProps&amp;lt;Props&amp;gt;(), { &amp;#39;默认标题&amp;#39;,
  count: 0,
  userInfo: () =&amp;gt; ({ id: 0, name: &amp;#39;未知&amp;#39; })
})
const { title, count, userInfo } = toRefs(props)
// 这样就对了&lt;/pre&gt;
&lt;p&gt;Vue3.3还新增了&lt;strong&gt;defineProps的解构赋值语法糖&lt;/strong&gt;？不对，刚才的是结合toRefs的写法，Vue3.3新增的是&lt;strong&gt;defineEmits的解构赋值语法糖&lt;/strong&gt;和&lt;strong&gt;useSlots、useAttrs的编译器宏简化写法&lt;/strong&gt;，不过和props相关的主要还是defineProps的类型参数声明和withDefaults的配合使用更流畅了。&lt;/p&gt;
&lt;h2&gt;性能优化篇：用这2个技巧让Vue3 props的响应式更高效&lt;/h2&gt;
&lt;p&gt;刚才说了避坑和进阶用法,现在来聊聊性能优化，很多人觉得Vue3的性能已经够好了，不需要优化props，但其实在处理大量数据或者复杂组件的时候，优化props的响应式还是能带来明显的性能提升的。&lt;/p&gt;
&lt;h3&gt;第一个技巧：尽量避免传不必要的props&lt;/h3&gt;
&lt;p&gt;这一点看起来很简单,但很多人在开发的时候容易忽略，比如父组件有一个包含10个属性的对象，传给子组件的时候直接用了v-bind=&quot;obj&quot;，但子组件其实只用到了其中的2个属性，这样做不仅会让子组件的props声明变得混乱，还会让Vue3的响应式系统监听不必要的属性变化，浪费性能。
正确的做法是只传子组件需要的属性，比如&lt;code&gt;&amp;lt;MyComponent :title=&quot;obj.title&quot; :count=&quot;obj.count&quot; /&amp;gt;&lt;/code&gt;，或者用对象的解构赋值来过滤不需要的属性，比如&lt;code&gt;&amp;lt;MyComponent v-bind=&quot;{ title: obj.title, count: obj.count }&quot; /&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;第二个技巧：对于不需要响应式的props，可以用shallowRef或者shallowReactive包裹&lt;/h3&gt;
&lt;p&gt;在Vue3里,默认的响应式系统是深度监听的，也就是不管你传的对象有多少层嵌套，Vue都会把它转换成响应式的，但如果你传的props是一个很大的静态数据（比如一个下拉框的选项列表，不会变化），或者是一个不需要深度监听的对象（比如只需要监听整个对象的替换，不需要监听内部属性的变化），深度监听就会浪费性能。
这时候可以用&lt;strong&gt;shallowRef&lt;/strong&gt;或者&lt;strong&gt;shallowReactive&lt;/strong&gt;包裹父组件里的数据源，然后传给子组件，shallowRef只会监听整个值的替换，不会监听内部属性的变化；shallowReactive只会监听对象第一层属性的变化，不会监听第二层及以上的属性变化。
不过这里有个小细节：如果用shallowRef或者shallowReactive包裹数据源，父组件直接修改内部属性的话，子组件的props不会更新，只有替换整个值的时候才会更新，这一点要注意和业务需求匹配。&lt;/p&gt;
&lt;p&gt;Vue3 props是父子组件通信的核心方式之一，只要摸透了它的4个核心规则，避开了常见的6个坑，再用上2个性能优化技巧，就能写出既规范又高效的代码，Vue3的版本更新很快，尤其是3.3之后新增了很多实用的特性，建议大家多关注官方文档，及时学习新特性，提升自己的开发效率。&lt;/p&gt;
&lt;p&gt;今天的分享就到这里,如果你有其他关于Vue3 props的问题，或者想了解更多Vue3的实用技巧，欢迎在评论区留言讨论。&lt;/p&gt;</description><pubDate>Tue, 04 Aug 2026 14:02:56 +0800</pubDate></item><item><title>Vue3组件传参用props太麻烦？provide还有这些你不知道的用法和坑？</title><link>https://www.codeqd.com/post/20260821984.html</link><description>&lt;p&gt;Vue3开发到现在，很多开发者还是习惯用props一层一层传数据，可一旦项目里组件嵌套超过3层，中间那些“只负责转手不干活”的组件就会把代码堆得乱七八糟——比如祖宗App.vue存个用户登录状态或者电商后台的主题色，要传到商品页里的价格标签（曾曾孙级别），侧边栏、商品列表容器、商品分类子容器、商品卡片都得加个userStatus或themeColor的props，哪怕它们根本不用这个值，这种时候，provide和inject组成的“跨级直达通道”就特别好用，但很多人只是简单会用个provide传静态值、inject接值，其实它还有响应式、注入别名、依赖注入优化、高阶组件配合这些用法，还有踩坑的风险,今天就好好唠唠。&lt;/p&gt;
&lt;h2&gt;Vue3 provide/inject的基础逻辑，和props到底有啥本质区别？&lt;/h2&gt;
&lt;p&gt;很多入门教程会直接说“provide是提供数据，inject是接收数据”，但没讲清楚底层逻辑，导致大家乱用，其实你可以把provide想象成公司的“共享文件库”——不是随便谁都能看到的，只有公司内部的“员工”（也就是当前组件的所有后代组件，包括子、孙、曾孙……不管嵌套多少层）才能申请查看（inject），而props呢，是“一对一指定送件”——必须从某个“寄件人”组件明确指定送到“收件人”子组件，中间不能跳，要是收件人隔得远，中间还得请“邻居组件”（中间层）帮忙转。&lt;/p&gt;
&lt;p&gt;不过这里要注意一个官方文档里提过的核心约束：provide提供的数据，默认不是响应式的！哪怕你传的是ref或reactive，如果不小心修改了引用指向（比如把整个reactive对象重新赋值），后代inject接收到的也不会更新,这一点后面讲坑点的时候会细说。&lt;/p&gt;
&lt;h2&gt;Vue3 provide的第一个进阶用法：响应式传参，到底怎么写才对？&lt;/h2&gt;
&lt;p&gt;很多人写响应式provide容易踩雷，要么直接传ref.value（传了普通值，肯定不响应），要么随便操作reactive对象但没传对，要么没加readonly保护导致数据被后代乱改，下面分两种情况讲&lt;strong&gt;正确的响应式写法&lt;/strong&gt;,以及为什么要用readonly。&lt;/p&gt;
&lt;h3&gt;情况1：提供单个ref响应式数据&lt;/h3&gt;
&lt;p&gt;比如在App.vue里传主题色，这里的主题色可能是用户点击切换的，需要全局更新，直接传整个ref就可以，不能传.value：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// App.vue
import { ref, provide } from &amp;#39;vue&amp;#39;;
const themeColor = ref(&amp;#39;#3b82f6&amp;#39;); // 蓝色主题
provide(&amp;#39;globalTheme&amp;#39;, themeColor);&lt;/pre&gt;
&lt;p&gt;然后后代组件用inject接收，接收的时候还可以给个默认值（比如用户没登录或者全局没设置主题的时候用）：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// 价格标签组件 PriceTag.vue
import { inject } from &amp;#39;vue&amp;#39;;
// 默认值是橙色，防止后代找不到provide
const globalTheme = inject(&amp;#39;globalTheme&amp;#39;, ref(&amp;#39;#f97316&amp;#39;));&lt;/pre&gt;
&lt;p&gt;这时候如果在App.vue里修改&lt;code&gt;themeColor.value = &#039;#ec4899&#039;&lt;/code&gt;，所有后代的globalTheme都会跟着变,完全没问题。&lt;/p&gt;
&lt;h3&gt;情况2：提供多个reactive响应式数据，或者要传方法&lt;/h3&gt;
&lt;p&gt;如果要传一堆相关的响应式数据（比如用户信息：用户名、头像、登录时间，还有修改头像的方法），最好用reactive包成一个对象，然后用readonly把整个对象包起来再provide——这不是多此一举，而是官方推荐的&lt;strong&gt;最佳实践&lt;/strong&gt;，因为如果不包readonly，任何后代组件都能直接修改provide的数据，万一某个小同事写的组件不小心改了全局用户信息,排查起来特别困难。&lt;/p&gt;
&lt;p&gt;用readonly还有个小技巧：如果要传允许后代调用的方法，不用把方法放在reactive里，因为方法本身不需要响应式,直接单独provide或者和readonly对象一起放在一个provide里都可以。&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// App.vue
import { ref, reactive, provide, readonly } from &amp;#39;vue&amp;#39;;
// 用户信息用reactive
const userInfo = reactive({
  name: &amp;#39;&amp;#39;,
  avatar: &amp;#39;&amp;#39;,
  loginTime: &amp;#39;&amp;#39;
});
// 切换头像的方法，允许后代调用
const changeAvatar = (newAvatarUrl) =&amp;gt; {
  userInfo.avatar = newAvatarUrl;
};
// 把整个userInfo变成只读的provide出去，方法单独provide
provide(&amp;#39;readonlyUserInfo&amp;#39;, readonly(userInfo));
provide(&amp;#39;changeAvatar&amp;#39;, changeAvatar);&lt;/pre&gt;
&lt;p&gt;后代组件接收后，只能通过调用changeAvatar来修改头像，直接改&lt;code&gt;readonlyUserInfo.avatar&lt;/code&gt;会在控制台报错,安全多了。&lt;/p&gt;
&lt;h2&gt;Vue3 provide的第二个进阶用法：注入别名、类型保护，适合中大型项目&lt;/h2&gt;
&lt;p&gt;如果你的项目用的是TypeScript，或者项目里有很多provide/inject，容易出现“key写错了”“不知道inject接收到的是什么类型”的问题，这时候可以用&lt;strong&gt;Symbol作为key&lt;/strong&gt;加&lt;strong&gt;类型定义文件&lt;/strong&gt;或者&lt;strong&gt;组合式函数包装&lt;/strong&gt;来解决。&lt;/p&gt;
&lt;h3&gt;组合式函数包装是更通用的做法，不用TypeScript也能用&lt;/h3&gt;
&lt;p&gt;为什么要用组合式函数？因为Symbol不能直接在不同组件里写一样的，除非单独定义在一个文件里导出，而组合式函数可以把Symbol的定义、provide的逻辑、inject的逻辑都封装在一起，不仅不会写错key，还能统一管理默认值、类型提示（如果用TS），甚至可以加一些额外的逻辑,比如inject找不到provide的时候给个警告。&lt;/p&gt;
&lt;p&gt;比如刚才的全局主题切换，可以封装成一个叫&lt;code&gt;useGlobalTheme&lt;/code&gt;的组合式函数，放在&lt;code&gt;composables/useGlobalTheme.js&lt;/code&gt;里：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// composables/useGlobalTheme.js
import { ref, provide, inject, readonly } from &amp;#39;vue&amp;#39;;
// 用Symbol作为唯一的key，避免和其他组件的provide key冲突
const GLOBAL_THEME_KEY = Symbol(&amp;#39;globalTheme&amp;#39;);
const GLOBAL_THEME_CHANGE_KEY = Symbol(&amp;#39;globalThemeChange&amp;#39;);
// 这个函数是给“提供数据的组件”（比如App.vue）用的
export function provideGlobalTheme(initialTheme = &amp;#39;#3b82f6&amp;#39;) {
  const themeColor = ref(initialTheme);
  const changeTheme = (newColor) =&amp;gt; {
    themeColor.value = newColor;
  };
  // 把key和数据/方法绑定在一起，别人用组合式函数的inject部分就不会写错
  provide(GLOBAL_THEME_KEY, readonly(themeColor));
  provide(GLOBAL_THEME_CHANGE_KEY, changeTheme);
}
// 这个函数是给“接收数据的组件”用的
export function useGlobalTheme() {
  const themeColor = inject(GLOBAL_THEME_KEY);
  const changeTheme = inject(GLOBAL_THEME_CHANGE_KEY);
  // 加个简单的警告，防止接收组件的父级没有调用provideGlobalTheme
  if (!themeColor || !changeTheme) {
    throw new Error(&amp;#39;useGlobalTheme必须在调用了provideGlobalTheme的组件后代中使用！&amp;#39;);
  }
  return {
    themeColor,
    changeTheme
  };
}&lt;/pre&gt;
&lt;p&gt;然后App.vue里直接调用&lt;code&gt;provideGlobalTheme()&lt;/code&gt;,不用管key是什么：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// App.vue
import { provideGlobalTheme } from &amp;#39;./composables/useGlobalTheme&amp;#39;;
// 可以传初始值，也可以不传
provideGlobalTheme(&amp;#39;#ec4899&amp;#39;);&lt;/pre&gt;
&lt;p&gt;后代组件里直接&lt;code&gt;useGlobalTheme()&lt;/code&gt;：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// PriceTag.vue
import { useGlobalTheme } from &amp;#39;../composables/useGlobalTheme&amp;#39;;
// 直接解构，不用默认值也不怕，因为组合式函数里有警告
const { themeColor, changeTheme } = useGlobalTheme();&lt;/pre&gt;
&lt;p&gt;这种写法不仅规范，还能避免很多低级错误,适合中大型团队协作的项目。&lt;/p&gt;
&lt;h2&gt;Vue3 provide的第三个进阶用法：配合Teleport、Suspense、高阶组件，实现更灵活的功能&lt;/h2&gt;
&lt;p&gt;很多人以为provide只能在普通的父子嵌套里用，其实它和Vue3的很多新特性都能配合，比如Teleport、Suspense，还有Vue2就有的高阶组件（HOC）。&lt;/p&gt;
&lt;h3&gt;配合Teleport使用，解决模态框等“飞出去”的组件传参问题&lt;/h3&gt;
&lt;p&gt;Vue3的Teleport可以把组件的DOM结构“传送”到&lt;code&gt;body&lt;/code&gt;或者其他指定的DOM元素里，但组件的逻辑还是属于原来的父组件树的，所以provide/inject依然能用——比如你在一个嵌套很深的按钮组件里用Teleport传了一个模态框，模态框需要用到全局主题色，直接用刚才的&lt;code&gt;useGlobalTheme()&lt;/code&gt;就行，完全没问题,不用再通过props或者事件总线。&lt;/p&gt;
&lt;h3&gt;配合高阶组件使用，实现功能注入&lt;/h3&gt;
&lt;p&gt;高阶组件简单来说就是“接收一个组件作为参数，返回一个增强后的组件”的函数，Vue3里用组合式函数虽然也能实现很多功能，但有些时候高阶组件配合provide/inject会更方便——比如你要给一批组件都注入“全局主题色”和“修改主题的方法”，不用每个组件都写&lt;code&gt;useGlobalTheme()&lt;/code&gt;，可以写一个高阶组件&lt;code&gt;withGlobalTheme&lt;/code&gt;：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// components/withGlobalTheme.js
import { defineComponent, provideGlobalTheme } from &amp;#39;../composables/useGlobalTheme&amp;#39;;
export function withGlobalTheme(WrappedComponent, initialTheme = &amp;#39;#3b82f6&amp;#39;) {
  return defineComponent({
    name: `WithGlobalTheme${WrappedComponent.name}`,
    setup(props, { slots }) {
      // 先provide全局主题
      provideGlobalTheme(initialTheme);
      // 然后渲染被包裹的组件
      return () =&amp;gt; slots.default();
    }
  });
}&lt;/pre&gt;
&lt;p&gt;然后直接在App.vue里用这个高阶组件包裹整个应用的内容：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;!-- App.vue --&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;WithGlobalTheme&amp;gt;
    &amp;lt;!-- 整个应用的内容，包括侧边栏、商品列表等 --&amp;gt;
    &amp;lt;router-view /&amp;gt;
  &amp;lt;/WithGlobalTheme&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup&amp;gt;
import { withGlobalTheme } from &amp;#39;./components/withGlobalTheme&amp;#39;;
import { RouterView } from &amp;#39;vue-router&amp;#39;;
const WithGlobalTheme = withGlobalTheme(RouterView);
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这样所有路由对应的组件及其后代，都能直接用&lt;code&gt;useGlobalTheme()&lt;/code&gt;，不用再在App.vue里单独写provide。&lt;/p&gt;
&lt;h2&gt;Vue3 provide的常见坑点，踩过一个你可能就要找半天bug&lt;/h2&gt;
&lt;p&gt;说了这么多好用的用法，接下来必须讲坑点,不然很多人用了之后会踩雷。&lt;/p&gt;
&lt;h3&gt;坑点1：直接传普通值，或者传ref.value，导致数据不响应&lt;/h3&gt;
&lt;p&gt;刚才已经提到过，但很多人还是会犯——比如你在App.vue里写&lt;code&gt;const count = ref(0)&lt;/code&gt;，然后&lt;code&gt;provide(&#039;count&#039;, count.value)&lt;/code&gt;，后代组件接收到的就是普通的0，不管你怎么修改count.value，后代都不会更新。&lt;strong&gt;一定要传整个ref或reactive对象，或者传readonly包裹的整个对象！&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;坑点2：没有加readonly，导致后代组件乱改全局数据&lt;/h3&gt;
&lt;p&gt;这个坑在多人协作的项目里特别常见——比如某个小同事在商品卡片组件里写&lt;code&gt;readonlyUserInfo.avatar = &#039;xxx&#039;&lt;/code&gt;，虽然控制台会报错（如果是开发环境），但如果是生产环境没开严格模式，可能不会报错，但数据还是改不了？不对，等一下，开发环境下Vue会给readonly对象的属性设置setter，修改的时候会直接报错；但生产环境下为了性能，Vue可能不会设置setter，修改的时候会静默失败，不管是报错还是静默失败，都会影响项目的稳定性，所以&lt;strong&gt;所有全局提供的响应式数据，一定要加readonly！&lt;/strong&gt; 允许后代修改的话,单独provide一个方法。&lt;/p&gt;
&lt;h3&gt;坑点3：inject的默认值不能是响应式对象？不对，是默认值如果是对象的话，要注意内存地址&lt;/h3&gt;
&lt;p&gt;刚才讲基础响应式的时候，给&lt;code&gt;globalTheme&lt;/code&gt;的默认值是&lt;code&gt;ref(&#039;#f97316&#039;)&lt;/code&gt;，这是对的，因为每次inject的时候，这个默认值都是新创建的ref吗？不对，等一下，如果直接写&lt;code&gt;inject(&#039;globalTheme&#039;, ref(&#039;#f97316&#039;))&lt;/code&gt;，那么当多个后代组件都找不到provide的时候，它们接收到的ref是同一个内存地址吗？是的，这就会导致一个后代修改默认值的ref.value，其他后代的默认值也会跟着变,这肯定不是我们想要的。&lt;/p&gt;
&lt;p&gt;所以&lt;strong&gt;如果inject的默认值是对象或ref/reactive，要把它写成一个返回默认值的函数&lt;/strong&gt;，这样每次inject的时候,都会创建一个新的内存地址：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// 正确的写法，默认值是函数返回的新ref
const globalTheme = inject(&amp;#39;globalTheme&amp;#39;, () =&amp;gt; ref(&amp;#39;#f97316&amp;#39;));&lt;/pre&gt;
&lt;p&gt;哦对了，这里要注意，Vue3的inject默认值只有当找不到对应的provide的时候才会生效，而且如果默认值是函数的话，不用手动调用,Vue会自动调用它。&lt;/p&gt;
&lt;h3&gt;坑点4：provide的时机不对，导致后代组件inject不到数据&lt;/h3&gt;
&lt;p&gt;provide是在组件的setup函数执行的时候才会生效的，所以如果你的后代组件是在setup函数执行之前就挂载的（比如用了Suspense的fallback组件，或者某些异步组件加载的逻辑不对）,那么后代组件可能会inject不到数据。&lt;/p&gt;
&lt;p&gt;这个坑点的解决方法很简单：&lt;strong&gt;尽量在最顶层的组件（比如App.vue）的setup函数里调用provide&lt;/strong&gt;，这样所有后代组件（包括fallback组件、异步组件）都能inject到数据。&lt;/p&gt;
&lt;h3&gt;坑点5：不要用provide/inject代替所有的props传参&lt;/h3&gt;
&lt;p&gt;很多人学了provide/inject之后，觉得它太好用了，不管什么数据都用它传，这是不对的，官方文档里明确说了，provide/inject是为了&lt;strong&gt;解决深层嵌套的共享数据问题&lt;/strong&gt;，对于父子组件之间的直接传参，还是用props和emits更好——因为props和emits更直观，一眼就能看到组件接收什么数据、触发什么事件,方便维护和调试。&lt;/p&gt;
&lt;p&gt;比如商品卡片组件需要接收商品的id、名称、价格，这些数据都是从商品列表组件直接传过来的，用props就可以，没必要用provide/inject。&lt;/p&gt;
&lt;h2&gt;Vue3 provide/inject到底什么时候用？怎么用才规范？&lt;/h2&gt;
&lt;p&gt;最后给大家总结一下,省得看了这么多忘光了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;什么时候用？&lt;/strong&gt; 当数据需要传给3层以上的后代组件，或者需要传给Teleport“飞出去”的组件，或者需要实现全局共享数据（比如主题色、用户登录状态、国际化语言）的时候用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;怎么用才规范？&lt;/strong&gt; &lt;ul&gt;
&lt;li&gt;尽量用组合式函数封装provide/inject,用Symbol作为唯一的key；&lt;/li&gt;
&lt;li&gt;所有全局提供的响应式数据,一定要加readonly；&lt;/li&gt;
&lt;li&gt;允许后代修改的话,单独provide一个方法；&lt;/li&gt;
&lt;li&gt;inject的默认值如果是对象或ref/reactive,要写成函数；&lt;/li&gt;
&lt;li&gt;尽量在最顶层的组件里调用provide；&lt;/li&gt;
&lt;li&gt;不要用provide/inject代替所有的props传参。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Vue3的provide/inject其实还有很多细节，比如provide的作用域是当前组件的后代，兄弟组件之间不能直接用；比如可以在provide的时候覆盖祖先组件的provide（但最好不要这么做，会导致代码混乱）；比如Vue3.3+新增了&lt;code&gt;injectStrict&lt;/code&gt;函数，找不到provide的时候会直接报错，不用自己在组合式函数里写警告，不过这些细节今天就不展开讲了，大家可以自己去官方文档里看看，或者在实际开发中慢慢摸索。
就到这里，如果你还有其他关于Vue3的问题,欢迎在评论区留言哦！&lt;/p&gt;</description><pubDate>Tue, 04 Aug 2026 08:02:32 +0800</pubDate></item><item><title>Vue3中跨7、8层组件传参总崩，provide inject到底怎么用才不会踩坑又高效？</title><link>https://www.codeqd.com/post/20260821983.html</link><description>&lt;p&gt;日常做企业级Vue项目的时候，你肯定碰过这种场景：登录页存了用户信息，要传到后台管理的侧边栏头像、顶部通知、右侧设置弹窗，中间隔了导航栏、主布局、二级菜单容器、内容区卡片组……一层一层props钻下来，钻到第5层都嫌烦，改个字段名要动七八个组件,谁碰谁挠头。&lt;/p&gt;
&lt;p&gt;这时候很多老Vue2转过来的或者刚学Vue3的新手，第一反应就是用Vuex/Pinia？不对不对，不是所有数据都适合塞状态管理库的——比如某个独立模块的临时主题配置、表单组件内部的子级联动数据、只读的全局常量替代品，用Pinia太“重”，反而增加项目复杂度和维护成本，那有没有更轻量、更灵活的跨层级传参方案？&lt;/p&gt;
&lt;p&gt;当然有，就是Vue3官方主推的provide和inject组合式API或者选项式API，专门解决这种“祖先组件-任意后代组件”的通信问题，不管中间隔了多少层，甚至隔了动态路由、异步组件，都能直接把数据传过去，不过provide inject也不是万能的，踩过坑的人都知道：比如数据变了后代组件不更新、响应式丢失导致表单联动失效、inject拿到的数据是undefined找不到原因……今天就把这些问题一次性说透，连怎么结合Vue3新特性（比如组合式函数、watchEffect、readonly、shallowRef这些）玩出花来都讲。&lt;/p&gt;
&lt;h2&gt;先搞懂基础：provide和inject到底是什么？用组合式和选项式怎么写？&lt;/h2&gt;
&lt;p&gt;别急着看高级用法，先把最基本的东西啃明白,不然踩坑了都不知道从哪改。&lt;/p&gt;
&lt;h3&gt;它们的核心定位&lt;/h3&gt;
&lt;p&gt;官方文档给的解释很简单：&lt;strong&gt;provide（提供）允许祖先组件向所有后代组件注入数据或方法，inject（注入）允许后代组件从最近的提供该数据/方法的祖先组件中拿到，中间的组件可以完全不参与传递&lt;/strong&gt;，和props的“父子单向绑定”“必须显式声明接收”完全不一样，它更像是一条“隐形的管道”，祖先把东西放进去，后代直接掏就行,不用管管道有多长。&lt;/p&gt;
&lt;h3&gt;组合式API（推荐，毕竟Vue3主推setup/script setup）&lt;/h3&gt;
&lt;p&gt;现在大部分新项目都是用script setup语法糖写的吧？那组合式的写法最顺手。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提供方（provide）&lt;/strong&gt;：需要从vue里引入provide函数，第一个参数是“注入键名”（可以是字符串、Symbol，Symbol更安全，避免和第三方库或者全局键名冲突），第二个参数是“要提供的数据或方法”。
比如我们写个用户头像的提供方组件，假设是App.vue：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { ref, provide } from &amp;#39;vue&amp;#39;;
// 用Symbol做键名，防止重名
const USER_AVATAR_KEY = Symbol(&amp;#39;user-avatar&amp;#39;);
// 响应式数据
const userAvatar = ref(&amp;#39;https://picsum.photos/200/200&amp;#39;);
// 提供出去，同时把修改头像的方法也提供给后代（如果有需要的话）
provide(USER_AVATAR_KEY, {
userAvatar,
updateAvatar: (newUrl) =&amp;gt; {
  userAvatar.value = newUrl;
}
});
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注入方（inject）&lt;/strong&gt;：同样从vue里引入inject函数，第一个参数是&lt;strong&gt;和提供方完全一致的键名&lt;/strong&gt;（不能写错大小写，Symbol的话要import同一个实例！这点超级重要，很多新手拿到undefined都是因为键名不一样），第二个参数是&lt;strong&gt;可选的默认值&lt;/strong&gt;（如果找不到对应的提供方，就用默认值，不会报错）。
比如最深层的右侧设置弹窗里的头像预览组件：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { inject } from &amp;#39;vue&amp;#39;;
// 必须import同一个Symbol实例！如果是在另一个文件定义的，比如utils/keys.js里导出的，这里要引入
import { USER_AVATAR_KEY } from &amp;#39;@/utils/keys&amp;#39;;
// 注入数据，给个默认占位头像
const { userAvatar, updateAvatar } = inject(USER_AVATAR_KEY, {
userAvatar: &amp;#39;https://picsum.photos/seed/default-avatar/200/200&amp;#39;,
updateAvatar: () =&amp;gt; console.warn(&amp;#39;没有找到头像提供方&amp;#39;)
});
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
&amp;lt;div class=&amp;quot;avatar-preview&amp;quot;&amp;gt;
  &amp;lt;img :src=&amp;quot;userAvatar&amp;quot; alt=&amp;quot;用户头像&amp;quot;&amp;gt;
  &amp;lt;button @click=&amp;quot;updateAvatar(&amp;#39;https://picsum.photos/seed/new-avatar/200/200&amp;#39;)&amp;quot;&amp;gt;换个头像&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;选项式API（老项目或者习惯Vue2写法的人用）&lt;/h3&gt;
&lt;p&gt;如果项目是Vue2升级过来的，部分组件还在用选项式,provide和inject也能写。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提供方&lt;/strong&gt;：在组件选项里加provide，可以是对象（只能提供静态数据），也可以是返回对象的函数（函数里可以用this拿到组件的data、props等，提供响应式数据或者方法）。&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;export default {
data() {
  return {
    userAvatar: &amp;#39;https://picsum.photos/200/200&amp;#39;
  }
},
methods: {
  updateAvatar(newUrl) {
    this.userAvatar = newUrl;
  }
},
// 必须用函数返回，不然this是undefined，拿不到data和methods
provide() {
  return {
      userAvatar: this.userAvatar,
      updateAvatar: this.updateAvatar
    }
  }
}
}&lt;/pre&gt;
&lt;p&gt;这里要注意&lt;strong&gt;大坑预警1&lt;/strong&gt;：选项式里用provide提供响应式数据的话，直接传this.userAvatar是不行的！因为this.userAvatar是一个普通值（除非你用Vue2的provide+provide/inject的响应式增强，但Vue3里选项式的provide也可以配合ref/reactive用），后代组件拿到的是静态副本，数据变了不会更新，哦对了，Vue3里不管组合式还是选项式，提供的响应式数据本质是传递的“引用”，所以组合式里直接传ref/reactive对象是没问题的,只要后代组件正确解构或者直接用就行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;踩过最多的3个坑，看完就能绕开&lt;/h2&gt;
&lt;h3&gt;坑1：inject拿到的数据是undefined！&lt;/h3&gt;
&lt;p&gt;90%的新手第一次用provide inject都会踩这个坑,原因无非这几个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;键名不匹配&lt;/strong&gt;：组合式里用了Symbol但没有在同一个文件导出引入，字符串键名写错了大小写（比如提供方是&#039;user_avatar&#039;，注入方是&#039;UserAvatar&#039;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供方的组件层级不对&lt;/strong&gt;：provide必须在&lt;strong&gt;想要注入的组件的祖先组件里&lt;/strong&gt;（包括父组件、祖父组件、曾祖父组件……直到根组件），不能在兄弟组件或者后代组件里提供,然后兄弟组件去注入；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组合式里provide放在了异步代码之后&lt;/strong&gt;：比如在script setup里，provide写在了onMounted或者axios请求的.then/.catch里，组件初始化的时候提供的内容还没注册，后代组件 inject 就会找不到；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步组件的时序问题&lt;/strong&gt;：如果注入方是异步组件（用defineAsyncComponent或者路由懒加载），提供方的provide虽然注册了，但异步组件还没挂载，会不会有问题？其实不会，Vue3的provide/inject机制是&lt;strong&gt;后代组件挂载时才去查找最近的提供方&lt;/strong&gt;，只要提供方在后代组件挂载前（包括挂载前的任何钩子函数里，除了onUnmounted之后）完成provide就行，但最好还是放在script setup的顶层或者setup函数的同步代码里。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;怎么排查呢？可以用Vue DevTools！打开DevTools的组件树，选中注入方组件，然后看右侧面板的“Inject”标签，里面会显示所有注入的键名，如果键名是灰色的或者没有对应的提供方，就说明没找到；再选中祖先组件，看“Provide”标签，有没有对应的键名,键名的值对不对。&lt;/p&gt;
&lt;h3&gt;坑2：数据变了后代组件不更新！响应式怎么丢的？&lt;/h3&gt;
&lt;p&gt;这是第二个超级常见的坑,原因分组合式和选项式两种情况：&lt;/p&gt;
&lt;h4&gt;组合式里的响应式丢失&lt;/h4&gt;
&lt;p&gt;组合式里提供的如果是ref/reactive对象，本身是有响应式的，但后代组件如果&lt;strong&gt;错误解构了&lt;/strong&gt;就会丢失：
比如提供方是这样的：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { reactive, provide } from &amp;#39;vue&amp;#39;;
const USER_INFO_KEY = Symbol(&amp;#39;user-info&amp;#39;);
const userInfo = reactive({
  name: &amp;#39;张三&amp;#39;,
  age: 25
});
provide(USER_INFO_KEY, userInfo);
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;后代组件如果这么写：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { inject } from &amp;#39;vue&amp;#39;;
import { USER_INFO_KEY } from &amp;#39;@/utils/keys&amp;#39;;
// 错误写法！直接解构reactive对象会得到普通值，失去响应式
const { name, age } = inject(USER_INFO_KEY);
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这时候修改提供方的userInfo.name，后代组件的name不会变，那怎么解决？
有三个方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不解构，直接用整个reactive对象&lt;/strong&gt;：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;const userInfo = inject(USER_INFO_KEY);
&amp;lt;!-- 模板里直接用userInfo.name --&amp;gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用toRefs把reactive对象转换成响应式的ref对象再解构&lt;/strong&gt;：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { inject, toRefs } from &amp;#39;vue&amp;#39;;
const userInfo = inject(USER_INFO_KEY);
const { name, age } = toRefs(userInfo);&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供方直接提供ref对象的集合&lt;/strong&gt;：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { ref, provide } from &amp;#39;vue&amp;#39;;
const USER_INFO_KEY = Symbol(&amp;#39;user-info&amp;#39;);
const name = ref(&amp;#39;张三&amp;#39;);
const age = ref(25);
provide(USER_INFO_KEY, { name, age });
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;后代组件直接解构就行,因为解构出来的还是ref对象：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;const { name, age } = inject(USER_INFO_KEY);
&amp;lt;!-- 模板里可以直接用name，不用.value，因为Vue3的script setup会自动解包顶层ref --&amp;gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;选项式里的响应式丢失&lt;/h4&gt;
&lt;p&gt;刚才已经提到过一点，选项式里如果用provide的“对象形式”，只能提供静态数据，完全没有响应式；如果用“函数返回对象”的形式，直接传this.xxx（data里的属性）也是不行的，因为这时候传的是data属性的&lt;strong&gt;值副本&lt;/strong&gt;,不是响应式引用。&lt;/p&gt;
&lt;p&gt;怎么解决？Vue3里选项式的provide也可以配合ref/reactive用,或者用computed包裹：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;export default {
  data() {
    return {
      _name: &amp;#39;张三&amp;#39; // 加个下划线表示内部属性，避免直接修改
    }
  },
  computed: {
    userName() {
      return this._name;
    }
  },
  methods: {
    updateUserName(newName) {
      this._name = newName;
    }
  },
  provide() {
    return {
      // 方法1：用computed提供，computed本身是响应式的
      userName: this.userName,
      // 方法2：提供修改方法，让后代组件调用
      updateUserName: this.updateUserName
    }
  }
}&lt;/pre&gt;
&lt;p&gt;不过更推荐Vue3选项式里也用ref/reactive，结合setup语法糖的混合写法（setup可以和选项式共存）：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;export default {
  setup() {
    import { ref, provide } from &amp;#39;vue&amp;#39;;
    const USER_INFO_KEY = Symbol(&amp;#39;user-info&amp;#39;);
    const name = ref(&amp;#39;张三&amp;#39;);
    provide(USER_INFO_KEY, name);
    return {};
  },
  // 其他选项式的代码
  mounted() {
    console.log(&amp;#39;组件挂载了&amp;#39;);
  }
}&lt;/pre&gt;
&lt;h3&gt;坑3：后代组件不小心修改了提供方的原始数据！&lt;/h3&gt;
&lt;p&gt;这个坑虽然不会导致程序报错，但会让数据流向变得混乱——provide inject本来没有明确的“单向绑定”规则，祖先组件可以提供修改方法让后代组件用，但如果后代组件直接修改了提供方的ref/reactive对象的属性，就会出现“谁都能改数据”的情况,后期维护起来根本找不到数据被谁改了。&lt;/p&gt;
&lt;p&gt;怎么防止这种情况？用Vue3提供的&lt;strong&gt;readonly()&lt;/strong&gt; 函数！把提供的响应式数据用readonly包裹一层，后代组件拿到的就是“只读的响应式数据”，如果尝试直接修改，控制台会报警告（开发环境下），生产环境下会静默失败。
比如刚才的用户信息提供方可以改成这样：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { reactive, provide, readonly } from &amp;#39;vue&amp;#39;;
const USER_INFO_KEY = Symbol(&amp;#39;user-info&amp;#39;);
const _userInfo = reactive({ // 加下划线表示内部可修改的原始数据
  name: &amp;#39;张三&amp;#39;,
  age: 25
});
// 提供只读的userInfo，同时提供显式的修改方法
provide(USER_INFO_KEY, {
  userInfo: readonly(_userInfo),
  updateName: (newName) =&amp;gt; {
    _userInfo.name = newName;
  },
  updateAge: (newAge) =&amp;gt; {
    _userInfo.age = newAge;
  }
});
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这样后代组件只能通过提供的updateName和updateAge方法修改数据,数据流向就变得清晰可控了。&lt;/p&gt;
&lt;h2&gt;高级用法来了！结合组合式函数、watchEffect、shallowRef玩出花&lt;/h2&gt;
&lt;p&gt;掌握了基础和避坑技巧，我们可以用provide inject做一些更高级的事情,甚至可以替代部分简单的Pinia状态管理。&lt;/p&gt;
&lt;h3&gt;用法1：封装成独立的组合式函数（useProvideXxx），方便复用&lt;/h3&gt;
&lt;p&gt;如果某个数据（比如全局的临时主题配置、表单组件的子级联动数据）需要在多个模块里被提供和注入，我们可以把provide和inject的逻辑封装成一个独立的组合式函数（composable），放在src/composables文件夹里，这样不用每次都写重复的Symbol、provide、inject代码。
比如封装一个全局临时主题配置的组合式函数useTheme：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// src/composables/useTheme.js
import { ref, provide, inject, readonly } from &amp;#39;vue&amp;#39;;
// 定义唯一的Symbol键名
const THEME_KEY = Symbol(&amp;#39;theme&amp;#39;);
// 默认主题配置
const DEFAULT_THEME = {
  primaryColor: &amp;#39;#1890ff&amp;#39;,
  fontSize: 14,
  darkMode: false
};
// 提供方的函数，只能在祖先组件里调用
export function useThemeProvider() {
  const theme = ref({...DEFAULT_THEME}); // 用扩展运算符创建新对象，避免修改默认值
  // 切换深色模式的方法
  const toggleDarkMode = () =&amp;gt; {
    theme.value.darkMode =!theme.value.darkMode;
  };
  // 修改主题色的方法
  const setPrimaryColor = (color) =&amp;gt; {
    theme.value.primaryColor = color;
  };
  // 提供只读的主题和修改方法
  provide(THEME_KEY, {
    theme: readonly(theme),
    toggleDarkMode,
    setPrimaryColor
  });
  // 可以返回给提供方组件自己用
  return { theme, toggleDarkMode, setPrimaryColor };
}
// 注入方的函数，只能在后代组件里调用
export function useTheme() {
  const themeContext = inject(THEME_KEY);
  // 如果找不到提供方，直接报错（比给默认值更安全，防止忘记在祖先组件里调用useThemeProvider）
  if (!themeContext) {
    throw new Error(&amp;#39;useTheme必须在useThemeProvider的后代组件里调用&amp;#39;);
  }
  return themeContext;
}&lt;/pre&gt;
&lt;p&gt;然后在根组件App.vue里调用useThemeProvider：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { useThemeProvider } from &amp;#39;@/composables/useTheme&amp;#39;;
// 提供方自己也可以用
const { toggleDarkMode } = useThemeProvider();
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;app&amp;quot;&amp;gt;
    &amp;lt;button @click=&amp;quot;toggleDarkMode&amp;quot;&amp;gt;切换深色模式（根组件）&amp;lt;/button&amp;gt;
    &amp;lt;MainLayout /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;在任意后代组件里调用useTheme注入：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { useTheme } from &amp;#39;@/composables/useTheme&amp;#39;;
const { theme, setPrimaryColor } = useTheme();
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;some-component&amp;quot; :style=&amp;quot;{ backgroundColor: theme.darkMode? &amp;#39;#333&amp;#39; : &amp;#39;#fff&amp;#39; }&amp;quot;&amp;gt;
    &amp;lt;p style=&amp;quot;font-size: `${theme.fontSize}px`&amp;quot;&amp;gt;主题色是{{ theme.primaryColor }}&amp;lt;/p&amp;gt;
    &amp;lt;input type=&amp;quot;color&amp;quot; v-model=&amp;quot;tempColor&amp;quot; @change=&amp;quot;setPrimaryColor(tempColor)&amp;quot;&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup&amp;gt;
// 单独加个临时ref存颜色输入框的值，避免直接修改只读的theme
import { ref } from &amp;#39;vue&amp;#39;;
const tempColor = ref(theme.primaryColor);
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这样是不是超级方便？而且代码逻辑清晰，复用性强,还避免了键名冲突的问题。&lt;/p&gt;
&lt;h3&gt;用法2：结合watchEffect/watch监听后代组件的注入数据变化&lt;/h3&gt;
&lt;p&gt;有时候我们需要在提供方或者其他后代组件里，监听某个注入数据的变化，做一些额外的操作（比如保存到本地存储、发送埋点请求、更新其他相关数据），这时候结合watchEffect或者watch就行。
比如刚才的useTheme组合式函数，我们可以在提供方里加个watchEffect,把主题配置自动保存到localStorage里：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// 修改src/composables/useTheme.js里的useThemeProvider函数
import { ref, provide, inject, readonly, watchEffect } from &amp;#39;vue&amp;#39;;
export function useThemeProvider() {
  // 先从localStorage里读取主题配置，如果没有就用默认值
  const savedTheme = localStorage.getItem(&amp;#39;theme&amp;#39;);
  const theme = ref(savedTheme? JSON.parse(savedTheme) : {...DEFAULT_THEME});
  // 切换深色模式的方法
  const toggleDarkMode = () =&amp;gt; {
    theme.value.darkMode =!theme.value.darkMode;
  };
  // 修改主题色的方法
  const setPrimaryColor = (color) =&amp;gt; {
    theme.value.primaryColor = color;
  };
  // 监听theme的变化，自动保存到localStorage
  watchEffect(() =&amp;gt; {
    localStorage.setItem(&amp;#39;theme&amp;#39;, JSON.stringify(theme.value));
  });
  // 提供只读的主题和修改方法
  provide(THEME_KEY, {
    theme: readonly(theme),
    toggleDarkMode,
    setPrimaryColor
  });
  return { theme, toggleDarkMode, setPrimaryColor };
}&lt;/pre&gt;
&lt;p&gt;这样不管是哪个后代组件调用toggleDarkMode还是setPrimaryColor，主题配置都会自动保存到localStorage里,刷新页面也不会丢失。&lt;/p&gt;
&lt;h3&gt;用法3：结合shallowRef/shallowReactive优化性能&lt;/h3&gt;
&lt;p&gt;如果提供的数据是一个&lt;strong&gt;非常大的对象或数组&lt;/strong&gt;（比如后台管理系统的侧边栏菜单数据，可能有几十上百个菜单项），而且我们只需要监听整个对象或数组的替换，不需要监听内部属性的变化，这时候可以用shallowRef/shallowReactive代替ref/reactive，优化性能——因为shallowRef/shallowReactive只会对对象或数组的第一层做响应式处理，内部的属性是普通值，不会被递归监听，减少了内存占用和响应式追踪的开销。
比如封装一个侧边栏菜单的组合式函数useSidebarMenu：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// src/composables/useSidebarMenu.js
import { shallowRef, provide, inject, readonly } from &amp;#39;vue&amp;#39;;
const SIDEBAR_MENU_KEY = Symbol(&amp;#39;sidebar-menu&amp;#39;);
// 默认菜单数据
const DEFAULT_MENU = [
  { id: 1, title: &amp;#39;首页&amp;#39;, path: &amp;#39;/&amp;#39; },
  { id: 2, title: &amp;#39;用户管理&amp;#39;, path: &amp;#39;/user&amp;#39;, children: [
    { id: 21, title: &amp;#39;用户列表&amp;#39;, path: &amp;#39;/user/list&amp;#39; },
    { id: 22, title: &amp;#39;用户权限&amp;#39;, path: &amp;#39;/user/permission&amp;#39; }
  ]}
];
export function useSidebarMenuProvider() {
  // 用shallowRef，因为我们只会替换整个菜单数据，不会修改单个菜单项的内部属性
  const sidebarMenu = shallowRef([...DEFAULT_MENU]);
  // 替换整个菜单数据的方法
  const setSidebarMenu = (newMenu) =&amp;gt; {
    sidebarMenu.value = newMenu;
  };
  provide(SIDEBAR_MENU_KEY, {
    sidebarMenu: readonly(sidebarMenu),
    setSidebarMenu
  });
  return { sidebarMenu, setSidebarMenu };
}
export function useSidebarMenu() {
  const menuContext = inject(SIDEBAR_MENU_KEY);
  if (!menuContext) {
    throw new Error(&amp;#39;useSidebarMenu必须在useSidebarMenuProvider的后代组件里调用&amp;#39;);
  }
  return menuContext;
}&lt;/pre&gt;
&lt;p&gt;这样如果菜单数据有几十上百个菜单项，shallowRef只会对最外层的数组做响应式处理，内部的children对象都是普通值,性能会比ref好很多。&lt;/p&gt;
&lt;h2&gt;什么时候用provide inject？什么时候用Pinia？千万别搞混！&lt;/h2&gt;
&lt;p&gt;很多人分不清provide inject和Pinia的使用场景，随便乱用反而增加维护成本,这里给大家一个简单的判断标准：&lt;/p&gt;
&lt;h3&gt;适合用provide inject的场景&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;跨层级传递只读或仅由少数特定组件修改的数据&lt;/strong&gt;：比如某个独立模块的临时配置、表单组件内部的子级联动数据、全局的UI组件库主题（非用户自定义的固定主题）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨动态路由、异步组件传递数据&lt;/strong&gt;：provide inject不受组件加载时序的影响（只要提供方在后代组件挂载前完成provide）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装可复用的组件库或组件&lt;/strong&gt;：比如封装一个表格组件，需要把表格的分页信息、筛选信息传递给内部的表头组件、表体组件、分页组件，用provide inject比一层一层props钻下来方便得多；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;替代Vue2的$attrs/$listeners（部分场景）&lt;/strong&gt;：Vue3的$attrs/$listeners合并成了$attrs，但如果需要传递很多属性或方法给多层嵌套的内部组件，provide inject更清晰。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;适合用Pinia的场景&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;需要在整个应用的任意组件里共享、修改的数据&lt;/strong&gt;：比如用户登录信息、购物车数据、全局的通知消息；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要持久化的数据&lt;/strong&gt;：虽然provide inject结合localStorage也能做，但Pinia有专门的插件（比如pinia-plugin-persistedstate）,配置起来更简单；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要处理复杂的业务逻辑、异步操作的数据&lt;/strong&gt;：比如用户登录、获取商品列表、提交订单这些异步操作,Pinia的actions专门用来处理这些；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要时间旅行调试（DevTools的Pinia插件）的数据&lt;/strong&gt;：Pinia的DevTools插件可以查看每一次状态变化的历史，回退到之前的状态,调试起来超级方便。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Vue3的provide inject是一个非常轻量、灵活的跨层级传参方案，专门解决“祖先组件-任意后代组件”的通信问题，不管中间隔了多少层都能直接传递，只要掌握了基础写法、避开了常见的3个坑（undefined、响应式丢失、数据流向混乱），再结合组合式函数、watchEffect、shallowRef这些新特性，就能用它做很多事情,甚至可以替代部分简单的Pinia状态管理。&lt;/p&gt;
&lt;p&gt;但要记住，provide inject不是万能的，它和Pinia是互补的关系，不是替代的关系——简单的、局部的跨层级传参用provide inject，复杂的、全局的状态管理用Pinia，这样才能让项目的代码结构更清晰,维护成本更低。&lt;/p&gt;</description><pubDate>Mon, 03 Aug 2026 20:02:52 +0800</pubDate></item><item><title>Pinia和Vuex核心区别到底在哪？Vue3项目直接用Pinia会不会踩坑？</title><link>https://www.codeqd.com/post/20260821982.html</link><description>&lt;p&gt;最近在前端开发社区逛,发现不少刚入手Vue3或者还在犹豫要不要替换Vuex的开发者，对Pinia的态度有点纠结：说它官方推荐吧，但Vuex4也支持Vue3；说它更简洁吧，好像功能又有点不一样？今天就用日常开发的真实场景当引子，把大家最关心的点掰扯清楚，最后再说说直接用Pinia会不会有坑，怎么提前规避。&lt;/p&gt;
&lt;h2&gt;什么是Pinia？先别扯“下一代Vuex”&lt;/h2&gt;
&lt;p&gt;很多教程一上来就给Pinia扣“下一代Vuex”的帽子，其实没必要，先说说它的出身：它是Vue核心团队成员Eduardo San Martin Morote做的状态管理库，2021年年底才被正式纳入Vue官方生态——注意，不是“取代Vuex”，而是给Vue3量身定制了一个更契合的选择，Vuex4现在主要是为了让Vue2转Vue3的项目平滑迁移用的。&lt;/p&gt;
&lt;p&gt;简单理解的话,Pinia就是一个“去掉冗余的Vuex+TypeScript友好到骨子里的轻量状态管理工具”，轻量到什么程度？压缩后只有1KB左右，比Vuex4小了近一半，而且它的API设计，完全贴合Vue3的Composition API（组合式API）思维，不用再纠结什么“Mutations里只能放同步代码，Actions里异步调用Mutations”这种让人绕晕的老规矩。&lt;/p&gt;
&lt;h2&gt;从这5个场景，一眼看出Pinia和Vuex的核心差异&lt;/h2&gt;
&lt;p&gt;光说概念没用,咱们拿几个写代码时天天碰到的场景对比，保证看完就能懂。&lt;/p&gt;
&lt;h3&gt;场景1：创建一个简单的状态管理模块&lt;/h3&gt;
&lt;p&gt;先回忆下Vuex4的写法：你得先定义state、mutations、actions、getters，四个模块缺一不可，哪怕只用到state和getters，也要写个空的mutations或者actions占位，不然结构不对；而且要通过createStore创建实例，挂载到Vue实例的$store上，用的时候要么通过this.$store，要么用useStore钩子，调用时还要加.state、.commit、.dispatch这些前缀，代码量有点多。&lt;/p&gt;
&lt;p&gt;比如做一个“用户登录状态+获取用户名首字母当头像”的模块，Vuex4的代码大概是这样：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// store/user.js（Vuex4写法）
import { createStore } from &amp;#39;vuex&amp;#39;
export default createStore({
  state() {
    return {
      username: &amp;#39;&amp;#39;,
      isLoggedIn: false
    }
  },
  mutations: {
    SET_USERNAME(state, name) {
      state.username = name
    },
    SET_LOGIN_STATUS(state, status) {
      state.isLoggedIn = status
    }
  },
  actions: {
    login({ commit }, { name, password }) {
      // 模拟异步登录
      setTimeout(() =&amp;gt; {
        commit(&amp;#39;SET_USERNAME&amp;#39;, name)
        commit(&amp;#39;SET_LOGIN_STATUS&amp;#39;, true)
      }, 1000)
    }
  },
  getters: {
    avatarInitial: state =&amp;gt; state.username.charAt(0).toUpperCase()
  }
})&lt;/pre&gt;
&lt;p&gt;然后在组件里用useStore：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { useStore } from &amp;#39;vuex&amp;#39;
const store = useStore()
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div v-if=&amp;quot;store.state.isLoggedIn&amp;quot;&amp;gt;
    欢迎 {{ store.state.username }}，您的头像：{{ store.getters.avatarInitial }}
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;看着还行,但如果你用Composition API的写法，这些前缀其实有点啰嗦。&lt;/p&gt;
&lt;p&gt;再看Pinia的写法：模块不再叫“模块”，叫“Store”；你可以用Options API或者Composition API两种风格写Store，完全看你的习惯；第三，没有mutations了！同步异步修改状态都可以直接在Store里写，不需要再通过commit和dispatch中转；第四，getters可以直接写成普通的计算属性（如果用Options风格）或者ref/computed（如果用Composition风格）。&lt;/p&gt;
&lt;p&gt;同样的登录模块,Pinia用Options风格写的话：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// stores/user.js（Pinia Options风格）
import { defineStore } from &amp;#39;pinia&amp;#39;
// 第一个参数是Store的唯一ID，必须全局唯一
export const useUserStore = defineStore(&amp;#39;user&amp;#39;, {
  state: () =&amp;gt; ({
    username: &amp;#39;&amp;#39;,
    isLoggedIn: false
  }),
  // getters对应计算属性，自动缓存
  getters: {
    avatarInitial: (state) =&amp;gt; state.username.charAt(0).toUpperCase()
  },
  // actions可以放同步也可以放异步，修改状态直接用this
  actions: {
    async login({ name, password }) {
      // 模拟异步登录
      await new Promise(resolve =&amp;gt; setTimeout(resolve, 1000))
      this.username = name
      this.isLoggedIn = true
    }
  }
})&lt;/pre&gt;
&lt;p&gt;在组件里用的话,直接解构就行，不用加任何前缀：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { useUserStore } from &amp;#39;@/stores/user&amp;#39;
// 解构时如果想保留响应式，要加storeToRefs
const userStore = useUserStore()
const { username, isLoggedIn, avatarInitial } = storeToRefs(userStore)
const { login } = userStore // actions直接解构，不用加storeToRefs
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div v-if=&amp;quot;isLoggedIn&amp;quot;&amp;gt;
    欢迎 {{ username }}，您的头像：{{ avatarInitial }}
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;是不是明显简洁多了？而且没有了Mutations这个中间层，修改状态的逻辑更清晰，调试起来也方便。&lt;/p&gt;
&lt;h3&gt;场景2：TypeScript的支持程度&lt;/h3&gt;
&lt;p&gt;Vuex4虽然也支持TypeScript,但支持得很“被动”——你得自己写很多类型定义，比如state的类型、mutations的载荷类型、actions的载荷和返回值类型，还得给useStore做泛型包装，不然IDE根本不会提示你有哪些state、getters、actions，写代码全靠记忆，很容易出错。&lt;/p&gt;
&lt;p&gt;比如Vuex4的TypeScript版本得这么写：&lt;/p&gt;
&lt;pre class=&quot;brush:typescript;toolbar:false&quot;&gt;// store/user.ts（Vuex4 TypeScript版本）
import { createStore, Store, useStore as baseUseStore } from &amp;#39;vuex&amp;#39;
import { InjectionKey } from &amp;#39;vue&amp;#39;
// 定义state的类型
interface UserState {
  username: string
  isLoggedIn: boolean
}
// 定义整个Store的类型
interface RootState {
  user: UserState
}
// 定义InjectionKey，这一步很麻烦，而且容易忘
export const key: InjectionKey&amp;lt;Store&amp;lt;RootState&amp;gt;&amp;gt; = Symbol()
// 创建Store
export const store = createStore&amp;lt;RootState&amp;gt;({
  modules: {
    user: {
      state: (): UserState =&amp;gt; ({
        username: &amp;#39;&amp;#39;,
        isLoggedIn: false
      }),
      mutations: {
        SET_USERNAME(state: UserState, name: string) {
          state.username = name
        },
        SET_LOGIN_STATUS(state: UserState, status: boolean) {
          state.isLoggedIn = status
        }
      },
      actions: {
        async login({ commit }, { name, password }: { name: string; password: string }) {
          await new Promise(resolve =&amp;gt; setTimeout(resolve, 1000))
          commit(&amp;#39;SET_USERNAME&amp;#39;, name)
          commit(&amp;#39;SET_LOGIN_STATUS&amp;#39;, true)
        }
      },
      getters: {
        avatarInitial: (state: UserState) =&amp;gt; state.username.charAt(0).toUpperCase()
      }
    }
  }
})
// 包装useStore，让它支持类型提示
export function useStore() {
  return baseUseStore(key)
}&lt;/pre&gt;
&lt;p&gt;就算你这么写了,如果模块多了，还要给每个模块写类型，还要注意命名空间（Vuex4的命名空间TypeScript支持也不太好），简直是噩梦。&lt;/p&gt;
&lt;p&gt;再看Pinia的TypeScript版本,简直是无缝衔接：不管你用Options风格还是Composition风格，IDE都会自动推断所有的类型，根本不需要你手动写InjectionKey、泛型包装这些东西。&lt;/p&gt;
&lt;p&gt;同样的登录模块,Pinia的TypeScript Options风格版本：&lt;/p&gt;
&lt;pre class=&quot;brush:typescript;toolbar:false&quot;&gt;// stores/user.ts（Pinia TypeScript Options风格版本）
import { defineStore } from &amp;#39;pinia&amp;#39;
export const useUserStore = defineStore(&amp;#39;user&amp;#39;, {
  state: () =&amp;gt; ({
    username: &amp;#39;&amp;#39;,
    isLoggedIn: false
  }),
  getters: {
    avatarInitial: (state) =&amp;gt; state.username.charAt(0).toUpperCase() // 自动推断state的类型，getter的返回值也自动推断
  },
  actions: {
    async login({ name, password }: { name: string; password: string }) {
      await new Promise(resolve =&amp;gt; setTimeout(resolve, 1000))
      this.username = name // 自动提示this有username和isLoggedIn属性
      this.isLoggedIn = true
    }
  }
})&lt;/pre&gt;
&lt;p&gt;在组件里用的话,解构出来的变量、调用的actions，IDE都会有完整的提示，写错一个字母立刻报错，开发效率提升不止一点点。&lt;/p&gt;
&lt;h3&gt;场景3：多个模块之间的调用&lt;/h3&gt;
&lt;p&gt;Vuex4多个模块之间调用时,需要注意命名空间（如果开启了的话），而且调用方式很绕：比如A模块要调用B模块的actions，要加root: true，还要注意路径；A模块要读取B模块的state或者getters，也要加rootState或者rootGetters，代码看起来很乱，维护起来也麻烦。&lt;/p&gt;
&lt;p&gt;比如开启命名空间的情况下,A模块调用B模块的actions：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// store/moduleA.js（Vuex4命名空间版本）
export default {
  namespaced: true,
  actions: {
    doSomething({ dispatch, rootState }) {
      // 调用B模块的actions，必须加root: true
      dispatch(&amp;#39;moduleB/doSomethingElse&amp;#39;, null, { root: true })
      // 读取B模块的state
      console.log(rootState.moduleB.someState)
    }
  }
}&lt;/pre&gt;
&lt;p&gt;而Pinia的模块调用就非常简单：不管是在组件里还是在另一个Store里，只要import对应的useXxxStore函数，调用一下拿到实例，就能直接访问它的state、getters、actions，没有任何命名空间的限制，也不用加root: true这种参数。&lt;/p&gt;
&lt;p&gt;比如在store/cart.js里调用store/user.js的isLoggedIn属性：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// stores/cart.js（Pinia多模块调用）
import { defineStore } from &amp;#39;pinia&amp;#39;
import { useUserStore } from &amp;#39;./user&amp;#39;
export const useCartStore = defineStore(&amp;#39;cart&amp;#39;, {
  state: () =&amp;gt; ({
    items: []
  }),
  getters: {
    // 只有登录用户才能看到购物车总价
    totalPrice: (state) =&amp;gt; {
      const userStore = useUserStore()
      if (!userStore.isLoggedIn) return 0
      return state.items.reduce((sum, item) =&amp;gt; sum + item.price * item.quantity, 0)
    }
  }
})&lt;/pre&gt;
&lt;p&gt;是不是清晰多了？每个Store都是独立的实例，想怎么调用就怎么调用。&lt;/p&gt;
&lt;h3&gt;场景4：状态的调试和时间旅行&lt;/h3&gt;
&lt;p&gt;很多人担心Pinia没有Vuex那样的时间旅行调试功能,其实完全不用担心——Pinia官方有自己的DevTools插件，而且和Vue3的DevTools无缝集成，功能比Vuex4的还要强大。&lt;/p&gt;
&lt;p&gt;Pinia的DevTools里,每个Store都是独立的节点，你可以清晰地看到每个Store的state、getters、actions，不用像Vuex那样在一个大的rootState里找半天；Pinia的DevTools支持修改单个Store的状态，支持撤销和重做，也就是时间旅行功能，和Vuex的一模一样；第三，Pinia的DevTools还支持记录每个action的调用参数、返回值、执行时间，这是Vuex4的DevTools没有的功能，调试异步代码的时候特别有用。&lt;/p&gt;
&lt;p&gt;Pinia的调试体验还有一个小细节：当你修改状态的时候，DevTools里会直接显示你修改的是哪个属性，不用像Vuex那样显示commit的mutation的名字和载荷，找问题更直观。&lt;/p&gt;
&lt;h3&gt;场景5：热更新&lt;/h3&gt;
&lt;p&gt;Vuex4的热更新功能也有,但配置起来有点麻烦，尤其是在有命名空间的情况下；而且热更新时，有时候会丢失状态，需要手动刷新页面。&lt;/p&gt;
&lt;p&gt;Pinia的热更新功能是内置的,而且配置非常简单，只需要在main.js或者stores/index.js里加几行代码就行：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// stores/index.js（Pinia热更新配置）
import { createPinia } from &amp;#39;pinia&amp;#39;
const pinia = createPinia()
// 热更新配置
if (import.meta.hot) {
  import.meta.hot.accept(
    [&amp;#39;./user&amp;#39;, &amp;#39;./cart&amp;#39;], // 你要监听热更新的Store文件
    () =&amp;gt; {
      pinia._p.forEach(plugin =&amp;gt; {
        if (plugin.$id) {
          pinia.use(plugin)
        }
      })
    }
  )
}
export default pinia&lt;/pre&gt;
&lt;p&gt;配置好之后,当你修改Store文件时，页面不会刷新，状态也不会丢失，直接就能看到修改后的效果，开发体验非常好。&lt;/p&gt;
&lt;h2&gt;直接用Pinia会不会踩坑？这3个坑提前注意&lt;/h2&gt;
&lt;p&gt;虽然Pinia很好用,但刚入手的话还是可能踩一些小坑，下面说说最常见的3个，以及怎么提前规避。&lt;/p&gt;
&lt;h3&gt;坑1：解构Store时丢失响应式&lt;/h3&gt;
&lt;p&gt;刚才在对比场景1的时候提到过,解构Store的state和getters时，如果不加storeToRefs，就会丢失响应式，修改state的时候组件不会更新，这个坑很多新手都会踩，一定要记住：解构state和getters用storeToRefs，解构actions直接解构就行，因为actions是普通函数，不需要响应式。&lt;/p&gt;
&lt;p&gt;storeToRefs的用法也很简单,从pinia里import出来，然后包裹一下Store实例就行，就像场景1里写的那样。&lt;/p&gt;
&lt;h3&gt;坑2：修改Store的状态时直接替换整个对象&lt;/h3&gt;
&lt;p&gt;虽然Pinia允许直接修改状态,但如果你直接替换整个state对象或者某个对象类型的state属性，就会丢失响应式，因为Vue3的响应式是基于Proxy的，替换整个对象会让Proxy失效。&lt;/p&gt;
&lt;p&gt;比如不要这么写：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;const userStore = useUserStore()
// 错误写法：直接替换整个userInfo对象
userStore.userInfo = { name: &amp;#39;张三&amp;#39;, age: 18 }&lt;/pre&gt;
&lt;p&gt;应该这么写：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;const userStore = useUserStore()
// 正确写法1：逐个修改属性
userStore.userInfo.name = &amp;#39;张三&amp;#39;
userStore.userInfo.age = 18
// 正确写法2：使用Object.assign
Object.assign(userStore.userInfo, { name: &amp;#39;张三&amp;#39;, age: 18 })
// 正确写法3：使用Pinia提供的$patch方法（批量修改更高效）
userStore.$patch({
  userInfo: { name: &amp;#39;张三&amp;#39;, age: 18 }
})&lt;/pre&gt;
&lt;p&gt;这里推荐用$patch方法批量修改状态，因为Pinia会对$patch方法做优化，只会触发一次状态更新，而逐个修改属性会触发多次，性能更好。&lt;/p&gt;
&lt;h3&gt;坑3：在组件的setup外或者非Vue组件里使用Store&lt;/h3&gt;
&lt;p&gt;Pinia的useXxxStore函数,只能在组件的setup函数里、或者Vue的生命周期钩子函数里、或者其他Store里调用，不能在组件的setup外或者非Vue组件里直接调用，否则会报错：“getActivePinia was called with no active Pinia. Did you forget to install pinia?”&lt;/p&gt;
&lt;p&gt;比如不要这么写：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// utils/api.js（非Vue组件，错误写法）
import { useUserStore } from &amp;#39;@/stores/user&amp;#39;
const userStore = useUserStore() // 这里会报错
export function fetchUserOrders() {
  return fetch(`/api/orders?userId=${userStore.userId}`)
}&lt;/pre&gt;
&lt;p&gt;应该这么写：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// utils/api.js（非Vue组件，正确写法）
import { useUserStore } from &amp;#39;@/stores/user&amp;#39;
export function fetchUserOrders() {
  // 在函数内部调用useXxxStore
  const userStore = useUserStore()
  return fetch(`/api/orders?userId=${userStore.userId}`)
}&lt;/pre&gt;
&lt;p&gt;如果是在组件的setup外调用,比如在路由守卫里调用，可以通过router.app获取到Vue实例，然后从实例的$pinia属性里获取到Pinia实例，再调用useXxxStore函数并传入Pinia实例：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// router/index.js（路由守卫，正确写法）
import { createRouter, createWebHistory } from &amp;#39;vue-router&amp;#39;
import { useUserStore } from &amp;#39;@/stores/user&amp;#39;
const router = createRouter({
  history: createWebHistory(),
  routes: [
    // 路由配置
  ]
})
router.beforeEach((to, from, next) =&amp;gt; {
  // 从router.app里获取Pinia实例
  const pinia = router.app?.config.globalProperties.$pinia
  if (pinia) {
    const userStore = useUserStore(pinia)
    if (to.meta.requireAuth &amp;amp;&amp;amp; !userStore.isLoggedIn) {
      next(&amp;#39;/login&amp;#39;)
    } else {
      next()
    }
  } else {
    next()
  }
})
export default router&lt;/pre&gt;
&lt;h2&gt;Vue3项目到底选Pinia还是Vuex？&lt;/h2&gt;
&lt;p&gt;现在应该很清楚了吧？如果你是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;全新的Vue3项目&lt;/strong&gt;：直接选Pinia，不用犹豫，它更简洁、更轻量、TypeScript支持更好、调试体验更好、热更新更方便。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从Vue2转Vue3的老项目&lt;/strong&gt;：如果项目不大，或者你有时间重构，建议直接换成Pinia；如果项目很大，重构成本太高，可以先用Vuex4过渡，等以后有时间再慢慢替换。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;再补充一句：Pinia的学习成本非常低，如果你用过Vuex，半天就能上手；如果你没用过Vuex，直接学Pinia就行，不用再学Vuex那些老规矩。&lt;/p&gt;
&lt;p&gt;现在Pinia已经是Vue3的官方推荐状态管理库了,生态也越来越完善，很多第三方库都已经支持Pinia了，比如vue-router、element-plus、ant-design-vue等等，所以完全不用担心兼容性的问题。&lt;/p&gt;</description><pubDate>Mon, 03 Aug 2026 14:01:49 +0800</pubDate></item><item><title>用Vue3做项目时选Pinia还是Vuex？具体怎么用才顺手？</title><link>https://www.codeqd.com/post/20260821981.html</link><description>&lt;p&gt;接触Vue3做项目的时候，很多人会在选状态管理工具这里卡壳——到底是接着用熟的Vuex4，还是直接上手官方推荐的Pinia？今天就用大白话把这个事儿掰扯清楚，再把Pinia的核心用法、避坑点也一起唠明白。&lt;/p&gt;
&lt;h2&gt;为什么现在做Vue3官方更推Pinia？&lt;/h2&gt;
&lt;p&gt;之前Vuex4虽然能适配Vue3的Composition API，但本质上还是延续的Options API时代的逻辑，比如模块划分太死、mutations只能传单参数、不能直接调试看状态更新路径这些老问题，一个都没解决，而Pinia呢，从一开始就是为Vue3量身定做的，连名字据说还是为了纪念菠萝这种「果实聚在一起的形态——这不就是我们想要的“轻量级、易组合的状态管理工具嘛。&lt;/p&gt;
&lt;p&gt;对比几个硬伤来看，选Pinia的理由真的太明显了，首先是代码量少，写起来爽，Vuex要写state、getters、mutations、actions四个核心属性，Pinia直接把mutations取消了，state直接修改就行？哦不对，是用方法就行？别慌，后面具体说，其次是没有命名空间冲突的烦恼，Vuex模块多了，dispatch或者commit的时候，长串的命名空间嵌套太容易写错，Pinia每个store都是独立的小模块，想调用直接用store名.变量或者store名.方法，清爽得很，还有就是TS支持完美，之前Vuex写TS类型提示的时候，要么要写好多接口，要么用mapStateToProps/mapMutations这种绕死人的辅助函数转，体验一言难尽，Pinia在TS里直接自动推导，变量和方法的类型都能精准出来，再也不用担心打错字或者传参错了IDE半天找不到问题，调试这块，Vue DevTools对Pinia的支持也更友好，能直接看到每个store的修改记录，还能撤销重做,状态管理的安全感拉满。&lt;/p&gt;
&lt;p&gt;还有一个更实在的点是，现在官方的Vue3脚手架、Vite模板，默认推荐的状态管理都是Pinia，不管是新手入门还是找资料还是查问题，资源比Vuex4多太多了，踩坑也容易找到解决方案，除非你之前的老项目还在用Vuex2/3，升级到Vue3不想改太多东西才用Vuex4，不然真的别犹豫,直接Pinia。&lt;/p&gt;
&lt;h2&gt;Pinia的核心三步曲是什么？怎么从零搭建？&lt;/h2&gt;
&lt;p&gt;别听那些说Pinia难的，其实核心就三步：安装配置、定义store、使用store,连小孩搭积木一样简单。&lt;/p&gt;
&lt;h3&gt;第一步：安装配置&lt;/h3&gt;
&lt;p&gt;不管是用Vite还是Vue CLI，安装命令都差不多，我一般用npm的话，就是npm install pinia，用yarn就是yarn add pinia，pnpm更快，pnpm add pinia就行，安装好之后，要在Vue3的入口文件main.js或者main.ts里注册一下，怎么注册呢？先从pinia里引createPinia，然后用app.use(createPinia())，就这么一行,比Vuex还简单。&lt;/p&gt;
&lt;h3&gt;第二步：定义store&lt;/h3&gt;
&lt;p&gt;这是最关键的一步，Pinia定义store有两种方式，一种是Options Store，一种是Setup Store，官方现在更推Setup Store，因为和Vue3的Composition API完全一致，写起来也更顺,更方便复用。&lt;/p&gt;
&lt;p&gt;先说说Setup Store的写法，首先要从pinia里引defineStore，然后给store起个唯一的名字，比如叫useCounterStore，这个名字就是调试的时候在Vue DevTools里能看到的标识，别和其他store重名就行，然后defineStore的第二个参数，就是一个回调函数，和Vue3的setup函数一样，里面可以写ref、reactive、computed、watch这些Vue3原生的响应式数据和方法,最后return出来就行。&lt;/p&gt;
&lt;p&gt;举个最简单的计数器例子，CounterStore.js文件里这么写：
先引defineStore，然后defineStore(&#039;counter&#039;, () =&amp;gt; {
// 定义响应式数据，用ref和Vue3的ref就行，因为Setup Store里自动响应式
const count = ref(0)
// 计算属性用computed
const doubleCount = computed(() =&amp;gt; count.value * 2)
// 定义方法，不管是同步还是异步，都直接写普通函数就行，不需要像Vuex那样分mutations和actions
const increment = () =&amp;gt; {
count.value++
}
const async incrementAsync = async (num = 1) =&amp;gt; {
// 这里可以加个定时器模拟异步请求
await new Promise(resolve =&amp;gt; setTimeout(resolve, 1000))
count.value += num
}
// 最后一定要return出来要暴露给组件或者其他store用的东西
return { count, doubleCount, increment, incrementAsync }
})&lt;/p&gt;
&lt;p&gt;Options Store的写法也提一嘴，和Vuex有点像，但更简化了，不用写mutations，直接用actions或者methods（Setup里的方法）改state就行，不过现在大家都用Composition API了，还是学Setup Store更实用。&lt;/p&gt;
&lt;h3&gt;第三步：使用store&lt;/h3&gt;
&lt;p&gt;使用store也有两种方式，一种是在组件里直接调用defineStore的返回值，另一种是用辅助函数，不过直接调用返回值更灵活,推荐这个。&lt;/p&gt;
&lt;p&gt;在组件里怎么用呢？首先从刚才写的CounterStore.js里引useCounterStore，然后在setup函数或者script setup里直接调用const counterStore = useCounterStore()，然后就可以直接counterStore.count，counterStore.increment()，counterStore.incrementAsync()，counterStore.doubleCount了，连.value都不用写！因为defineStore已经自动解包了ref,这点特别贴心。&lt;/p&gt;
&lt;p&gt;如果是Options API的组件怎么办？也可以用辅助函数，比如mapState、mapActions这些，不过用法和Vuex差不多，但我觉得还是Composition API直接调用方便。&lt;/p&gt;
&lt;h2&gt;Pinia有哪些新手容易踩的坑？怎么避？&lt;/h2&gt;
&lt;p&gt;虽然Pinia简单，但新手还是容易踩几个小坑,提前知道就不会浪费时间。&lt;/p&gt;
&lt;p&gt;第一个坑：忘记return，Setup Store里定义的东西一定要return出来，不然在组件里或者其他store里根本用不了，这点和Vue3的setup函数一模一样，我刚开始写的时候就犯过这个错,找了半天发现没return。&lt;/p&gt;
&lt;p&gt;第二个坑：直接修改store里的整个对象属性，不是整个对象替换的话，直接赋值没问题，但如果是整个对象替换的话，别用普通赋值，要用Vue3的响应式怎么弄，这里就怎么弄，比如用Object.assign，或者展开运算符再赋值，不然就直接替换整个对象，比如counterStore.someObj = { ...counterStore.someObj, newProp: 123 }，这样才会保持响应式，不过一般直接用reactive定义的对象，直接修改属性就行，比如counterStore.someObj.name = &#039;张三&#039;,没问题的。&lt;/p&gt;
&lt;p&gt;第三个坑：用解构赋值store之后响应式丢失，如果在组件里直接解构store，比如const { count, increment } = counterStore，这样count的话，count就会变成普通的数字，失去响应式了，那怎么办？这里可以用toRefs，或者直接用counterStore.count，或者用pinia提供的storeToRefs，哦对，storeToRefs更推荐，因为它只会把state里的响应式数据转成ref，方法还是原来的方法，还保持响应式，比如const { count, doubleCount } = storeToRefs(counterStore)，const { increment } = counterStore,这样就没问题了。&lt;/p&gt;
&lt;p&gt;第四个坑：异步操作的时候，别把异步的逻辑写错，Setup Store里异步操作直接写async/await就行，和普通的Vue3组件里的异步方法一样，不需要像Vuex那样dispatch再commit，这点很方便，但要注意async函数里的错误处理，比如加个try/catch,不然请求失败的话没有提示。&lt;/p&gt;
&lt;p&gt;第五个坑：多个store之间怎么互相调用？这个也很简单，直接在一个store的回调函数里，调用另一个store的defineStore返回值就行，比如在UserStore里要调用CounterStore，就先引useCounterStore，然后在UserStore的方法里调用const counterStore = useCounterStore()，然后就可以用counterStore的东西了，要不要注意初始化时机？不用，Pinia会自动处理的，不用担心循环依赖的问题,这点官方做的比Vuex强多了。&lt;/p&gt;
&lt;h2&gt;Pinia有哪些进阶用法？让你的代码更优雅？&lt;/h2&gt;
&lt;p&gt;学会了基础用法之后，再学几个进阶用法，代码会写得更顺,更易维护。&lt;/p&gt;
&lt;p&gt;第一个进阶用法：持久化store，有时候我们希望页面刷新之后，store里的某些数据还在，比如用户登录状态、购物车数据，这时候就可以用pinia-plugin-persistedstate这个插件，安装配置也很简单，npm install pinia-plugin-persistedstate，然后在main.js里注册pinia的时候，把这个插件加进去就行，然后在定义store的时候，配置一下persist属性，比如在useCounterStore里，return之后，第三个参数是个对象，里面写persist: true，这样刷新页面之后count就不会重置了，如果只想要持久化某些数据，或者换存储方式（比如sessionStorage），也可以配置persist的配置项，比如persist: { storage: sessionStorage, paths: [&#039;count&#039;] }，这样就只会持久化count,用sessionStorage存储。&lt;/p&gt;
&lt;p&gt;第二个进阶用法：action的拦截器，Pinia的action拦截器可以在action执行之前、之后、出错的时候做一些事情，比如记录日志、loading状态管理，比如在定义store的时候，配置一下useCounterStore的第三个参数对象里，写actions: { increment() { console.log(&#039;increment执行前&#039;) }, incrementAsync() { console.log(&#039;incrementAsync执行前&#039;) } }，不过这个是Options Store的写法？不对，Setup Store也可以用pinia的useStore.$onAction方法，或者用插件也行，不过我一般用插件的话更全局统一管理更方便，比如在main.js里注册pinia之后，遍历所有的store,然后给每个store的action都加个拦截器。&lt;/p&gt;
&lt;p&gt;第三个进阶用法：重置store的state，有时候我们希望重置store里的所有state，比如用户退出登录的时候，清空用户信息，Setup Store里怎么重置呢？可以在定义store的时候，return一个reset方法，把所有的state数据都重置成初始值，比如const reset = () =&amp;gt; { count.value = 0, someObj.value = { name: &#039;&#039; }，或者用pinia提供的$reset方法？哦对，Options Store里默认有$reset方法，Setup Store里没有，除非自己写一个，那怎么让Setup Store也有$reset方法？可以用插件，或者在定义store的时候，先把初始值定义成一个函数，然后return的时候，把state从初始值函数里取，然后reset方法就调用初始值函数重新赋值就行。
defineStore(&#039;counter&#039;, () =&amp;gt; {
const getInitialState = () =&amp;gt; ({
count: 0,
someObj: { name: &#039;&#039; }
})
const state = reactive(getInitialState())
const count = computed(() =&amp;gt; state.count)
const someObj = computed(() =&amp;gt; state.someObj)
const reset = () =&amp;gt; {
Object.assign(state, getInitialState())
}
// 其他方法...
return { state, count, someObj, reset }
})&lt;/p&gt;
&lt;h2&gt;总结一下&lt;/h2&gt;
&lt;p&gt;用Vue3做新项目，直接选Pinia就对了，代码量少、TS支持好、调试方便、资源多，核心三步曲：安装配置、定义Setup Store、用storeToRefs避免解构响应式丢失，进阶用法可以学持久化、action拦截器、重置state，多练几个小项目，比如购物车、用户登录、Todo List，很快就能上手了,用Pinia管理状态真的比Vuex爽太多。&lt;/p&gt;</description><pubDate>Mon, 03 Aug 2026 08:01:03 +0800</pubDate></item><item><title>Vue3 Playground除了写demo还有啥用？新手入门和进阶技巧全知道</title><link>https://www.codeqd.com/post/20260821980.html</link><description>&lt;p&gt;你刚接触Vue3,是不是只把官方的Vue3 Playground当成个临时写“Hello World”、快速试错组件语法的小工具？要是这么想，那真的太可惜了——这个看起来界面简单的在线编辑器，其实藏着不少能提升前端开发效率的功能，从新手的语法纠错、API理解，到进阶开发者的跨框架集成预演、SSR/SSG适配测试，甚至还有团队分享原型的场景，今天咱们就用一问一答的方式，把Vue3 Playground的所有实用点挖透。&lt;/p&gt;
&lt;h2&gt;新手入门篇：解决学Vue3时最头疼的3个问题&lt;/h2&gt;
&lt;p&gt;很多新手抱着文档啃Vue3,遇到“setup里怎么拿到props和emit？”“响应式变量到底用ref还是reactive？”“v-model在自定义组件里怎么拆？”这类问题时，要么不敢随便改本地项目的代码怕弄乱，要么得搭半天脚手架太麻烦——这时候Playground就是最好的“训练场”。&lt;/p&gt;
&lt;h3&gt;问题1：刚学的API记不住？会不会随时有官方纠错？&lt;/h3&gt;
&lt;p&gt;记不住Vue3的API太正常了,比如Composition API里的watch有3种写法，provide/inject跨层级传值要不要加readonly？别慌，Playground自带的功能能帮你少走很多弯路。
首先是语法提示和实时纠错：编辑器左侧是代码区，写代码时会像VSCode一样弹出Composition API、组件属性、模板指令的提示列表，按Tab就能补全；要是写错了，比如把ref写成Ref（注意大小写哦，Composition API里的ref函数是小写，类型注解才是大写），或者在setup里直接用this，代码下面会立刻出现红色波浪线，鼠标悬停还能看到具体的错误原因和官方文档的提示入口，点一下就能跳转到对应的API说明页。
其次是实时预览：编辑器右侧是预览区，你改一行代码，预览区的内容大概1秒内就会更新，完全不用等本地项目的热重载（本地脚手架有时候卡的时候，热重载可能要等3、5秒），比如你想试watchEffect的立即执行特性，只要写个简单的例子：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;button @click=&amp;quot;count++&amp;quot;&amp;gt;点我+1&amp;lt;/button&amp;gt;
  &amp;lt;p&amp;gt;当前计数：{{ count }}&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;上次打印时的计数：{{ prevLogCount }}&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup&amp;gt;
import { ref, watchEffect } from &amp;#39;vue&amp;#39;
const count = ref(0)
const prevLogCount = ref(0)
// 这里加个onCleanup看看
watchEffect((onCleanup) =&amp;gt; {
  const prev = prevLogCount.value
  console.log(&amp;#39;watchEffect触发了，当前count是&amp;#39;, count.value)
  onCleanup(() =&amp;gt; {
    console.log(&amp;#39;watchEffect清理前，上次的prevLogCount是&amp;#39;, prev)
    prevLogCount.value = count.value
  })
})
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;改完立刻点按钮,就能在浏览器控制台里看到watchEffect的执行顺序和onCleanup的作用，比对着文档空想强10倍。&lt;/p&gt;
&lt;h3&gt;问题2：本地项目搭起来麻烦？想快速验证组件的兼容性？&lt;/h3&gt;
&lt;p&gt;有时候你在刷技术博客看到别人写的一个Vue3自定义组件,比如一个可拖拽的标签页组件，或者一个带动画的进度条组件，想直接拿来试试效果，或者想改改参数看看会不会变样——这时候Playground能帮你跳过“npm init vite@latest my-vue-app → cd my-vue-app → npm install → 把代码复制进去 → npm run dev”这一大串步骤，直接就能验证。
那第三方组件库怎么在Playground里用呢？其实很简单，Playground支持通过CDN引入常用的UI组件库和工具库，比如Element Plus、Ant Design Vue、Axios、Lodash这些，比如你想试试Element Plus的el-input和el-button，只要在代码区的顶部加个script标签引入CDN就行（不用下载npm包哦）：&lt;/p&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;!-- 引入Element Plus的CSS --&amp;gt;
&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;//unpkg.com/element-plus/dist/index.css&amp;quot; /&amp;gt;
&amp;lt;!-- 引入Vue3 --&amp;gt;
&amp;lt;script src=&amp;quot;//unpkg.com/vue@3/dist/vue.global.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;!-- 引入Element Plus的JS --&amp;gt;
&amp;lt;script src=&amp;quot;//unpkg.com/element-plus/dist/index.full.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;引入之后,就能像在本地项目里一样用Element Plus的组件了，连注册步骤都不用——因为通过CDN引入的Element Plus是全局注册的，对了，要是你想试按需引入的组件库怎么办？比如只引入Element Plus的el-card和el-rate？别担心，Playground也支持ESM的CDN，比如用unpkg或者jsdelivr的ESM版本：&lt;/p&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;!-- 引入Vue3的ESM版本 --&amp;gt;
&amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;
import { createApp, ref } from &amp;#39;//unpkg.com/vue@3/dist/vue.esm-browser.js&amp;#39;
import { ElCard, ElRate } from &amp;#39;//unpkg.com/element-plus/dist/index.esm-browser.js&amp;#39;
import &amp;#39;//unpkg.com/element-plus/dist/index.css&amp;#39;
const app = createApp({
  setup() {
    const score = ref(4)
    return { score }
  },
  template: `
    &amp;lt;el-card shadow=&amp;quot;hover&amp;quot; style=&amp;quot;width: 300px; margin: 20px auto;&amp;quot;&amp;gt;
      &amp;lt;template #header&amp;gt;
        &amp;lt;div class=&amp;quot;card-header&amp;quot;&amp;gt;给这篇文章打个分吧～&amp;lt;/div&amp;gt;
      &amp;lt;/template&amp;gt;
      &amp;lt;el-rate v-model=&amp;quot;score&amp;quot; show-score /&amp;gt;
    &amp;lt;/el-card&amp;gt;
  `
})
// 按需注册组件
app.component(&amp;#39;ElCard&amp;#39;, ElCard)
app.component(&amp;#39;ElRate&amp;#39;, ElRate)
app.mount(&amp;#39;#app&amp;#39;)
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这样就不用加载整个组件库的代码,验证效率更高。&lt;/p&gt;
&lt;h3&gt;问题3：老师/朋友让我看Vue3的某个代码片段？怎么打开？&lt;/h3&gt;
&lt;p&gt;有时候你的Vue3老师在课上会给你分享一个代码链接,或者你的朋友遇到了Vue3的问题，把他写的代码片段发给你——这时候你不用把代码复制到本地项目里，直接用Playground的“导入功能”就能打开。
那怎么导入呢？有三种方式：第一种是直接在Playground的地址栏里输入别人分享的链接；第二种是点击编辑器顶部的“Import”按钮，然后粘贴别人分享的代码、JSON配置或者GitHub仓库的文件路径；第三种是直接把本地项目的.vue文件、.js文件或者.json文件拖到编辑器左侧的文件树里，哦对了，Playground的文件树功能也很实用——你可以像在本地项目里一样创建多个文件，比如创建一个App.vue作为入口文件，创建一个components文件夹放子组件，创建一个utils文件夹放工具函数，这样就能模拟真实的Vue3项目结构，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── App.vue
├── components
│   ├── Counter.vue
│   └── TodoList.vue
├── utils
│   └── formatTime.js
└── index.html&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建好之后,在App.vue里就能像在本地项目里一样导入子组件和工具函数了，完全不用改路径。&lt;/p&gt;
&lt;h2&gt;进阶开发篇：解决进阶开发者的3个痛点&lt;/h2&gt;
&lt;p&gt;如果你已经掌握了Vue3的基础语法,那Playground对你来说就不只是个“训练场”了，它还能帮你解决跨框架集成预演、SSR/SSG适配测试、团队原型快速分享这三个进阶开发者的痛点。&lt;/p&gt;
&lt;h3&gt;问题1：想试试跨框架集成？比如Vue3+React组件？&lt;/h3&gt;
&lt;p&gt;现在很多公司的前端项目都是混合框架的,比如老项目是React的，新项目是Vue3的，这时候就需要把React组件集成到Vue3项目里，或者把Vue3组件集成到React项目里——但集成之前，你肯定想先快速验证一下可行性，对吧？这时候Playground就能帮你预演。
那怎么在Playground里实现Vue3+React的跨框架集成呢？其实也不难，只要通过CDN引入React和ReactDOM，然后用Vue3的“defineCustomElement”或者第三方库“vue3-react-integration”来封装React组件就行，比如用第三方库“vue3-react-integration”（这个库是专门用来在Vue3里渲染React组件的，支持双向绑定和插槽），先在Playground的代码区顶部加个script标签引入：&lt;/p&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;!-- 引入Vue3的ESM版本 --&amp;gt;
&amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;
import { createApp, ref, h } from &amp;#39;//unpkg.com/vue@3/dist/vue.esm-browser.js&amp;#39;
// 引入React和ReactDOM的ESM版本
import React from &amp;#39;//unpkg.com/react@18/umd/react.production.min.js?module&amp;#39;
import ReactDOM from &amp;#39;//unpkg.com/react-dom@18/umd/react-dom.production.min.js?module&amp;#39;
// 引入vue3-react-integration的ESM版本
import { VueReact } from &amp;#39;//unpkg.com/vue3-react-integration/dist/vue3-react-integration.esm.js&amp;#39;
// 先写一个简单的React按钮组件
const ReactButton = ({ text, onClick, children }) =&amp;gt; {
  return (
    &amp;lt;button
      style={{
        padding: &amp;#39;8px 16px&amp;#39;,
        backgroundColor: &amp;#39;#42b883&amp;#39;,
        color: &amp;#39;#fff&amp;#39;,
        border: &amp;#39;none&amp;#39;,
        borderRadius: &amp;#39;4px&amp;#39;,
        cursor: &amp;#39;pointer&amp;#39;
      }}
      onClick={onClick}
    &amp;gt;
      {text || children}
    &amp;lt;/button&amp;gt;
  )
}
// 再写一个简单的React计数组件，支持双向绑定
const ReactCounter = ({ value, onChange }) =&amp;gt; {
  return (
    &amp;lt;div style={{ margin: &amp;#39;10px 0&amp;#39; }}&amp;gt;
      &amp;lt;span&amp;gt;React组件里的计数：{value}&amp;lt;/span&amp;gt;
      &amp;lt;div style={{ marginTop: &amp;#39;5px&amp;#39; }}&amp;gt;
        &amp;lt;ReactButton text=&amp;quot;-1&amp;quot; onClick={() =&amp;gt; onChange(value - 1)} /&amp;gt;
        &amp;lt;span style={{ margin: &amp;#39;0 10px&amp;#39; }}&amp;gt;&amp;lt;/span&amp;gt;
        &amp;lt;ReactButton text=&amp;quot;+1&amp;quot; onClick={() =&amp;gt; onChange(value + 1)} /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
// 创建Vue3应用
const app = createApp({
  setup() {
    const count = ref(0)
    // 把React组件传给VueReact
    const WrappedReactButton = VueReact(ReactButton)
    const WrappedReactCounter = VueReact(ReactCounter, {
      model: {
        prop: &amp;#39;value&amp;#39;,
        event: &amp;#39;change&amp;#39;
      }
    })
    return { count, WrappedReactButton, WrappedReactCounter }
  },
  template: `
    &amp;lt;div style=&amp;quot;width: 300px; margin: 20px auto; padding: 20px; border: 1px solid #eee; border-radius: 8px;&amp;quot;&amp;gt;
      &amp;lt;h3 style=&amp;quot;margin-top: 0; color: #35495e;&amp;quot;&amp;gt;Vue3+React跨框架集成预演&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;Vue3组件里的计数：{{ count }}&amp;lt;/p&amp;gt;
      &amp;lt;div style=&amp;quot;margin: 10px 0;&amp;quot;&amp;gt;
        &amp;lt;WrappedReactButton text=&amp;quot;Vue3控制React按钮+1&amp;quot; @click=&amp;quot;count++&amp;quot; /&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;WrappedReactCounter v-model=&amp;quot;count&amp;quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
  `
})
app.mount(&amp;#39;#app&amp;#39;)
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;改完之后立刻就能看到效果,双向绑定也正常，是不是很方便？&lt;/p&gt;
&lt;h3&gt;问题2：要做SSR/SSG项目？怎么快速测试组件的SSR兼容性？&lt;/h3&gt;
&lt;p&gt;现在很多Vue3项目都需要做SSR（服务端渲染）或者SSG（静态站点生成），比如企业官网、新闻网站、博客这些，因为SSR/SSG能提升网站的首屏加载速度和SEO效果——但不是所有的Vue3组件都能直接在SSR/SSG项目里用的，比如有些组件会在mounted或者onMounted钩子函数里直接访问window或者document对象（SSR/SSG在服务端渲染时是没有window和document对象的），这时候就会报错，那怎么快速测试组件的SSR兼容性呢？
别担心，Playground支持SSR模式！只要点击编辑器顶部的“Toggle SSR”按钮，就能切换到SSR模式——切换之后，编辑器右侧的预览区会先显示服务端渲染的HTML内容，然后再进行客户端激活（hydration），要是你的组件在服务端渲染时访问了window或者document对象，预览区就会立刻报错，鼠标悬停还能看到具体的错误位置，比如你写一个这样的组件：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;p&amp;gt;浏览器窗口的宽度是：{{ windowWidth }}&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup&amp;gt;
import { ref, onMounted } from &amp;#39;vue&amp;#39;
const windowWidth = ref(0)
// 这里直接在setup的顶层访问window对象，SSR模式下会报错
// const windowWidth = ref(window.innerWidth)
// 正确的写法是在onMounted钩子函数里访问，或者用useWindowWidth这类SSR兼容的组合式API
onMounted(() =&amp;gt; {
  windowWidth.value = window.innerWidth
  window.addEventListener(&amp;#39;resize&amp;#39;, () =&amp;gt; {
    windowWidth.value = window.innerWidth
  })
})
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;要是你把注释掉的那行代码取消注释,切换到SSR模式之后，预览区就会立刻报错“ReferenceError: window is not defined”——这样就能快速定位到SSR兼容性问题，不用等本地SSR/SSG项目启动之后再找。&lt;/p&gt;
&lt;h3&gt;问题3：要给产品经理/UI设计师看原型？怎么快速分享？&lt;/h3&gt;
&lt;p&gt;有时候你写了一个Vue3的原型组件,比如一个新功能的交互原型，想给产品经理或者UI设计师看——但你总不能把本地项目的代码打包之后发给他们，让他们自己搭服务器看吧？这时候Playground的“分享功能”就派上用场了。
那怎么分享呢？只要点击编辑器顶部的“Share”按钮，然后选择分享方式就行——有三种分享方式：第一种是生成一个永久链接，别人只要点开这个链接就能看到你的代码和预览效果，还能复制你的代码或者Fork你的项目（Fork之后别人就能修改你的代码，然后再分享给你）；第二种是生成一个二维码，别人只要用手机或者平板扫描这个二维码就能看到预览效果；第三种是生成一个嵌入代码，你可以把这个嵌入代码放到你的博客、公司的内部文档或者Notion里，别人就能直接在这些页面里看到你的代码和预览效果，哦对了，分享之前你可以选择是否隐藏代码区——要是你只想给产品经理或者UI设计师看预览效果，不想让他们看到你的代码，只要在“Share”弹窗里把“Show code”的开关关掉就行。&lt;/p&gt;
&lt;h2&gt;隐藏彩蛋篇：Playground里你可能不知道的3个小功能&lt;/h2&gt;
&lt;p&gt;除了上面提到的新手入门和进阶开发功能,Playground里还有几个隐藏的小功能，知道之后能让你的开发效率更高。&lt;/p&gt;
&lt;h3&gt;彩蛋1：支持TypeScript！&lt;/h3&gt;
&lt;p&gt;很多前端开发者现在都在用TypeScript写Vue3项目,Playground当然也支持TypeScript啦！只要在创建文件时把文件后缀改成.ts或者.vue（然后在script标签里加lang=&quot;ts&quot;）就行，编辑器会像VSCode一样弹出TypeScript的语法提示和实时纠错，比如你想写一个带类型注解的Vue3组件：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h3&amp;gt;TypeScript版的待办事项列表&amp;lt;/h3&amp;gt;
    &amp;lt;input
      type=&amp;quot;text&amp;quot;
      v-model=&amp;quot;newTodoText&amp;quot;
      placeholder=&amp;quot;请输入待办事项&amp;quot;
      @keyup.enter=&amp;quot;addTodo&amp;quot;
    /&amp;gt;
    &amp;lt;button @click=&amp;quot;addTodo&amp;quot;&amp;gt;添加&amp;lt;/button&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li v-for=&amp;quot;todo in todos&amp;quot; :key=&amp;quot;todo.id&amp;quot;&amp;gt;
        &amp;lt;input type=&amp;quot;checkbox&amp;quot; v-model=&amp;quot;todo.completed&amp;quot; /&amp;gt;
        &amp;lt;span :style=&amp;quot;{ textDecoration: todo.completed ? &amp;#39;line-through&amp;#39; : &amp;#39;none&amp;#39; }&amp;quot;&amp;gt;{{ todo.text }}&amp;lt;/span&amp;gt;
      &amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup lang=&amp;quot;ts&amp;quot;&amp;gt;
import { ref } from &amp;#39;vue&amp;#39;
// 定义待办事项的类型
interface Todo {
  id: number
  text: string
  completed: boolean
}
// 待办事项列表
const todos = ref&amp;lt;Todo[]&amp;gt;([
  { id: 1, text: &amp;#39;学习Vue3 Playground&amp;#39;, completed: false },
  { id: 2, text: &amp;#39;给这篇文章点赞&amp;#39;, completed: false },
  { id: 3, text: &amp;#39;收藏这篇文章&amp;#39;, completed: false }
])
// 新待办事项的文本
const newTodoText = ref&amp;lt;string&amp;gt;(&amp;#39;&amp;#39;)
// 添加待办事项的函数
const addTodo = (): void =&amp;gt; {
  if (newTodoText.value.trim() === &amp;#39;&amp;#39;) return
  todos.value.push({
    id: Date.now(),
    text: newTodoText.value.trim(),
    completed: false
  })
  newTodoText.value = &amp;#39;&amp;#39;
}
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;
div {
  width: 400px;
  margin: 20px auto;
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 8px;
}
input[type=&amp;quot;text&amp;quot;] {
  width: 250px;
  padding: 6px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-right: 10px;
}
button {
  padding: 6px 12px;
  background-color: #42b883;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
ul {
  list-style: none;
  padding: 0;
  margin-top: 20px;
}
li {
  padding: 8px 0;
  border-bottom: 1px solid #eee;
}
&amp;lt;/style&amp;gt;&lt;/pre&gt;
&lt;p&gt;写好之后,要是你把newTodoText的类型注解改成number，编辑器下面就会立刻出现红色波浪线，提示“类型&#039;number&#039;的参数不能赋给类型&#039;string&#039;的参数”——这样就能快速发现类型错误。&lt;/p&gt;
&lt;h3&gt;彩蛋2：支持自定义主题！&lt;/h3&gt;
&lt;p&gt;你是不是觉得Playground的默认主题太单调了？没关系，Playground支持自定义主题！只要点击编辑器顶部的“Settings”按钮，然后在“Theme”下拉菜单里选择你喜欢的主题就行——有Light、Dark、Monokai、Dracula、GitHub Light、GitHub Dark等多种主题可以选，总有一款适合你。&lt;/p&gt;
&lt;h3&gt;彩蛋3：支持下载项目！&lt;/h3&gt;
&lt;p&gt;要是你在Playground里写的代码比较完善,想把它下载到本地继续开发，怎么办？只要点击编辑器顶部的“Download”按钮，就能把整个项目打包成一个.zip文件下载下来——下载之后，解压.zip文件，然后在解压后的文件夹里运行npm install和npm run dev（或者yarn install和yarn dev，或者pnpm install和pnpm dev），就能在本地启动这个项目了，完全不用自己搭脚手架。&lt;/p&gt;
&lt;p&gt;Vue3 Playground真的不是一个只能写“Hello World”的小工具，它是一个功能强大的在线IDE，能帮新手快速掌握Vue3的基础语法，能帮进阶开发者解决跨框架集成预演、SSR/SSG适配测试、团队原型快速分享这三个痛点，还有几个隐藏的小功能能提升开发效率，以后再学Vue3或者做Vue3项目时，别忘了先打开Vue3 Playground试试哦！&lt;/p&gt;</description><pubDate>Sun, 02 Aug 2026 21:10:35 +0800</pubDate></item><item><title>Vue3里怎么实现不依赖第三方付费插件的流畅PDF预览？</title><link>https://www.codeqd.com/post/20260821979.html</link><description>&lt;p&gt;最近做Vue3的项目时,碰到了客户要求的PDF嵌入需求——不能插iframe跳外链（怕跳丢流量），不能用带限制的付费预览服务，手机端、PC端都要适配，还得有基础的翻页、缩放、打印功能，一开始以为很难，搜了一堆零散教程踩了不少坑，折腾了两天终于摸出了一套稳定、免费的方案，今天就用大家日常能听懂的话，把从选型到踩坑再到优化的全流程理清楚。&lt;/p&gt;
&lt;h2&gt;为什么不直接用HTML5原生的&lt;embed&gt;或者
&lt;p&gt;很多人第一反应会想到这俩原生标签,我一开始也试了，确实快——直接把PDF文件的路径塞进去就行，浏览器自带的PDF阅读器会接管，但踩过的三个大坑让我放弃了这个方案：&lt;/p&gt;
&lt;p&gt;第一个坑是兼容性参差不齐,你永远不知道用户用的是什么浏览器内核：IE就不说了，早就淘汰了，但国内的360极速、QQ浏览器这类双核浏览器，如果用户默认切到了兼容模式（很多政企老系统或者家庭电脑还会这么设置），原生标签要么白屏要么直接提示下载，完全没法用。&lt;/p&gt;
&lt;p&gt;第二个坑是功能完全不可控,浏览器自带的阅读器UI长什么样、有什么按钮，我们开发者说了不算——比如有些浏览器没有“跳转到指定页码”的按钮，有些手机端阅读器会直接全屏隐藏原页面的导航栏，这对电商详情页或者企业文档系统这种需要保持页面结构的场景来说，简直是致命伤。&lt;/p&gt;
&lt;p&gt;第三个坑是文件体积大的时候加载体验差,原生标签没有懒加载单页PDF的能力，整个文件一次性全部下载下来，要是碰到几百兆的技术手册或者设计稿，用户可能要等几十秒甚至更久，耐心早就磨没了。&lt;/p&gt;
&lt;h2&gt;选什么免费开源库最稳妥？&lt;/h2&gt;
&lt;p&gt;既然原生标签不行,那肯定得找开源库，我对比了Vue3生态里主流的三个：pdfjs-dist、vue-pdf-app-next、vue-pdf-embed。&lt;/p&gt;
&lt;p&gt;vue-pdf-app-next其实是基于pdf.js改的Vue3组件，一开始我觉得这个会最顺手，毕竟封装好了很多功能，但实际用的时候发现，最近一年作者好像没怎么更新了，文档也写得比较简略，碰到手机端双指缩放卡顿的问题，翻遍了GitHub的issue区也没找到官方解决方案，只能自己硬啃底层代码改，效率太低。&lt;/p&gt;
&lt;p&gt;vue-pdf-embed也是封装好的，而且更新频率比vue-pdf-app-next高一点，但它只保留了最基础的渲染、翻页功能，要是想加打印、搜索、缩略图侧边栏，还得自己手动集成pdf.js的其他模块，相当于既要用封装层，又要懂底层，有点“画蛇添足”的意思。&lt;/p&gt;
&lt;p&gt;最后选的是直接用原生的pdfjs-dist，别被“原生”两个字吓到，其实pdf.js官方早就提供了适配各种框架的思路，而且它的兼容性是所有方案里最好的——覆盖了Chrome、Firefox、Safari（包括iOS14之前的老版本）、Edge，国内的主流双核浏览器在极速模式下也完全没问题，功能最全，文档也非常详细，遇到问题查起来方便。&lt;/p&gt;
&lt;h2&gt;从零开始搭建Vue3+pdfjs-dist的PDF预览功能&lt;/h2&gt;
&lt;p&gt;废话不多说,直接上干货，分PC端基础版和手机端优化版两部分讲。&lt;/p&gt;
&lt;h3&gt;第一步：安装依赖和引入资源&lt;/h3&gt;
&lt;p&gt;pdfjs-dist虽然是原生库，但有两个版本：一个是普通的CommonJS版本，另一个是ES模块版本，推荐用ES模块版本，配合Vite或者Webpack的tree-shaking功能，可以减小打包体积。&lt;/p&gt;
&lt;p&gt;安装依赖很简单,直接在终端敲：&lt;/p&gt;
&lt;pre class=&quot;brush:bash;toolbar:false&quot;&gt;npm install pdfjs-dist&lt;/pre&gt;
&lt;p&gt;安装好之后,&lt;strong&gt;关键一步来了&lt;/strong&gt;——别直接在代码里import完整的pdf.js，因为里面的worker文件太大了，直接打包会导致首屏加载慢，pdf.js官方推荐的做法是把worker文件和字体文件（用来渲染中文、日文、韩文这些非拉丁文字）单独放到项目的静态资源目录里，然后通过CDN或者本地路径引入。&lt;/p&gt;
&lt;p&gt;如果是用Vite的项目,静态资源目录默认是public，我们可以把node_modules/pdfjs-dist/build/pdf.worker.min.js和node_modules/pdfjs-dist/cmaps整个文件夹（注意是整个文件夹！因为cmaps里有很多支持不同语言的映射文件）复制到public目录下，如果是用Webpack的项目，静态资源目录一般是src/assets或者public，具体看你的配置，复制的步骤是一样的。&lt;/p&gt;
&lt;h3&gt;第二步：写基础的渲染组件&lt;/h3&gt;
&lt;p&gt;我们先写一个单文件组件PDFViewer.vue，实现最基础的功能：加载PDF文件、渲染第一页、上一页/下一页翻页、跳转到指定页码、缩放、显示总页数和当前页码。&lt;/p&gt;
&lt;p&gt;首先在script setup里引入pdf.js的核心模块，并设置worker和cmaps的路径：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { onMounted, ref, watch } from &amp;#39;vue&amp;#39;
import * as pdfjsLib from &amp;#39;pdfjs-dist&amp;#39;
// 设置worker文件的路径
pdfjsLib.GlobalWorkerOptions.workerSrc = &amp;#39;/pdf.worker.min.js&amp;#39;
// 组件的props，用来接收PDF文件的路径或者Blob对象
const props = defineProps({
  pdfUrl: {
    type: [String, Blob],
    required: true
  }
})
// 定义响应式变量
const pdfDoc = ref(null) // 整个PDF文档的对象
const currentPage = ref(1) // 当前页码
const totalPages = ref(0) // 总页数
const scale = ref(1) // 缩放比例，1代表原始大小
const canvasRef = ref(null) // 用来渲染PDF的canvas元素
const pdfContainerRef = ref(null) // canvas的父容器，用来适配宽度&lt;/pre&gt;
&lt;p&gt;然后写一个加载PDF文档的函数loadPdf：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;const loadPdf = async () =&amp;gt; {
  try {
    // 如果props.pdfUrl是Blob对象，需要先转换成URL
    let url = props.pdfUrl
    if (url instanceof Blob) {
      url = URL.createObjectURL(url)
    }
    // 加载PDF文档
    const loadingTask = pdfjsLib.getDocument({
      url: url,
      cMapUrl: &amp;#39;/cmaps/&amp;#39;, // 设置cmaps的路径
      cMapPacked: true // 表示cmaps是压缩过的
    })
    pdfDoc.value = await loadingTask.promise
    totalPages.value = pdfDoc.value.numPages
    // 加载完文档后渲染第一页
    renderPage(currentPage.value)
  } catch (error) {
    console.error(&amp;#39;PDF加载失败：&amp;#39;, error)
    alert(&amp;#39;PDF文件加载失败，请检查文件路径是否正确或者文件是否损坏&amp;#39;)
  }
}&lt;/pre&gt;
&lt;p&gt;接下来写渲染单页的函数renderPage：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;const renderPage = async (pageNum) =&amp;gt; {
  try {
    // 获取指定页码的页面对象
    const page = await pdfDoc.value.getPage(pageNum)
    // 获取父容器的宽度，用来计算缩放比例，实现自适应宽度
    const containerWidth = pdfContainerRef.value.clientWidth
    const viewport = page.getViewport({ scale: 1 })
    const calculatedScale = containerWidth / viewport.width
    // 如果用户手动设置了缩放比例，就用用户设置的，否则用自适应的
    const finalScale = scale.value === 1 ? calculatedScale : scale.value
    const finalViewport = page.getViewport({ scale: finalScale })
    // 获取canvas的2D上下文
    const canvas = canvasRef.value
    const context = canvas.getContext(&amp;#39;2d&amp;#39;)
    // 设置canvas的宽高
    canvas.width = finalViewport.width
    canvas.height = finalViewport.height
    // 渲染PDF页面到canvas上
    const renderContext = {
      canvasContext: context,
      viewport: finalViewport
    }
    await page.render(renderContext).promise
    // 更新当前页码
    currentPage.value = pageNum
  } catch (error) {
    console.error(&amp;#39;PDF页面渲染失败：&amp;#39;, error)
  }
}&lt;/pre&gt;
&lt;p&gt;然后写一些交互函数,比如上一页、下一页、跳转到指定页码、放大、缩小、恢复原始大小：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// 上一页
const prevPage = () =&amp;gt; {
  if (currentPage.value &amp;gt; 1) {
    renderPage(currentPage.value - 1)
  }
}
// 下一页
const nextPage = () =&amp;gt; {
  if (currentPage.value &amp;lt; totalPages.value) {
    renderPage(currentPage.value + 1)
  }
}
// 跳转到指定页码
const jumpToPage = (e) =&amp;gt; {
  const pageNum = parseInt(e.target.value)
  if (pageNum &amp;gt;= 1 &amp;amp;&amp;amp; pageNum &amp;lt;= totalPages.value) {
    renderPage(pageNum)
  } else {
    alert(`请输入1到${totalPages.value}之间的页码`)
  }
}
// 放大
const zoomIn = () =&amp;gt; {
  if (scale.value &amp;lt; 3) { // 限制最大缩放比例为3倍，避免canvas太大导致浏览器卡顿
    scale.value += 0.25
    renderPage(currentPage.value)
  }
}
// 缩小
const zoomOut = () =&amp;gt; {
  if (scale.value &amp;gt; 0.5) { // 限制最小缩放比例为0.5倍，避免看不清
    scale.value -= 0.25
    renderPage(currentPage.value)
  }
}
// 恢复原始大小
const resetScale = () =&amp;gt; {
  scale.value = 1
  renderPage(currentPage.value)
}&lt;/pre&gt;
&lt;p&gt;然后在onMounted钩子函数里调用loadPdf,监听pdfUrl的变化，重新加载PDF：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;onMounted(() =&amp;gt; {
  loadPdf()
})
watch(() =&amp;gt; props.pdfUrl, () =&amp;gt; {
  if (props.pdfUrl) {
    loadPdf()
  }
})&lt;/pre&gt;
&lt;p&gt;最后写template部分,布局要简洁，PC端和手机端都能适配：&lt;/p&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;pdf-viewer-wrapper&amp;quot;&amp;gt;
    &amp;lt;!-- 工具栏 --&amp;gt;
    &amp;lt;div class=&amp;quot;pdf-toolbar&amp;quot;&amp;gt;
      &amp;lt;button @click=&amp;quot;prevPage&amp;quot; :disabled=&amp;quot;currentPage === 1&amp;quot;&amp;gt;上一页&amp;lt;/button&amp;gt;
      &amp;lt;span&amp;gt;{{ currentPage }} / {{ totalPages }}&amp;lt;/span&amp;gt;
      &amp;lt;input type=&amp;quot;number&amp;quot; :min=&amp;quot;1&amp;quot; :max=&amp;quot;totalPages&amp;quot; @change=&amp;quot;jumpToPage&amp;quot; :value=&amp;quot;currentPage&amp;quot; /&amp;gt;
      &amp;lt;button @click=&amp;quot;nextPage&amp;quot; :disabled=&amp;quot;currentPage === totalPages&amp;quot;&amp;gt;下一页&amp;lt;/button&amp;gt;
      &amp;lt;div class=&amp;quot;pdf-zoom-buttons&amp;quot;&amp;gt;
        &amp;lt;button @click=&amp;quot;zoomOut&amp;quot;&amp;gt;-&amp;lt;/button&amp;gt;
        &amp;lt;button @click=&amp;quot;resetScale&amp;quot;&amp;gt;原始大小&amp;lt;/button&amp;gt;
        &amp;lt;button @click=&amp;quot;zoomIn&amp;quot;&amp;gt;+&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;!-- PDF容器 --&amp;gt;
    &amp;lt;div class=&amp;quot;pdf-container&amp;quot; ref=&amp;quot;pdfContainerRef&amp;quot;&amp;gt;
      &amp;lt;canvas ref=&amp;quot;canvasRef&amp;quot;&amp;gt;&amp;lt;/canvas&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;再加点简单的CSS样式,让它看起来更美观：&lt;/p&gt;
&lt;pre class=&quot;brush:css;toolbar:false&quot;&gt;.pdf-viewer-wrapper {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  box-sizing: border-box;
}
.pdf-toolbar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 15px;
  margin-bottom: 20px;
  padding: 10px 20px;
  background-color: #f5f5f5;
  border-radius: 8px;
  flex-wrap: wrap;
}
.pdf-toolbar button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #409eff;
  color: white;
  cursor: pointer;
  transition: all 0.3s;
}
.pdf-toolbar button:hover {
  background-color: #66b1ff;
}
.pdf-toolbar button:disabled {
  background-color: #c0c4cc;
  cursor: not-allowed;
}
.pdf-toolbar input {
  width: 60px;
  padding: 6px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  text-align: center;
}
.pdf-container {
  width: 100%;
  overflow-x: auto;
  text-align: center;
}
.pdf-container canvas {
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}&lt;/pre&gt;
&lt;h3&gt;第三步：加打印功能&lt;/h3&gt;
&lt;p&gt;打印功能其实也不难,pdf.js官方提供了PrintManager模块，但直接用的话有点复杂，我给大家简化了一个方案——利用浏览器的打印API，把当前渲染的canvas或者所有页面的canvas打印出来。&lt;/p&gt;
&lt;p&gt;如果只想打印当前页面,那很简单，直接在组件里加一个打印按钮，然后写一个printCurrentPage函数：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;const printCurrentPage = () =&amp;gt; {
  const canvas = canvasRef.value
  const dataUrl = canvas.toDataURL(&amp;#39;image/png&amp;#39;)
  const printWindow = window.open(&amp;#39;&amp;#39;, &amp;#39;_blank&amp;#39;)
  printWindow.document.write(`
    &amp;lt;html&amp;gt;
      &amp;lt;head&amp;gt;
        &amp;lt;title&amp;gt;打印当前页&amp;lt;/title&amp;gt;
        &amp;lt;style&amp;gt;
          body {
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
          }
          img {
            max-width: 100%;
            max-height: 100vh;
          }
        &amp;lt;/style&amp;gt;
      &amp;lt;/head&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;img src=&amp;quot;${dataUrl}&amp;quot; /&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  `)
  printWindow.document.close()
  printWindow.onload = () =&amp;gt; {
    printWindow.print()
    printWindow.close()
  }
}&lt;/pre&gt;
&lt;p&gt;如果想打印整个PDF文档,那就要先把所有页面都渲染成canvas，然后再生成一个包含所有图片的临时页面打印，但要注意，如果PDF文档页数很多，渲染所有页面会消耗大量的内存和时间，所以一般建议只提供打印当前页或者打印指定范围页的功能。&lt;/p&gt;
&lt;h3&gt;第四步：手机端优化&lt;/h3&gt;
&lt;p&gt;手机端的主要优化点有两个：一是适配屏幕宽度，二是支持双指缩放和双击缩放。&lt;/p&gt;
&lt;p&gt;适配屏幕宽度其实我们在基础版里已经实现了,就是通过父容器的宽度计算缩放比例，这个在手机端也能用。&lt;/p&gt;
&lt;p&gt;支持双指缩放和双击缩放可以用原生的touch事件,也可以用第三方库比如hammer.js，我推荐用hammer.js，因为它封装好了各种手势事件，用起来非常简单。&lt;/p&gt;
&lt;p&gt;首先安装hammer.js：&lt;/p&gt;
&lt;pre class=&quot;brush:bash;toolbar:false&quot;&gt;npm install hammer.js&lt;/pre&gt;
&lt;p&gt;然后在PDFViewer.vue里引入，并绑定到pdfContainerRef上：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import Hammer from &amp;#39;hammer.js&amp;#39;
// 在onMounted钩子函数里添加
onMounted(() =&amp;gt; {
  loadPdf()
  // 绑定hammer.js的手势事件
  const hammer = new Hammer(pdfContainerRef.value)
  // 启用双击和双指缩放手势
  hammer.get(&amp;#39;doubletap&amp;#39;).set({ enable: true })
  hammer.get(&amp;#39;pinch&amp;#39;).set({ enable: true })
  // 双击缩放
  hammer.on(&amp;#39;doubletap&amp;#39;, (e) =&amp;gt; {
    if (scale.value === 1) {
      // 双击放大到2倍
      scale.value = 2
    } else {
      // 双击恢复原始大小
      resetScale()
    }
    renderPage(currentPage.value)
  })
  // 双指缩放
  let initialScale = 1
  hammer.on(&amp;#39;pinchstart&amp;#39;, (e) =&amp;gt; {
    initialScale = scale.value
  })
  hammer.on(&amp;#39;pinchmove&amp;#39;, (e) =&amp;gt; {
    let newScale = initialScale * e.scale
    // 限制缩放比例在0.5到3倍之间
    newScale = Math.max(0.5, Math.min(3, newScale))
    scale.value = newScale
    renderPage(currentPage.value)
  })
})&lt;/pre&gt;
&lt;h2&gt;踩过的其他坑和优化建议&lt;/h2&gt;
&lt;p&gt;除了前面提到的worker文件和cmaps的配置,我还踩了几个其他的坑，给大家提个醒：&lt;/p&gt;
&lt;p&gt;第一个坑是中文乱码,这个就是因为没有配置cmaps导致的，只要把node_modules/pdfjs-dist/cmaps整个文件夹复制到静态资源目录，并在getDocument的参数里设置cMapUrl和cMapPacked就可以解决。&lt;/p&gt;
&lt;p&gt;第二个坑是大文件加载慢,这个可以通过pdf.js的懒加载功能来优化，只加载当前页和相邻的几页，不过实现起来比较复杂，适合页数特别多的项目，如果项目页数不是特别多（比如少于50页），可以用loadingTask的onProgress回调函数来显示加载进度，提升用户体验。&lt;/p&gt;
&lt;p&gt;第三个坑是canvas渲染模糊,这个是因为现在的手机和电脑屏幕大多是高DPI的，canvas的默认分辨率是1:1的，所以渲染出来会模糊，解决方法是把canvas的宽高乘以设备的像素比（devicePixelRatio），然后再用CSS把canvas的宽高设置成原来的大小，我们可以修改renderPage函数里的canvas宽高设置部分：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// 获取设备的像素比
const dpr = window.devicePixelRatio || 1
// 设置canvas的物理宽高（乘以像素比）
canvas.width = finalViewport.width * dpr
canvas.height = finalViewport.height * dpr
// 设置canvas的CSS宽高（保持原来的大小）
canvas.style.width = `${finalViewport.width}px`
canvas.style.height = `${finalViewport.height}px`
// 缩放2D上下文，适配高DPI
context.scale(dpr, dpr)&lt;/pre&gt;
&lt;p&gt;最后给大家几个优化建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果PDF文件很大,可以考虑先压缩一下，比如用SmallPDF或者Adobe Acrobat Pro，压缩率能达到50%以上，而且不会太影响画质。&lt;/li&gt;
&lt;li&gt;如果项目是部署在CDN上的,可以把pdf.worker.min.js和cmaps文件夹也上传到CDN，这样加载速度会更快。&lt;/li&gt;
&lt;li&gt;可以添加一个加载动画,在PDF加载和页面渲染的时候显示，提升用户体验。&lt;/li&gt;
&lt;li&gt;如果需要搜索功能,可以自己集成pdf.js的TextLayerBuilder模块，实现起来也不难。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;好了,以上就是我从零开始搭建Vue3+pdfjs-dist的PDF预览功能的全流程，希望能帮到大家，如果有什么问题，欢迎在评论区留言交流。&lt;/p&gt;</description><pubDate>Sun, 02 Aug 2026 14:01:50 +0800</pubDate></item><item><title>Vue3项目怎么实现带水印可打印的PDF预览+导出？</title><link>https://www.codeqd.com/post/20260821978.html</link><description>&lt;h2&gt;先理清楚Vue3项目做PDF相关功能的核心需求&lt;/h2&gt;
&lt;p&gt;我们在做Vue3后台、企业官网、甚至个人工具站的时候，经常会碰到这几个和PDF相关的高频场景：比如合同预览、报表导出、简历/产品手册在线生成、涉密资料只看不能复制打印之类的，但不同场景侧重不一样，有的只需要快速渲染别人上传的PDF就行，有的要前端自己写HTML生成带特定格式的复杂PDF，还有的要额外加安全限制——像刚才标题里说的水印、打印控制，所以在选技术方案之前，得先把自己项目的核心需求抠准，别上来就随便塞一个库,不然后期改起来麻烦得很。&lt;/p&gt;
&lt;h2&gt;Vue3做PDF常用的三个技术方案，分别适合什么场景？&lt;/h2&gt;
&lt;p&gt;目前在Vue3生态里，用得最顺手、社区维护也比较好的方案其实就三个，我们一个个说,你看完就能直接对应自己的需求选了。&lt;/p&gt;
&lt;h3&gt;纯渲染已有PDF，可选加安全限制&lt;/h3&gt;
&lt;p&gt;如果你只需要上传或者后端传一个PDF文件流过来，在页面上展示，偶尔加个水印、禁止右键复制打印这种小需求，那直接用 &lt;strong&gt;vue-pdf-embed&lt;/strong&gt; 就行，这个库是专门为Vue3做PDF渲染轻量封装的，上手超级快,包体积也小。&lt;/p&gt;
&lt;p&gt;纯渲染的步骤其实很简单：先npm install vue-pdf-embed pdfjs-dist，然后pdfjs-dist记得要配worker，不然在有些浏览器会报错——pdfjs-dist自带了两个worker，一个是渲染用的，一个是文字层（如果不需要选文字复制的话可以关了省点事），配置worker的话，在Vue3的main.js或者main.ts里直接引入worker的CDN或者本地路径就行，&lt;/p&gt;
&lt;pre class=&quot;brush:js;toolbar:false&quot;&gt;import VuePdfEmbed from &amp;#39;vue-pdf-embed&amp;#39;
import * as pdfjsLib from &amp;#39;pdfjs-dist&amp;#39;
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`
app.component(&amp;#39;VuePdfEmbed&amp;#39;, VuePdfEmbed)&lt;/pre&gt;
&lt;p&gt;如果是本地打包部署怕CDN挂的话，也可以把node_modules/pdfjs-dist/build里的pdf.worker.min.js和pdf.worker.min.js.map复制到public文件夹下，然后路径写&#039;/pdf.worker.min.js&#039;。&lt;/p&gt;
&lt;p&gt;然后在页面里用组件就好了，src可以是本地的静态资源路径、后端返回的文件流Blob、或者PDF的URL，禁止右键复制打印的话，可以给组件外面包个div，加个@contextmenu.prevent，然后加css的user-select: none; pointer-events: none;（pointer-events: none会连滚动条和翻页都禁用，如果需要滚动的话，记得单独给滚动容器开pointer-events），水印的话，有两种方法：一种是给vue-pdf-embed组件加一层绝对定位的透明div，然后用canvas或者CSS生成重复的水印文字/图片；另一种是用pdfjs-dist直接操作PDF原始数据，给每页都加上水印图层再渲染——后者更稳，因为就算用户把外层div的样式改了，水印还是在PDF本身里,导出的话也能带水印。&lt;/p&gt;
&lt;h3&gt;前端自己写HTML生成复杂PDF，支持样式自定义&lt;/h3&gt;
&lt;p&gt;如果你的PDF内容是动态的，比如后台的销售报表、用户的个人简历、带图片和表格的产品报价单，后端没法直接生成，或者不想给后端加压力，那用 &lt;strong&gt;html2pdf.js&lt;/strong&gt; 就对了，这个库的原理是先把页面上的HTML或者指定的DOM元素转成canvas，然后再把canvas转成PDF，所以不管你用Vue3的插槽、Tailwind CSS、Element Plus还是Ant Design Vue写的样式，只要浏览器能渲染出来，它基本上都能生成出来——不过要注意一些兼容性问题，比如Flex布局在老版本浏览器上可能会有错位，还有阴影、渐变这些CSS3属性要设置html2pdf.js的options里的useCORS为true（如果图片是跨域的），useWebWorker为true（不然大文件会卡死页面）。&lt;/p&gt;
&lt;p&gt;用html2pdf.js和Vue3结合也不复杂，甚至不需要专门封装Vue组件，直接在需要导出的地方引入就行，首先npm install html2pdf.js，然后在页面里写一个ref,指向你要生成PDF的那个DOM元素，&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;pdf-container&amp;quot;&amp;gt;
    &amp;lt;!-- 这部分是要生成PDF的内容，别把按钮、导航栏放进来 --&amp;gt;
    &amp;lt;div ref=&amp;quot;pdfContentRef&amp;quot; class=&amp;quot;pdf-content&amp;quot;&amp;gt;
      &amp;lt;!-- 这里放你的Vue组件、HTML、样式 --&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;button @click=&amp;quot;exportPdf&amp;quot;&amp;gt;导出PDF&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup&amp;gt;
import { ref } from &amp;#39;vue&amp;#39;
import html2pdf from &amp;#39;html2pdf.js&amp;#39;
const pdfContentRef = ref(null)
const exportPdf = () =&amp;gt; {
  const options = {
    margin: 10,
    filename: &amp;#39;test.pdf&amp;#39;,
    image: { type: &amp;#39;jpeg&amp;#39;, quality: 0.98 },
    html2canvas: { scale: 2, useCORS: true },
    jsPDF: { unit: &amp;#39;mm&amp;#39;, format: &amp;#39;a4&amp;#39;, orientation: &amp;#39;portrait&amp;#39; }
  }
  html2pdf().set(options).from(pdfContentRef.value).save()
}
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这里的options可以根据自己的需求调整，比如margin可以设成数组（上右下左），jsPDF的orientation可以设成&#039;landscape&#039;（横向），scale设成2或者3的话，生成的PDF文字会更清晰,但包体积也会更大。&lt;/p&gt;
&lt;h3&gt;前端直接操作PDF原始数据，适合高级需求&lt;/h3&gt;
&lt;p&gt;如果你需要做更高级的操作，比如合并多个PDF、拆分PDF、给PDF加密码、修改已有PDF的文字（虽然比较麻烦），那前面两个库就不够用了，得用 &lt;strong&gt;pdf-lib&lt;/strong&gt;，这个库是用纯JavaScript写的，完全在前端运行，不需要后端，功能超级强大，而且社区维护得很好,最近两年更新挺频繁的。&lt;/p&gt;
&lt;p&gt;用pdf-lib和Vue3结合的话，稍微复杂一点，因为它没有直接的渲染组件，要是需要预览的话，还得结合vue-pdf-embed或者pdfjs-dist来用——也就是先用pdf-lib处理PDF（比如加密码、合并、加水印），然后生成Blob，再传给vue-pdf-embed渲染，或者直接导出，举个简单的合并两个PDF的例子：首先npm install pdf-lib，然后引入，用fetch获取两个PDF的URL或者文件流，转成ArrayBuffer，然后用pdf-lib的PDFDocument.load()加载，再用PDFDocument.create()创建一个新的PDF，把加载好的PDF的每页都复制到新PDF里，最后saveAsBase64或者saveAsBlob，传给vue-pdf-embed或者下载就行。&lt;/p&gt;
&lt;h2&gt;三个方案的优缺点对比，帮你快速做选择&lt;/h2&gt;
&lt;p&gt;可能光看上面的描述你还是有点懵，那我们列个简单的对比表,直观一点：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr class=&quot;firstRow&quot;&gt;
&lt;th&gt;技术方案&lt;/th&gt;
&lt;th&gt;核心功能&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;th&gt;适合场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vue-pdf-embed&lt;/td&gt;
&lt;td&gt;纯渲染PDF，可选简单安全限制&lt;/td&gt;
&lt;td&gt;轻量、上手快、Vue3原生支持好&lt;/td&gt;
&lt;td&gt;不能生成PDF，高级功能少&lt;/td&gt;
&lt;td&gt;合同预览、资料展示、只读场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;html2pdf.js&lt;/td&gt;
&lt;td&gt;HTML转PDF，支持样式自定义&lt;/td&gt;
&lt;td&gt;不需要懂PDF底层，写HTML就行，样式还原度高&lt;/td&gt;
&lt;td&gt;大文件容易卡死页面，Flex/阴影等兼容性偶尔有问题&lt;/td&gt;
&lt;td&gt;动态报表、简历、报价单生成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdf-lib&lt;/td&gt;
&lt;td&gt;直接操作PDF原始数据，高级功能&lt;/td&gt;
&lt;td&gt;功能最全，纯前端运行，性能好&lt;/td&gt;
&lt;td&gt;上手慢，需要懂一点PDF底层，没有直接渲染组件&lt;/td&gt;
&lt;td&gt;合并拆分PDF、加密码、修改已有PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;几个常见的坑，你一定要避开&lt;/h2&gt;
&lt;p&gt;不管你选哪个方案，做Vue3 PDF相关功能的时候，都很容易踩这几个坑，我给你提前说一下,省得你浪费时间：&lt;/p&gt;
&lt;h3&gt;坑一：pdfjs-dist的worker配置不对&lt;/h3&gt;
&lt;p&gt;这个是用vue-pdf-embed最常见的坑，要是worker没配好，页面会直接报错“Failed to load PDF document”或者“Worker not loaded”，解决方法刚才说了，要么用CDN（注意版本要和你下载的pdfjs-dist一致，不然可能也会有问题）,要么把worker文件放到public文件夹下。&lt;/p&gt;
&lt;h3&gt;坑二：html2pdf.js导出的PDF图片跨域&lt;/h3&gt;
&lt;p&gt;如果你的PDF内容里有跨域的图片，html2canvas（html2pdf.js的底层依赖之一）会渲染不出来，变成空白，解决方法有两个：一个是给图片的服务器加CORS头，允许你的域名访问；另一个是给图片标签加crossOrigin=&quot;anonymous&quot;属性，同时把html2pdf.js的options里的html2canvas.useCORS设为true。&lt;/p&gt;
&lt;h3&gt;坑三：html2pdf.js导出的PDF文字太小或者太模糊&lt;/h3&gt;
&lt;p&gt;这个问题主要是因为html2canvas的scale设置得太小，默认是1，也就是和屏幕分辨率一样，打印出来会很糊，解决方法是把scale设成2或者3，但不要设得太大，不然包体积会超级大，而且生成时间会很长，jsPDF的unit最好设成mm或者cm，format设成a4或者你需要的纸张大小,这样文字的位置会更准确。&lt;/p&gt;
&lt;h3&gt;坑四：vue-pdf-embed的文字层不显示或者错位&lt;/h3&gt;
&lt;p&gt;如果你需要让用户选文字复制的话，记得要开vue-pdf-embed的show-text属性（默认是开的），如果文字层错位的话，可能是因为你的PDF容器的宽度或者高度设得不对，或者是PDF本身的问题，可以试试给vue-pdf-embed组件加一个固定的宽度，比如100%,然后让它自适应高度。&lt;/p&gt;
&lt;h2&gt;根据自己的需求选对方案就行&lt;/h2&gt;
&lt;p&gt;其实做Vue3 PDF相关功能，核心就是选对技术方案，然后避开那几个常见的坑就行了，如果只是纯渲染，用vue-pdf-embed；如果是动态生成复杂PDF，用html2pdf.js；如果是高级需求，用pdf-lib，要是需要同时实现预览、导出、加安全限制，那可以把几个库结合起来用，比如先用html2pdf.js生成PDF的Blob，然后用pdf-lib给它加密码和水印，再用vue-pdf-embed渲染,最后导出。&lt;/p&gt;
&lt;p&gt;好了，今天的分享就到这里，你要是还有什么具体的问题，可以在评论区留言,我会尽量帮你解答。&lt;/p&gt;</description><pubDate>Sun, 02 Aug 2026 08:01:06 +0800</pubDate></item><item><title>Vue3为什么要弃用Object.defineProperty改用Proxy？这俩到底有啥不一样？</title><link>https://www.codeqd.com/post/20260821977.html</link><description>&lt;p&gt;最近很多转Vue3的前端小伙伴会发现，原来在Vue2里天天打交道的数据响应式核心原理“Object.defineProperty”，居然被Vue3彻底换成了“Proxy”，刚接触的时候可能会觉得只是换了个API的名字，写代码的方式没太变？但深入一点写，比如给对象动态加新属性、给数组改长度或者用下标直接改，不用再像Vue2那样手动调$set、$delete，还有更复杂的情况比如嵌套了好几层的对象、集合类（Map、Set这些），Vue3的响应式居然都能自动生效，不用提前处理，这背后到底藏着Proxy什么厉害的地方？今天咱们就好好掰扯清楚这俩东西的区别，还有Vue3弃旧用新的真正原因，不是跟风,是真的解决了Vue2一堆让人头疼的痛点问题。&lt;/p&gt;
&lt;h2&gt;先搞懂基础：Vue2的响应式是怎么靠Object.defineProperty干活的？&lt;/h2&gt;
&lt;p&gt;要说清楚Proxy的好，得先看看Object.defineProperty的局限，没有对比就没有伤害嘛，那咱们先回忆一下，或者简单说下新手能懂的Vue2响应式逻辑——其实就是把一个普通对象变成“响应式对象”的过程,核心是拦截属性的读取和修改操作。&lt;/p&gt;
&lt;p&gt;Vue2会在初始化组件data的时候，遍历data里的所有属性（包括嵌套对象的每一层），给每一个属性都用Object.defineProperty重新定义一遍getter和setter，getter的作用是当代码读取这个属性的时候，比如模板里用了{{ name }}，就悄悄把用这个属性的地方（也就是“依赖”，可以理解成哪个模板、哪个计算属性或者watch用到了它）存下来；setter的作用是当代码修改这个属性的时候，比如写this.name = &#039;新名字&#039;，就把之前存的依赖都找出来,通知它们更新视图或者重新计算。&lt;/p&gt;
&lt;p&gt;哦对了，数组的处理有点特殊，因为如果给数组的每一个下标都用Object.defineProperty定义，性能会特别差——比如一个有1000个元素的数组，得遍历1000次，Vue2觉得这没必要，所以它单独处理了数组的7个常用方法：push、pop、shift、unshift、splice、sort、reverse，这些方法被Vue2“重写”了，底层还是调用原来的数组方法，但调用前后会加一点逻辑：调用前先拿到数组的依赖,调用后触发这些依赖的更新。&lt;/p&gt;
&lt;p&gt;那这个时候，你如果直接用下标改数组，比如this.list[0] = &#039;新元素&#039;，或者改数组的length，比如this.list.length = 5，因为Object.defineProperty没给数组的下标和length属性定义过getter和setter（下标没遍历到，length没处理），所以Vue2根本不知道数据变了，视图也就不会更新，这时候就得用$set或者splice手动触发，这就是咱们之前常说的“Vue2数组的坑”。&lt;/p&gt;
&lt;p&gt;除了数组的坑，Object.defineProperty还有别的问题吗？当然有，咱们后面说Proxy的时候再对比，现在先把基础逻辑记牢：Vue2是&lt;strong&gt;逐个属性拦截&lt;/strong&gt;，不管对象还是数组，处理的时候得先“递归遍历+提前定义”,漏掉的就响应式失效。&lt;/p&gt;
&lt;h2&gt;再来聊核心：Proxy到底是什么？它的原理和Object.defineProperty有啥本质区别？&lt;/h2&gt;
&lt;p&gt;Proxy，中文翻译叫“代理”，这个名字其实特别形象——它不是直接修改原来的对象，而是给原来的对象“套”了一层代理层，然后我们所有对原来对象的操作（读取属性、修改属性、添加新属性、删除属性、遍历对象、甚至是调用函数、访问原型链这些操作），都必须先经过这层代理层，代理层想拦截哪个操作，就拦截哪个操作，拦截到之后可以做任何你想做的事，比如存依赖、通知更新、打印日志、修改参数、甚至是拒绝操作。&lt;/p&gt;
&lt;p&gt;哦对了，Proxy是ES6（也就是ECMAScript 2015）才新增的原生API，之前没有，所以Vue2的时候浏览器还没普遍支持ES6的话，就没法用，这也是为什么Vue2只能用Object.defineProperty的一个小原因，但不是主要原因,主要原因还是它俩的能力差异。&lt;/p&gt;
&lt;p&gt;那和Vue2的Object.defineProperty对比，本质区别到底在哪？
第一个本质区别：&lt;strong&gt;拦截的范围不一样&lt;/strong&gt;，Object.defineProperty只能拦截“已有属性”的“读取和修改”两个操作，刚才说过了，漏一个属性都不行，比如你动态给对象加一个新属性，它根本没被定义过getter和setter，自然就响应式失效了，这时候就得用$set；删除属性也是一样，Object.defineProperty没定义“删除属性”的拦截，所以用delete删除属性，Vue2也不知道，视图不会变，得用$delete，但Proxy不一样，它拦截的是&lt;strong&gt;整个对象的所有操作&lt;/strong&gt;，不管你是读取已有属性、修改已有属性、加新属性、删旧属性、遍历对象、访问Symbol属性、甚至是操作数组的下标和length，都能直接被代理层拦截到，不用提前递归遍历所有属性，也不用提前重写方法，自然就没有$set和$delete的必要了。
第二个本质区别：&lt;strong&gt;处理嵌套对象的方式不一样&lt;/strong&gt;，Vue2是&lt;strong&gt;在初始化的时候就递归遍历嵌套对象的每一层，给每一个属性都定义getter和setter&lt;/strong&gt;，不管你后来会不会用到这些嵌套属性——比如你的data里有个user对象，user对象里又有个address对象，address对象里又有个city属性，就算你整个应用从来没用到过city，Vue2在初始化的时候还是会递归到city那一层，给它定义好，这就导致了初始化阶段的性能浪费，尤其是当你的data里有非常大、嵌套非常深的对象的时候，但Proxy不一样，它是&lt;strong&gt;懒拦截&lt;/strong&gt;——也就是只有当你真正访问到嵌套对象的时候，才会给那个嵌套对象再套一层代理层，比如你第一次访问user.address的时候，代理层发现user.address是个普通对象，不是响应式的，就立刻给它套一层Proxy，变成响应式的，之后你再访问user.address.city，就会触发address代理层的getter，再给city套层代理？不对，city如果是个基本数据类型（比如字符串、数字、布尔值），就不需要套代理了，只有对象、数组、集合这些引用类型才需要套，这种懒拦截的方式，就大大减少了初始化阶段的性能消耗，尤其是大对象、深嵌套的场景。
第三个本质区别：&lt;strong&gt;能处理的数据类型不一样&lt;/strong&gt;，Object.defineProperty只能处理“普通对象”和“数组”的部分功能，集合类（Map、Set、WeakMap、WeakSet）这些ES6新增的数据结构，它根本没法处理——比如你在Vue2的data里放个Map，往里面加个键值对，Object.defineProperty根本拦截不到，视图肯定不会更新，但Proxy不一样，它是原生支持所有引用类型的代理的，不管是普通对象、数组、函数、Date、RegExp，还是Map、Set这些集合类，都能直接套上Proxy，所有操作都能被拦截，所以Vue3的响应式现在能完美支持这些集合类了,不用再自己封装一堆东西。&lt;/p&gt;
&lt;h2&gt;那Vue3用了Proxy之后，具体解决了Vue2哪些让人头疼的痛点？&lt;/h2&gt;
&lt;p&gt;刚才讲本质区别的时候其实已经提到了一些，咱们现在把这些痛点整理得更具体、更贴近实际开发场景一点，这样转Vue3的小伙伴一看就知道“哦，原来这个烦了我好久的问题，现在终于解决了”。&lt;/p&gt;
&lt;h3&gt;动态给对象加新属性、删旧属性，必须用$set、$delete&lt;/h3&gt;
&lt;p&gt;这个应该是Vue2开发里最常见的坑了吧？比如你在data里初始化了一个user对象，只有name和age两个属性，后来通过接口拿到了用户的gender属性，你直接写this.user.gender = &#039;女&#039;，结果视图里的{{ user.gender }}根本不显示，你得改成this.$set(this.user, &#039;gender&#039;, &#039;女&#039;)；后来用户说不想显示年龄了，你直接写delete this.user.age，结果视图里的{{ user.age }}还在，你得改成this.$delete(this.user, &#039;age&#039;)，很多新手刚学Vue2的时候，都会在这上面踩好几次坑，就算是老手，有时候也会不小心忘写$set或者$delete,然后找半天bug。&lt;/p&gt;
&lt;p&gt;但在Vue3里，完全不用这个麻烦了！比如你用ref或者reactive定义了一个user对象，直接this.user.gender = &#039;女&#039;，或者delete this.user.age，视图都会自动更新，因为Proxy拦截到了“添加属性”和“删除属性”的操作,会自动触发依赖更新。&lt;/p&gt;
&lt;h3&gt;直接用下标改数组、改数组length，视图不更新&lt;/h3&gt;
&lt;p&gt;这个也是Vue2的经典坑，刚才也提到过，比如你在data里初始化了一个list数组，是[&#039;a&#039;, &#039;b&#039;, &#039;c&#039;]，后来你想把第一个元素改成&#039;x&#039;，直接写this.list[0] = &#039;x&#039;，视图里还是显示&#039;a&#039;，你得改成this.$set(this.list, 0, &#039;x&#039;)或者this.list.splice(0, 1, &#039;x&#039;)；后来你想把数组长度改成2，直接写this.list.length = 2，视图里还是显示三个元素，你得改成this.list.splice(2)。&lt;/p&gt;
&lt;p&gt;但在Vue3里，这些操作都没问题了！比如你用ref或者reactive定义了一个list数组，直接this.list[0] = &#039;x&#039;，或者this.list.length = 2，视图都会自动更新，因为Proxy拦截到了“修改数组下标”和“修改数组length”的操作,会自动触发依赖更新。&lt;/p&gt;
&lt;h3&gt;初始化大对象、深嵌套对象时，性能浪费&lt;/h3&gt;
&lt;p&gt;刚才讲懒拦截的时候也提到了，比如你在Vue2的data里放了一个从接口拿到的超级大的商品列表，每个商品对象里又嵌套了商品详情、商品评价、商品规格这些对象，商品规格里又嵌套了颜色、尺寸这些对象，就算你整个应用在首页只用到了商品的id、name、price三个属性，Vue2在初始化的时候还是会递归遍历所有商品的所有嵌套属性，给每一个属性都定义getter和setter，这就会导致首页的加载时间变长，用户体验不好，尤其是在低端手机或者网络慢的情况下,这个问题会更明显。&lt;/p&gt;
&lt;p&gt;但在Vue3里，这个问题就好多了！因为Proxy是懒拦截的，只有当你真正访问到某个嵌套属性的时候，才会给那个引用类型套上代理层，比如首页只用到了商品的id、name、price，那Vue3只会给商品列表套一层代理，然后给每个商品对象套一层代理，然后当你访问某个商品的id、name、price的时候，因为它们是基本数据类型，就不会再套代理了，这样初始化阶段的性能消耗就大大减少了,首页加载速度也会变快。&lt;/p&gt;
&lt;h3&gt;没法直接支持ES6的集合类（Map、Set、WeakMap、WeakSet）&lt;/h3&gt;
&lt;p&gt;比如你在Vue2里想做一个多选框组件，用Set来存选中的id，因为Set自动去重的特性特别适合这个场景，但你直接在data里放个Set，往里面加id或者删id，视图根本不会更新，你得自己封装一个响应式的Set，比如重写add、delete、clear这些方法，在调用前后触发依赖更新，这就很麻烦,而且封装不好的话还容易出bug。&lt;/p&gt;
&lt;p&gt;但在Vue3里，完全不用自己封装了！比如你用ref或者reactive定义了一个selectedIds的Set，直接selectedIds.add(1)，或者selectedIds.delete(2)，视图都会自动更新，因为Proxy原生支持这些集合类的代理，所有操作都能被拦截,自动触发依赖更新。&lt;/p&gt;
&lt;h3&gt;对函数、Date、RegExp这些特殊引用类型的支持不好&lt;/h3&gt;
&lt;p&gt;比如你在Vue2里想存一个响应式的Date对象，修改它的年份，比如this.date.setFullYear(2025)，结果视图根本不会更新，因为Object.defineProperty只能拦截Date对象属性的读取和修改，没法拦截Date对象的方法调用，你得自己封装一个响应式的Date，比如重写setFullYear、setMonth这些方法，或者每次修改Date的时候，都重新赋值一个新的Date对象，比如this.date = new Date(2025, this.date.getMonth(), this.date.getDate()),这也很麻烦。&lt;/p&gt;
&lt;p&gt;但在Vue3里，这个问题也解决了！比如你用ref或者reactive定义了一个date对象，直接this.date.setFullYear(2025)，视图都会自动更新吗？等下，这里有个小细节，得说清楚：如果是用reactive定义的Date对象，Proxy确实能拦截到方法调用吗？不对，其实Date、RegExp这些内置对象，它们的内部状态是存储在内部槽里的，不是存储在公开属性里的，所以如果直接给它们套一层Proxy，调用它们的方法的时候，内部槽可能会找不到，从而导致报错或者不生效，那Vue3是怎么解决这个问题的？哦对了，Vue3的reactive内部对这些内置对象做了特殊处理，比如Date对象，Vue3会给它套一个自定义的代理层，当调用setFullYear、setMonth这些修改内部状态的方法的时候，会自动触发依赖更新，当调用getFullYear、getMonth这些读取内部状态的方法的时候，也会自动收集依赖，所以你在Vue3里用reactive定义一个Date对象，直接调用它的修改方法，视图是会自动更新的,不用再自己封装或者重新赋值了。&lt;/p&gt;
&lt;h2&gt;那Proxy有没有什么缺点？为什么Vue2的时候不用？&lt;/h2&gt;
&lt;p&gt;刚才说了一堆Proxy的优点，好像它是完美的，但其实它也有一个小小的缺点，还有一个之前的限制，咱们也得客观地说一下,不能只吹它的好。&lt;/p&gt;
&lt;h3&gt;缺点：没法完全兼容低版本浏览器&lt;/h3&gt;
&lt;p&gt;Proxy是ES6才新增的原生API，那低版本浏览器肯定不支持啊，比如IE11及以下的浏览器，完全不支持Proxy，而且Polyfill（也就是浏览器垫片，用来给低版本浏览器补新API的）也没法完全模拟Proxy的所有功能——因为Proxy是底层的API，它的拦截能力太强了，很多操作Polyfill根本模拟不出来，比如拦截函数的调用、拦截原型链的访问、拦截集合类的操作这些。&lt;/p&gt;
&lt;p&gt;那为什么Vue2的时候不用？哦，刚才已经提到过了，就是因为之前浏览器的兼容性问题——在Vue2刚出来的时候（大概2016年），ES6还没普及，IE11还有很多用户，要是用了Proxy，IE11的用户就完全用不了了，所以Vue2只能用兼容性更好的Object.defineProperty，但现在不一样了，IE11已经基本退出历史舞台了，国内的话，比如微信浏览器、支付宝浏览器、各大国产浏览器的最新版本，还有Chrome、Firefox、Safari这些主流浏览器的最新版本，都已经完全支持Proxy了,所以Vue3就果断弃旧用新了。&lt;/p&gt;
&lt;p&gt;那如果你的项目确实需要兼容IE11怎么办？哦，那你还是得用Vue2，或者用Vue3的“Legacy Build”——也就是兼容低版本浏览器的版本，但Legacy Build的响应式核心还是用的Object.defineProperty，所以它还是有Vue2的那些痛点，比如需要用$set、$delete，没法直接用下标改数组，没法直接支持集合类这些，相当于你用了Vue3的语法，但用的还是Vue2的响应式核心，有点“换汤不换药”的感觉，所以如果必须兼容IE11的话,还是直接用Vue2更稳妥。&lt;/p&gt;
&lt;h2&gt;那在Vue3里，Proxy是怎么和ref、reactive配合工作的？&lt;/h2&gt;
&lt;p&gt;刚才咱们一直在说Proxy的原理和优点，那它在Vue3的实际开发里是怎么用的？其实咱们平时写代码的时候，不会直接用new Proxy()来创建响应式对象，而是用Vue3提供的两个API：ref和reactive，这两个API的底层其实都是用Proxy实现的，咱们简单说一下它们的底层逻辑,这样你能更深入地理解Vue3的响应式。&lt;/p&gt;
&lt;h3&gt;先说reactive&lt;/h3&gt;
&lt;p&gt;reactive是用来给&lt;strong&gt;引用类型&lt;/strong&gt;（普通对象、数组、集合类这些）创建响应式对象的，它的底层逻辑就是直接给传入的引用类型套一层Proxy，然后通过Proxy的getter和setter来收集依赖和触发更新，哦对了，刚才讲懒拦截的时候也提到了，如果传入的引用类型里还有嵌套的引用类型，reactive不会在初始化的时候就给它们套Proxy，只有当你真正访问到它们的时候，才会给它们套,这样就减少了性能消耗。&lt;/p&gt;
&lt;h3&gt;再说ref&lt;/h3&gt;
&lt;p&gt;ref是用来给&lt;strong&gt;基本数据类型&lt;/strong&gt;（字符串、数字、布尔值、null、undefined、Symbol这些）和&lt;strong&gt;引用类型&lt;/strong&gt;都能创建响应式对象的，那为什么基本数据类型也能创建响应式对象？因为Proxy只能代理引用类型，不能代理基本数据类型，所以ref的底层逻辑是：把传入的值包装成一个“普通对象”，这个普通对象只有一个value属性，然后给这个普通对象套一层Proxy？不对，其实ref的底层逻辑是用了Object.defineProperty吗？哦，不对，刚才讲了Vue3的响应式核心是Proxy，那ref是怎么处理基本数据类型的？等下，咱们可以看一下Vue3的源码（大概的逻辑，不用看具体代码）：其实ref创建的是一个RefImpl类的实例，这个实例有一个value属性，还有一个dep属性（用来存依赖的），当你读取ref.value的时候，会收集依赖；当你修改ref.value的时候，会触发依赖更新，那这个收集依赖和触发更新的逻辑，是不是用了Proxy？哦，不对，RefImpl类的实例是个普通对象，它的value属性是用Object.defineProperty定义的吗？还是说Vue3的响应式有一套统一的依赖收集和触发更新的机制，不管是reactive用的Proxy，还是ref用的RefImpl，都会调用这套机制？哦对，Vue3的响应式有一套核心的API：track（用来收集依赖）和trigger（用来触发更新），不管是reactive的Proxy的getter里，还是ref的RefImpl的value的getter里，都会调用track；不管是reactive的Proxy的setter里，还是ref的RefImpl的value的setter里，都会调用trigger，所以其实ref的底层不是直接用Proxy，但是它和reactive用的是同一套依赖收集和触发更新的机制，而且如果ref传入的是引用类型，那ref.value其实就是用reactive包装过的响应式对象,所以ref也能享受到Proxy的所有优点。&lt;/p&gt;
&lt;h2&gt;写在最后：Vue3用Proxy是技术进步的必然选择&lt;/h2&gt;
&lt;p&gt;说了这么多，咱们可以总结一下：Vue3弃用Object.defineProperty改用Proxy，不是为了炫技，也不是为了跟风，而是因为Proxy真的解决了Vue2一堆让人头疼的痛点问题，比如动态加删属性不用$set、$delete，直接用下标改数组不用splice，初始化大对象性能更好，能直接支持ES6的集合类和特殊引用类型这些，而且现在浏览器的兼容性问题已经基本解决了,所以Vue3用Proxy是技术进步的必然选择。&lt;/p&gt;
&lt;p&gt;对于转Vue3的小伙伴来说，不用太担心Proxy的原理有多复杂，其实平时写代码的时候，只要记住用ref和reactive来定义响应式数据就行，不用直接用new Proxy()，但了解一下Proxy的原理，能帮助你更好地理解Vue3的响应式，避免踩一些不必要的坑，比如不要把reactive定义的响应式对象直接解构赋值给普通变量，不然会失去响应式（这个坑也是因为Proxy的原理，解构赋值相当于把响应式对象的属性值取出来，赋值给普通变量，普通变量没有Proxy的代理层，自然就响应式失效了，要是想解构赋值还保持响应式，可以用toRefs或者toRef）。&lt;/p&gt;
&lt;p&gt;好了，今天的分享就到这里，希望能帮助你更好地理解Vue3的Proxy和响应式原理，要是还有什么疑问,欢迎在评论区留言讨论哦。&lt;/p&gt;</description><pubDate>Sat, 01 Aug 2026 20:02:54 +0800</pubDate></item></channel></rss>