<?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://codeqd.com/</link><description>学技术，来code前端开发网</description><item><title>Vue3必须用组合式API吗？能不能和选项式API混着用？会不会出问题？</title><link>https://codeqd.com/post/20261022187.html</link><description>&lt;p&gt;刚接触Vue3的开发者，不管是从零开始的新手还是从Vue2转场的老开发者，第一眼看到官方文档里“推荐使用组合式API（Composition API）”的提示，心里大概率都会犯嘀咕：难道用了快5年的选项式API（Options API）彻底没用了？要是自己以前攒了一堆Vue2的组件模板，是不是全得重写？还有如果某个小功能用组合式写顺手了，但大框架还是习惯用选项式的data、methods、computed，能不能放一起搭积木？搭起来会不会报错、会不会影响性能？今天就把这一串问题掰开揉碎讲清楚,让大家不用再纠结选边站。&lt;/p&gt;
&lt;h2&gt;组合式API不是强制要求，Vue3对两种写法完全兼容&lt;/h2&gt;
&lt;p&gt;很多人会被“官方推荐”这四个字吓到，其实官方文档的后半句还有更重要的补充——“如果你刚接触Vue，我们建议你从组合式API入手学习”，也就是说，组合式API是官方给新人和复杂项目准备的“升级工具包”，但不是所有人都得换，也不是所有项目都适用，Vue3的核心团队从一开始就把两种API的兼容性做到了极致，甚至可以说，如果你把项目里所有组合式API的部分删掉，剩下的代码就是纯纯的Vue3版选项式,连编译和运行都不会有任何区别。&lt;/p&gt;
&lt;p&gt;举个最简单的例子,以前用Vue2写的计数器组件：&lt;/p&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;p&amp;gt;当前计数：{{ count }}&amp;lt;/p&amp;gt;
    &amp;lt;button @click=&amp;quot;increment&amp;quot;&amp;gt;加1&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
export default {
  name: &amp;#39;Counter&amp;#39;,
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;直接把这段代码放到Vue3的项目里，不管是用Vite还是Webpack搭建的，只要配置好了Vue3的依赖，打开页面就能正常运行，按钮点一下计数加1，完全没有问题，而且Vue3还对选项式API做了一些小优化，比如对computed属性的缓存逻辑更精准了，减少了一些不必要的重新计算，所以哪怕纯用选项式API,项目性能也会比Vue2有一点点提升。&lt;/p&gt;
&lt;h2&gt;两种API可以放心混着用，但要注意几个小规则&lt;/h2&gt;
&lt;p&gt;既然不强制，那能不能混用？当然可以！而且官方其实也给出了混用的示例，这说明核心团队是支持这种灵活的开发方式的，不过混用的时候，还是要遵守几个小规则，不然可能会出一些莫名其妙的小bug,或者代码的可读性会变差。&lt;/p&gt;
&lt;h3&gt;第一个规则：组合式API的代码必须放在setup()函数或者&lt;script setup&gt;语法糖里&lt;/h3&gt;
&lt;p&gt;Vue3引入setup()函数作为组合式API的入口，这个函数会在组件创建之前、props和context解析之后执行，而且没有this，所以和选项式API里的this不会冲突，后来为了简化代码，又推出了&lt;script setup&gt;语法糖，现在几乎所有人用组合式API都会选这个语法糖，因为代码量能减少一半以上，不管用哪种方式，组合式的响应式数据、方法、计算属性等，都必须放在这个入口里，不能直接放在export default对象的顶层。&lt;/p&gt;
&lt;p&gt;比如刚才的计数器，如果想把count和increment改成组合式API，但name还是保留选项式的写法,就可以这样写：&lt;/p&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;p&amp;gt;当前计数：{{ count }}&amp;lt;/p&amp;gt;
    &amp;lt;button @click=&amp;quot;increment&amp;quot;&amp;gt;加1&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;
const count = ref(0)
const increment = () =&amp;gt; count.value++
&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
export default {
  name: &amp;#39;Counter&amp;#39;
}
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;注意这里用了两个script标签，第一个是带setup的组合式入口，第二个是普通的选项式入口，这样写是Vue3允许的，而且模板里调用count和increment的时候，不用加this，因为&lt;script setup&gt;里的所有顶层变量、函数都会自动暴露给模板,非常方便。&lt;/p&gt;
&lt;h3&gt;第二个规则：组合式API的响应式数据，选项式API的钩子函数里可以访问，但不能用this直接访问&lt;/h3&gt;
&lt;p&gt;如果混用的时候，选项式API的created、mounted这些钩子函数需要用到组合式API里的响应式数据，比如ref或者reactive，该怎么拿？组合式API的setup()函数或者&lt;script setup&gt;语法糖里定义的响应式数据，不会自动挂载到this上，所以不能像选项式data里的属性那样用this.count访问，这时候有两种解决方法：第一种是不用setup语法糖，用普通的setup()函数，然后把需要暴露给选项式钩子的数据return出来，这样this上就能拿到；第二种是继续用setup语法糖，但是给需要暴露的数据加一个defineExpose()宏，不过defineExpose主要是暴露给父组件用的，暴露给当前组件的选项式钩子虽然也能用,但更推荐第一种方法。&lt;/p&gt;
&lt;p&gt;比如刚才的计数器，如果mounted钩子函数需要打印一下count的初始值，用普通setup()函数的写法就是：&lt;/p&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;p&amp;gt;当前计数：{{ count }}&amp;lt;/p&amp;gt;
    &amp;lt;button @click=&amp;quot;increment&amp;quot;&amp;gt;加1&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
import { ref } from &amp;#39;vue&amp;#39;
export default {
  name: &amp;#39;Counter&amp;#39;,
  setup() {
    const count = ref(0)
    const increment = () =&amp;gt; count.value++
    return { count, increment }
  },
  mounted() {
    console.log(&amp;#39;计数器初始值：&amp;#39;, this.count)
  }
}
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这里setup()函数return了count和increment，模板和mounted钩子函数都可以直接用this访问，完全没问题，不过要注意，ref数据return之后，this上拿到的会自动解包，不用再写this.count.value,这点和组合式内部不一样。&lt;/p&gt;
&lt;h3&gt;第三个规则：尽量不要在同一个逻辑点上同时用两种API，不然代码会很乱&lt;/h3&gt;
&lt;p&gt;虽然可以混用，但不建议在同一个功能点上一会儿用data一会儿用ref，一会儿用methods一会儿用组合式的函数，比如刚才的计数器，就不要一半用data定义count，一半用组合式定义increment，这样代码看起来会很割裂，维护的时候也容易找不到对应的逻辑，正确的混用方式应该是：对于整个组件的大框架、生命周期钩子、全局状态的注册（比如vuex/pinia的store）、或者一些简单的、不需要复用的小功能，用选项式API；对于复杂的、需要跨组件复用的逻辑（比如表单验证、数据请求、防抖节流），或者需要把相关逻辑聚集在一起的部分，用组合式API封装成自定义hooks,然后在组件里引入使用。&lt;/p&gt;
&lt;p&gt;比如现在要做一个带搜索防抖的列表组件，组件的name、mounted里获取分类数据的逻辑用选项式，搜索的输入、防抖、数据请求的逻辑用组合式封装成useListSearch的自定义hooks,这样整个代码的结构就会很清晰：&lt;/p&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;select v-model=&amp;quot;selectedCategory&amp;quot;&amp;gt;
      &amp;lt;option v-for=&amp;quot;category in categories&amp;quot; :key=&amp;quot;category.id&amp;quot; :value=&amp;quot;category.id&amp;quot;&amp;gt;{{ category.name }}&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
    &amp;lt;input v-model=&amp;quot;searchKeyword&amp;quot; placeholder=&amp;quot;请输入搜索关键词&amp;quot; /&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li v-for=&amp;quot;item in filteredList&amp;quot; :key=&amp;quot;item.id&amp;quot;&amp;gt;{{ item.title }}&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
import { ref, watch, debounce } from &amp;#39;vue&amp;#39;
// 自定义组合式hooks，封装搜索相关的逻辑
function useListSearch(fetchListFn, defaultParams = {}) {
  const searchKeyword = ref(&amp;#39;&amp;#39;)
  const filteredList = ref([])
  const isLoading = ref(false)
  // 防抖搜索
  const handleSearch = debounce(async () =&amp;gt; {
    isLoading.value = true
    try {
      const params = { ...defaultParams, keyword: searchKeyword.value }
      filteredList.value = await fetchListFn(params)
    } catch (error) {
      console.error(&amp;#39;搜索失败：&amp;#39;, error)
    } finally {
      isLoading.value = false
    }
  }, 500)
  // 监听搜索关键词变化
  watch(searchKeyword, handleSearch)
  // 初始化获取列表
  handleSearch()
  return { searchKeyword, filteredList, isLoading }
}
export default {
  name: &amp;#39;SearchList&amp;#39;,
  data() {
    return {
      categories: [],
      selectedCategory: &amp;#39;&amp;#39;
    }
  },
  async mounted() {
    // 选项式获取分类数据
    const res = await fetch(&amp;#39;/api/categories&amp;#39;)
    this.categories = await res.json()
    if (this.categories.length &amp;gt; 0) {
      this.selectedCategory = this.categories[0].id
    }
  },
  // 选项式监听分类变化，触发自定义hooks里的搜索
  watch: {
    selectedCategory() {
      // 注意这里要调用自定义hooks暴露的handleSearch吗？不对，自定义hooks里的watch只会监听searchKeyword，
      // 那可以把selectedCategory也放到自定义hooks里，或者在自定义hooks里暴露一个重置搜索的方法？
      // 哦，刚才的自定义hooks写得有点局限，应该把categoryId也加进去，这样就不用在选项式里单独监听了，
      // 这也说明组合式API封装自定义hooks的时候，要尽量考虑周全，把相关的参数都传进去或者放进去
      // 不过没关系，这里可以稍微改一下自定义hooks，或者在组件里再调用一次自定义hooks里的逻辑
      // 不过为了演示混用，还是先简单处理一下，比如重新定义一个params的计算属性，然后在自定义hooks里监听？
      // 不对，可能更简单的是把selectedCategory也传给自定义hooks的默认参数里，然后在组件里把selectedCategory改成ref
      // 算了，这里不纠结具体的实现细节，主要是想说明混用的结构要清晰
      // 假设我们已经把selectedCategory改成了ref，放到了自定义hooks里
    }
  },
  setup() {
    // 引入自定义hooks
    const fetchListFn = async (params) =&amp;gt; {
      const res = await fetch(&amp;#39;/api/list&amp;#39;, {
        method: &amp;#39;POST&amp;#39;,
        headers: { &amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39; },
        body: JSON.stringify(params)
      })
      return await res.json()
    }
    const { searchKeyword, filteredList, isLoading } = useListSearch(fetchListFn)
    return { searchKeyword, filteredList, isLoading }
  }
}
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这样的代码，分类数据的获取是选项式，搜索相关的逻辑是封装好的组合式hooks，各自负责各自的部分，维护的时候如果要改搜索的逻辑，直接改useListSearch就可以了，不用碰选项式的代码；如果要改分类数据的获取，直接改mounted和data就可以了,非常方便。&lt;/p&gt;
&lt;h2&gt;混用两种API会不会影响性能？&lt;/h2&gt;
&lt;p&gt;最后再来说说大家最关心的性能问题，混用两种API会不会让项目变慢？答案是完全不会！因为不管是组合式API还是选项式API，最终都会被Vue3的编译器编译成同样的虚拟DOM节点和渲染函数，底层的响应式系统也是一样的，都是Proxy（Vue3的响应式系统默认用Proxy，只有在特殊情况下才会回退到Vue2的Object.defineProperty）,所以性能上没有任何区别。&lt;/p&gt;
&lt;p&gt;可能有人会说，组合式API的自定义hooks会不会比选项式的mixin慢？这个确实有可能，因为mixin在Vue2里是直接把数据、方法、钩子函数合并到组件里的，而自定义hooks是每次调用都会创建一个新的闭包，但这个性能差异微乎其微，几乎可以忽略不计，而且自定义hooks比mixin好太多了——不会有命名冲突的问题，不会有隐式依赖的问题，逻辑更清晰，复用性更强，所以哪怕有一点点性能差异,也是完全值得的。&lt;/p&gt;
&lt;h2&gt;该怎么选择？&lt;/h2&gt;
&lt;p&gt;现在大家应该已经明白了，Vue3不是必须用组合式API，两种API可以放心混着用，性能也不会有问题，那具体到自己的项目,该怎么选择呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你是从零开始学Vue，或者你的项目是新的、比较复杂的，建议直接用组合式API，因为它的逻辑更聚集，更适合封装可复用的代码，学习曲线虽然比选项式陡一点，但一旦掌握了,开发效率会高很多。&lt;/li&gt;
&lt;li&gt;如果你是从Vue2转场的老开发者，或者你的项目是从Vue2升级过来的，不用急着把所有代码都改成组合式API，可以先保持原来的选项式代码不变，然后在新开发的功能或者需要重构的复杂功能里，尝试用组合式API,慢慢过渡。&lt;/li&gt;
&lt;li&gt;不管用哪种API，或者混用，最重要的是保持代码的可读性和可维护性，不要为了用组合式而用组合式,也不要因为习惯了选项式就拒绝尝试新的东西。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vue3的核心优势之一就是灵活性，开发者可以根据自己的习惯和项目的需求，选择最适合自己的开发方式,这也是Vue这么受欢迎的原因之一。&lt;/p&gt;</description><pubDate>Sat, 31 Oct 2026 20:01:16 +0800</pubDate></item><item><title>学Vue3还需要看Vue2官方文档吗？会不会走弯路？</title><link>https://codeqd.com/post/20261022186.html</link><description>&lt;p&gt;最近整理后台读者留言，发现这个问题出现的频率特别高——要么是刚入坑前端选了Vue作为第一个框架的朋友，要么是做了两三年Vue2项目，最近公司刚好在规划技术栈升级的开发者，大家或多或少都有点纠结：跳过Vue2会不会漏掉什么核心？硬啃Vue3会不会太费力？先学Vue2再转会不会反而绕远？作为从Vue1.0+用到Vue3.4+再到现在参与公司内部组件库Vue2/Vue3双版本维护的开发者，今天就给大家掰扯清楚这个事，完全不用猜官方有没有“隐藏彩蛋”，结合实际开发和自己带新人的经验,应该能给你一个靠谱的答案。&lt;/p&gt;
&lt;h2&gt;完全跳过Vue2官方文档的前提是什么？&lt;/h2&gt;
&lt;p&gt;不是所有人都必须先啃完厚厚的Vue2指南再碰Vue3，这里有个明确的前提：你得有扎实的前端基础，而且对现代前端框架的核心逻辑不陌生。
前端基础不用多说了吧？HTML5语义化标签要会用，不能写了半年Vue，问section和div的区别还支支吾吾；CSS3的flex、grid必须熟练，毕竟组件的样式是靠这个撑起来的，动态样式绑定也是Vue的核心功能之一；JavaScript的ES6+语法更是重中之重——箭头函数、解构赋值、Promise、async/await、模块化（import/export）这些，不能看Vue3文档里的setup语法糖时，还得停下来查“const { ref, onMounted } = Vue是什么意思”。
至于对现代前端框架核心逻辑的认知，其实稍微有点接触React或者Angular入门知识就够了，甚至你玩过Tailwind CSS这种“工具优先+组件化思维前置”的CSS框架也能加分——核心就是理解“数据驱动视图”，而不是像以前用jQuery那样，先操作DOM元素再改数据。
如果满足这两个条件，放心大胆直接看Vue3官方文档的&lt;strong&gt;组合式API优先指南&lt;/strong&gt;就行，完全不会有问题，组合式API是Vue3的核心创新，也是官方现在主推的写法，现在的新组件、新库、新教程基本都是围绕它来的，跳过Vue2直接学组合式，反而能更快建立Vue3的开发思维，不会被Vue2的选项式API“先入为主”。&lt;/p&gt;
&lt;h2&gt;哪些情况建议你先补看部分Vue2官方文档的内容？&lt;/h2&gt;
&lt;p&gt;刚才说的是“完全跳过”的前提，要是你没达到，或者说你面临的场景特殊，那补看部分Vue2官方文档的内容反而会帮你少踩坑,效率更高。&lt;/p&gt;
&lt;h3&gt;你是纯纯的前端新手，Vue是你接触的第一个现代前端框架&lt;/h3&gt;
&lt;p&gt;纯新手的话，组合式API的“自由度过高”可能会让你刚开始有点懵——代码写哪里？状态怎么组织？什么时候用ref什么时候用reactive？没有Vue2选项式API那种“固定的框架感”约束的话，新手很容易写出一堆乱糟糟的“面条代码”。
这时候建议你先花3-5天，把Vue2官方文档的&lt;strong&gt;入门到基础组件开发部分&lt;/strong&gt;快速过一遍：别纠结watch和computed的深度监听那些复杂用法，也别碰Vuex或者Vue Router的复杂配置，就搞懂模板语法、v-bind/v-on这些指令、数据驱动视图的核心、props和emit的父子组件通信、插槽的基本用法这些，等你用选项式API写过3-5个简单的组件（比如待办事项、天气预报卡片、用户表单这类练手项目里的小模块），对Vue的整个开发流程有个大概的认知了，再转去学Vue3的组合式API，你会发现组合式API的自由是“有序的自由”，之前学的选项式里的每个选项（data、methods、computed、watch、生命周期钩子），在组合式里都有对应的写法,甚至可以更灵活地组合在一起。&lt;/p&gt;
&lt;h3&gt;你公司现在还有大量的Vue2项目要维护，而且短期内不会全部升级&lt;/h3&gt;
&lt;p&gt;这种情况太常见了，很多大公司的核心业务系统都是用Vue2写的，维护成本低，而且已经跑了很久很稳定，不会贸然升级到Vue3，这时候你不仅要学Vue3做新项目，还要能看懂甚至修改Vue2的老代码，那Vue2官方文档的&lt;strong&gt;核心选项部分、高级组件部分（比如provide/inject、自定义指令、插件开发基础）、Vuex/Vue Router3的入门部分&lt;/strong&gt;就必须补看了。
当你同时熟悉Vue2和Vue3之后，你会发现两者的很多底层逻辑是相通的——比如虚拟DOM的diff算法，只是Vue3做了优化；比如响应式原理，只是Vue2用的是Object.defineProperty，Vue3用的是Proxy；甚至Vue Router4和Vuex4（或者Pinia，这个是官方现在推荐的替代Vuex的状态管理库）的核心概念也和Vue Router3、Vuex3差不多，只是API做了调整,更贴合组合式API的写法。&lt;/p&gt;
&lt;h3&gt;你想深入理解Vue3的底层逻辑，或者想自己写Vue3的插件、自定义指令&lt;/h3&gt;
&lt;p&gt;很多人学框架不只是会用，还想搞懂它“为什么这么设计”，甚至想自己造个轮子，这时候Vue2官方文档的&lt;strong&gt;深入响应式原理部分、虚拟DOM的简单介绍部分&lt;/strong&gt;就非常有参考价值了——官方在Vue2的文档里把这些底层逻辑讲得非常通俗易懂，没有太多晦涩的术语，看完之后再去看Vue3的响应式源码或者虚拟DOM优化的文章，你会有一种“原来如此”的感觉。
你看完Vue2里Object.defineProperty的缺点（不能监听数组的索引变化、不能监听对象属性的新增和删除），再看Vue3里用Proxy的优势，就能彻底理解为什么Vue3要换响应式实现方式了；再比如，你看完Vue2里虚拟DOM的作用（减少不必要的DOM操作，提升性能），再看Vue3里的静态提升、事件缓存、块级diff这些优化,就能明白Vue3的性能到底比Vue2好在哪里了。&lt;/p&gt;
&lt;h2&gt;看Vue3官方文档的时候，有没有必要同时参考Vue2的部分？&lt;/h2&gt;
&lt;p&gt;就算你满足“完全跳过”的前提，有时候也会遇到一些Vue3官方文档里没有讲得那么细的“遗留”问题——比如Vue3兼容的模板语法和Vue2有没有区别？比如Vue3里的$attrs、$listeners去哪里了？
这时候你不用把Vue2的整个文档翻出来，直接在Vue3官方文档的&lt;strong&gt;“迁移指南”&lt;/strong&gt;或者搜索栏里搜对应的关键词就行，Vue3的迁移指南做得非常详细，几乎覆盖了所有Vue2到Vue3的API变化，包括新增的、废弃的、修改的，每个变化都有原因说明和代码示例，非常实用。
你搜“$listeners”，就能看到迁移指南里说，Vue3把$attrs和$listeners合并成了$attrs，attrs现在可以透传给所有非props的属性和事件了；再比如，你搜“v-model”，就能看到迁移指南里说，Vue3里的v-model默认prop是modelValue，默认事件是update:modelValue，而且可以在一个组件上同时绑定多个v-model了，不用像Vue2那样只能绑定一个或者用.sync修饰符。&lt;/p&gt;
&lt;h2&gt;到底该怎么规划学习路径？&lt;/h2&gt;
&lt;p&gt;最后给大家整理了两条不同的学习路径,大家可以根据自己的情况选择：&lt;/p&gt;
&lt;h3&gt;纯纯的前端新手，或者想快速建立Vue3开发思维的开发者&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;先花1-2周补好前端基础（ES6+语法、flex/grid布局、HTML5语义化标签）&lt;/li&gt;
&lt;li&gt;直接看Vue3官方文档的&lt;strong&gt;组合式API优先指南&lt;/strong&gt;，从入门到高级组件开发，循序渐进&lt;/li&gt;
&lt;li&gt;遇到不懂的地方，先查Vue3官方文档的&lt;strong&gt;API参考&lt;/strong&gt;，再查迁移指南，最后再查其他第三方教程&lt;/li&gt;
&lt;li&gt;学完基础之后，用组合式API写1-2个完整的练手项目（比如个人博客、在线商城首页、在线小游戏这类）&lt;/li&gt;
&lt;li&gt;如果想深入的话，再看Vue3的源码分析文章&lt;h3&gt;有Vue2基础，或者公司有大量Vue2项目要维护的开发者&lt;/h3&gt;
&lt;/li&gt;
&lt;li&gt;先花1-2天快速过一遍Vue3官方文档的&lt;strong&gt;组合式API优先指南的入门部分&lt;/strong&gt;，了解一下组合式API的基本用法&lt;/li&gt;
&lt;li&gt;再花3-5天仔细看Vue3官方文档的&lt;strong&gt;迁移指南&lt;/strong&gt;，把所有Vue2到Vue3的API变化都搞清楚&lt;/li&gt;
&lt;li&gt;尝试把之前用Vue2写的练手项目或者公司里的非核心小模块，用Vue3的组合式API重构一遍&lt;/li&gt;
&lt;li&gt;学完基础之后，用组合式API写1-2个完整的练手项目&lt;/li&gt;
&lt;li&gt;如果想深入的话，再看Vue3的源码分析文章，或者参与公司内部组件库的双版本维护&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不管你选哪条路径，最重要的还是&lt;strong&gt;动手实践&lt;/strong&gt;——光看文档不写代码，就算把官方文档背下来，也学不会Vue，Vue3的官方文档已经做得非常好了，入门部分非常友好，高级部分也非常深入，完全可以作为你的“第一手学习资料”，不用到处找乱七八糟的第三方教程。
祝大家都能快速学会Vue3,写出高质量的代码！&lt;/p&gt;</description><pubDate>Sat, 31 Oct 2026 14:01:04 +0800</pubDate></item><item><title>Vue3生命周期到底和Vue2有啥不一样？setup里怎么用？新手避坑指南全在这里</title><link>https://codeqd.com/post/20261022185.html</link><description>&lt;p&gt;刚从Vue2转过来或者直接学Vue3的同学,有没有对着生命周期钩子的名字犯晕？一会儿是beforeCreate一会儿setup一会儿beforeMount变onBeforeMount？还有setup到底在什么阶段执行？销毁钩子为啥分unmounted和beforeUnmount？今天咱们用聊天式的问答，把这些坑一个个踩平，以后写代码再也不敢随便放钩子逻辑。&lt;/p&gt;
&lt;h2&gt;先搞懂：Vue2和Vue3的生命周期到底差在哪？&lt;/h2&gt;
&lt;p&gt;咱们先回忆下Vue2的经典8个钩子——beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed，后来加了keep-alive的activated、deactivated，还有errorCaptured，但核心的就是前面8个，那Vue3改了啥？
&lt;strong&gt;改名销毁阶段的钩子&lt;/strong&gt;：beforeDestroy变成beforeUnmount，destroyed变成unmounted，这个很好理解，就是语义化更准——Vue3说组件不再是“销毁”得渣都不剩，而是“卸载”出DOM，以后可能还要再挂载（比如用v-if切换），钩子名字就贴合这个过程了。
&lt;strong&gt;新增了setup专属钩子前缀on-&lt;/strong&gt;：Options API里的钩子还是原来的（除了销毁），但Composition API里不能直接写beforeCreate这种函数，要改成onBeforeMount这种带on开头的，而且得从vue里import进来，不然会报错找不到变量。
也是最关键的——&lt;strong&gt;beforeCreate和created被“干掉”了一半&lt;/strong&gt;：为啥是一半？因为Options API里还能写这俩，但Composition API里没有对应的onBeforeCreate、onCreated，直接用setup函数代替这俩阶段的逻辑了，这个改动是Vue3最核心的优化之一，因为Composition API要把状态和逻辑集中管理，不需要等组件实例完全创建再写状态了。&lt;/p&gt;
&lt;h2&gt;再掰碎：每个Vue3生命周期钩子具体做啥？用在哪？&lt;/h2&gt;
&lt;p&gt;咱们按组件从出生到卸载的完整流程说,不管Options还是Composition API，原理是一样的，只是写法不同。&lt;/p&gt;
&lt;h3&gt;准备出生阶段&lt;/h3&gt;
&lt;p&gt;准备阶段主要是给组件“打底”，比如注册响应式数据、绑定函数，但这时候还没有DOM，也拿不到$el、$refs这些东西。&lt;/p&gt;
&lt;h4&gt;beforeCreate（Options API专属）&lt;/h4&gt;
&lt;p&gt;啥时候执行？组件实例刚被创建，&lt;strong&gt;还没有做数据观测和事件初始化&lt;/strong&gt;——也就是说，这里访问data里的数据、computed属性、methods里的方法，全都是undefined，连this都指向的是空的或者Vue实例的原型链？其实新手基本上不用写这个钩子，因为啥也干不了，完全可以换成setup或者created。
举个踩坑的例子：新手刚转过来，在beforeCreate里写this.name = &#039;张三&#039;，结果到页面上根本显示不出来，还报错找不到name，就是因为这个阶段数据观测还没开始，你赋的值根本不会变成响应式的，也不会挂载到实例上。&lt;/p&gt;
&lt;h4&gt;created（Options API专属）&lt;/h4&gt;
&lt;p&gt;啥时候执行？数据观测、事件初始化都做完了，但&lt;strong&gt;还没有编译模板，没有挂载DOM&lt;/strong&gt;——也就是说，这里可以访问data、computed、methods、this了，但拿不到真实的DOM节点，比如你想用document.getElementById或者this.$refs.box去获取元素，全都是null。
那这个阶段能干啥？最适合做&lt;strong&gt;不需要DOM的数据请求&lt;/strong&gt;，比如登录后拉取用户的个人信息、拉取列表的第一页数据，因为这些请求只需要用到API，不需要操作页面。&lt;/p&gt;
&lt;h4&gt;setup（Composition API核心入口）&lt;/h4&gt;
&lt;p&gt;啥时候执行？敲黑板！新手最容易搞错的点——setup是在&lt;strong&gt;beforeCreate之前执行的&lt;/strong&gt;！哦不对，官方文档说的是“在组件实例创建之前执行”，准确来讲是在beforeCreate钩子执行的前一刻，这时候组件实例还没完全生成，所以setup里&lt;strong&gt;不能用this&lt;/strong&gt;！
那setup里能干啥？这是Vue3 Composition API的“主战场”——所有Composition API的东西都在这里写，比如ref、reactive定义响应式数据，computed、watch、watchEffect监听数据变化，还有那些带on开头的钩子也得在这里注册，甚至子组件传过来的props、父组件传的事件emit，都得在这里通过参数拿到（setup有两个参数：props和context，context里有attrs、slots、emit）。
举个例子，比如之前在created里写的数据请求，在setup里可以这么写：
import { ref, onMounted } from &#039;vue&#039;
import axios from &#039;axios&#039;&lt;/p&gt;
&lt;p&gt;export default {
setup() {
const userInfo = ref({})
const getUserInfo = async () =&amp;gt; {
const res = await axios.get(&#039;/api/user&#039;)
userInfo.value = res.data
}
// 注意：这里不能直接调用getUserInfo吗？其实可以，但更稳妥的是放在onMounted里？哦不对，这个要看需求——如果你的数据请求不需要DOM，直接在setup顶层调用也行，就像created里一样；但如果请求回来的数据需要操作DOM（比如给轮播图赋值后初始化），那就得放在onMounted里了。
// 先直接调用试试？当然可以，但后面我们讲mounted的时候再对比。
getUserInfo()
return { userInfo }
}
}
这里要注意两个坑：第一，setup里定义的响应式数据、函数，必须return出去，不然模板里用不了；第二，props是响应式的，但不能解构，不然会丢失响应式，要解构的话得用toRefs或者toRef。&lt;/p&gt;
&lt;h3&gt;挂载到页面阶段&lt;/h3&gt;
&lt;p&gt;挂载阶段是组件从“虚拟模板”变成“真实DOM”的过程，这时候就能拿到$el、$refs这些东西了。&lt;/p&gt;
&lt;h4&gt;beforeMount/onBeforeMount&lt;/h4&gt;
&lt;p&gt;啥时候执行？模板已经编译成了虚拟DOM，但&lt;strong&gt;还没有挂载到真实的页面上&lt;/strong&gt;——也就是说，这里还是拿不到真实的DOM节点，比如this.$refs.box还是null，但虚拟DOM已经生成了，不过新手基本上也不用操作虚拟DOM，所以这个钩子用得也不多。&lt;/p&gt;
&lt;h4&gt;mounted/onMounted&lt;/h4&gt;
&lt;p&gt;啥时候执行？敲黑板！新手最常用的钩子——&lt;strong&gt;虚拟DOM已经挂载到真实的页面上了&lt;/strong&gt;，这时候可以拿到真实的DOM节点，可以操作页面（比如给轮播图初始化、给下拉框设置默认值、调用第三方库需要DOM的API），也可以继续做数据请求（比如请求回来的数据需要结合DOM操作）。
那刚才的例子，如果请求回来的userInfo里有头像的URL，而我们要给头像加个懒加载，懒加载库需要拿到真实的img节点，那就得把getUserInfo放在onMounted里：
import { ref, onMounted } from &#039;vue&#039;
import axios from &#039;axios&#039;
import LazyLoad from &#039;xxx-lazyload&#039;&lt;/p&gt;
&lt;p&gt;export default {
setup() {
const userInfo = ref({})
const avatarRef = ref(null)
const getUserInfo = async () =&amp;gt; {
const res = await axios.get(&#039;/api/user&#039;)
userInfo.value = res.data
// 等响应式数据更新后，DOM还没立刻渲染，所以得用nextTick
await nextTick()
LazyLoad.init(avatarRef.value)
}
onMounted(() =&amp;gt; {
getUserInfo()
})
return { userInfo, avatarRef }
}
}
这里又有一个新手坑：mounted/onMounted里&lt;strong&gt;只能保证组件自身的DOM挂载完成，不能保证子组件的DOM也挂载完成&lt;/strong&gt;，如果子组件是异步加载的（比如用defineAsyncComponent），那得用子组件的mounted钩子或者父组件的onMounted里加个setTimeout/nextTick，但更稳妥的是用子组件的emit事件告诉父组件“我挂载完了”。&lt;/p&gt;
&lt;h3&gt;更新阶段&lt;/h3&gt;
&lt;p&gt;更新阶段是当组件的响应式数据发生变化时触发的,这里要注意区分：只有当数据变化导致模板重新渲染时，才会触发更新钩子，如果数据变化但模板里没用到，就不会触发。&lt;/p&gt;
&lt;h4&gt;beforeUpdate/onBeforeUpdate&lt;/h4&gt;
&lt;p&gt;啥时候执行？响应式数据已经变化，&lt;strong&gt;但虚拟DOM还没重新渲染，真实DOM也没更新&lt;/strong&gt;——也就是说，这里可以拿到更新前的DOM状态，比如用户输入了新的内容，beforeUpdate里可以拿到旧的input值，有时候用来做表单的“撤销上一步”功能。&lt;/p&gt;
&lt;h4&gt;updated/onUpdated&lt;/h4&gt;
&lt;p&gt;啥时候执行？虚拟DOM已经重新渲染，真实DOM也更新完成——也就是说，这里可以拿到更新后的DOM状态，但要注意&lt;strong&gt;不要在这里直接修改导致模板重新渲染的响应式数据&lt;/strong&gt;，不然会陷入无限循环！
举个无限循环的踩坑例子：
import { ref, onUpdated } from &#039;vue&#039;&lt;/p&gt;
&lt;p&gt;export default {
setup() {
const count = ref(0)
onUpdated(() =&amp;gt; {
count.value++ // 这里修改了count，会触发重新渲染，然后又触发onUpdated，又修改count，无限循环！
})
return { count }
}
}
那这个阶段能干啥？比如更新后调整一下滚动条的位置、更新第三方库的数据（比如ECharts的setOption），但一定要注意不要直接修改触发更新的数据，或者加个判断条件，只有满足特定条件才修改。&lt;/p&gt;
&lt;h3&gt;卸载阶段&lt;/h3&gt;
&lt;p&gt;卸载阶段是组件从真实DOM上移除的过程,这里要注意清理“副作用”，比如清除定时器、取消事件监听、销毁第三方库的实例，不然会造成内存泄漏！&lt;/p&gt;
&lt;h4&gt;beforeUnmount/onBeforeUnmount&lt;/h4&gt;
&lt;p&gt;啥时候执行？组件还没有从真实DOM上移除，&lt;strong&gt;实例还是完全可用的&lt;/strong&gt;——也就是说，这里可以拿到this、$el、$refs，是清理副作用的最佳时机！
举个清理定时器的例子：
import { ref, onMounted, onBeforeUnmount } from &#039;vue&#039;&lt;/p&gt;
&lt;p&gt;export default {
setup() {
const timer = ref(null)
const count = ref(0)
onMounted(() =&amp;gt; {
timer.value = setInterval(() =&amp;gt; {
count.value++
}, 1000)
})
onBeforeUnmount(() =&amp;gt; {
clearInterval(timer.value) // 组件卸载前清除定时器，防止内存泄漏
})
return { count }
}
}&lt;/p&gt;
&lt;h4&gt;unmounted/onUnmounted&lt;/h4&gt;
&lt;p&gt;啥时候执行？组件已经从真实DOM上移除，&lt;strong&gt;实例已经被销毁，所有的事件监听、响应式数据绑定都被解除了&lt;/strong&gt;——也就是说，这里拿不到this、$el、$refs了，基本上不用写这个钩子，所有的清理工作都应该在beforeUnmount里完成。&lt;/p&gt;
&lt;h3&gt;特殊钩子（keep-alive和错误捕获）&lt;/h3&gt;
&lt;p&gt;除了上面的核心钩子,Vue3还有几个特殊钩子，新手可能偶尔会用到。&lt;/p&gt;
&lt;h4&gt;activated/onActivated、deactivated/onDeactivated&lt;/h4&gt;
&lt;p&gt;这俩是keep-alive组件专属的钩子——当keep-alive包裹的组件被激活（比如从其他页面切回来）时，触发activated/onActivated；当被缓存（比如切到其他页面）时，触发deactivated/onDeactivated。
举个例子，比如你的首页是keep-alive包裹的，切到其他页面时不需要拉取数据，但切回来时需要刷新一下列表，就可以在activated里写：
import { ref, onActivated } from &#039;vue&#039;
import axios from &#039;axios&#039;&lt;/p&gt;
&lt;p&gt;export default {
setup() {
const list = ref([])
const getList = async () =&amp;gt; {
const res = await axios.get(&#039;/api/list&#039;)
list.value = res.data
}
onActivated(() =&amp;gt; {
getList() // 每次切回首页都刷新列表
})
return { list }
}
}
这里要注意：keep-alive包裹的组件，第一次挂载时还是会触发beforeMount、mounted，切回来时只会触发activated，不会再触发beforeMount、mounted；切走时只会触发deactivated，不会触发beforeUnmount、unmounted。&lt;/p&gt;
&lt;h4&gt;errorCaptured/onErrorCaptured&lt;/h4&gt;
&lt;p&gt;这个钩子是用来捕获子孙组件的错误的——当子孙组件抛出错误时，会先触发errorCaptured/onErrorCaptured，如果这个钩子返回false，错误就不会继续向上传播（不会触发app.config.errorHandler）。
举个例子，比如你的组件里有个异步加载的子组件，可能会报错，就可以用这个钩子来捕获：
import { ref, onErrorCaptured } from &#039;vue&#039;
import AsyncChild from &#039;./AsyncChild.vue&#039;&lt;/p&gt;
&lt;p&gt;export default {
components: { AsyncChild },
setup() {
const errorMsg = ref(&#039;&#039;)
onErrorCaptured((err, instance, info) =&amp;gt; {
errorMsg.value = &lt;code&gt;子组件出错了：${err.message}&lt;/code&gt;
return false // 阻止错误继续向上传播
})
return { errorMsg }
}
}
然后在模板里可以显示errorMsg，给用户一个友好的提示。&lt;/p&gt;
&lt;h4&gt;serverPrefetch（SSR专属）&lt;/h4&gt;
&lt;p&gt;这个钩子是服务端渲染专属的,客户端渲染不会触发，主要是用来在服务端预取数据的，新手如果不做SSR的话，可以不用了解。&lt;/p&gt;
&lt;h2&gt;新手必看：Composition API和Options API的钩子怎么选？&lt;/h2&gt;
&lt;p&gt;很多新手会问：既然Vue3同时支持Options API和Composition API，那我应该用哪种写法的钩子？
其实这个问题没有标准答案，要看你的项目情况：
如果你的项目是&lt;strong&gt;Vue2转过来的小项目&lt;/strong&gt;，或者你是&lt;strong&gt;刚学Vue3的新手&lt;/strong&gt;，可以先用Options API的钩子，毕竟和Vue2的写法差不多，容易上手；
如果你的项目是&lt;strong&gt;Vue3的新项目&lt;/strong&gt;，或者你的组件&lt;strong&gt;逻辑比较复杂，需要复用状态和逻辑&lt;/strong&gt;，那一定要用Composition API的钩子，因为Composition API可以把相关的状态和逻辑集中在一起，比如一个搜索功能的状态（搜索关键词、搜索结果、加载状态）、逻辑（搜索请求、防抖），可以放在一个useSearch函数里，然后在多个组件里复用，而Options API里这些东西是分散在data、methods、computed、watch里的，逻辑复杂了之后很难维护。&lt;/p&gt;
&lt;h2&gt;Vue3生命周期的核心要点&lt;/h2&gt;
&lt;p&gt;今天讲了这么多,最后给新手总结几个核心要点，方便大家记忆：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;销毁钩子改名&lt;/strong&gt;：beforeDestroy→beforeUnmount，destroyed→unmounted；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composition API加on-前缀&lt;/strong&gt;，并从vue里import；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;setup替代beforeCreate和created&lt;/strong&gt;，setup里不能用this；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mounted/onMounted是操作DOM、结合DOM做数据请求的最佳时机&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;beforeUnmount/onBeforeUnmount是清理副作用的最佳时机&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;updated/onUpdated里不要直接修改触发更新的数据&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;activated/onActivated、deactivated/onDeactivated是keep-alive专属的&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Options API适合小项目、新手，Composition API适合大项目、逻辑复杂、需要复用的组件&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;好了,今天的Vue3生命周期就讲到这里，大家有没有收获？如果还有不懂的地方，可以在评论区留言，我会一一解答。&lt;/p&gt;</description><pubDate>Sat, 31 Oct 2026 08:01:23 +0800</pubDate></item><item><title>Vue3里的watch到底怎么用？和computed、watchEffect有啥不一样？避坑点全整理</title><link>https://codeqd.com/post/20261022184.html</link><description>&lt;h2&gt;Vue3 watch的核心定位：精准追踪数据变化并触发自定义逻辑&lt;/h2&gt;
&lt;p&gt;先把最基础的搞明白——Vue3的watch不是凭空来的，它是Vue2 watch的升级优化版，但内核逻辑没变：&lt;strong&gt;专门盯着一个或一组“响应式数据的某个变化维度”，当这个维度符合预期时，执行一段你提前写好的回调&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里的“维度”是Vue3新增的关键概念，Vue2只能默认看“值变化”，甚至深层嵌套对象只能看引用是否改变，除非加deep:true，Vue3不一样，能灵活调整追踪的颗粒度：比如只看对象的某个属性、只追踪数组的push/pop/shift/unshift/splice/sort/reverse这些原生变异方法（非整组替换）、或者不管深层浅层只看数据本身的变化触发次数？先别急,颗粒度的问题后面单独用选项来说。&lt;/p&gt;
&lt;p&gt;先举个最简单的例子：做个实时计数保存到localStorage的功能，用setup语法糖写，首先定义一个响应式的count,然后写watch：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const count = ref(0)
    // 第一个参数是“被监听的源”，第二个是回调（新值、旧值）
    watch(count, (newVal, oldVal) =&amp;gt; {
      localStorage.setItem(&amp;#39;saved-count&amp;#39;, newVal)
    })
    return { count }
  }
}&lt;/pre&gt;
&lt;p&gt;这就是watch最基础的用法——监听ref定义的基本数据类型，值变了就存，如果count变成对象呢？比如ref包裹一个{ value: 0 }的对象,或者用reactive？&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, reactive, watch } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const refObj = ref({ value: 0 })
    const reactiveObj = reactive({ value: 0 })
    // 监听ref包裹的对象，默认只看引用是否变（比如refObj.value = { value: 1 }）
    // 要监听内部属性，有两种方式
    // 方式1：直接写要监听的属性的getter函数
    watch(() =&amp;gt; refObj.value.value, (newVal, oldVal) =&amp;gt; {
      console.log(&amp;#39;refObj的value变了&amp;#39;, newVal, oldVal)
    })
    // 方式2：监听整个refObj，加deep:true
    watch(refObj, (newVal, oldVal) =&amp;gt; {
      console.log(&amp;#39;refObj整个对象有变动（不管引用还是属性）&amp;#39;, newVal, oldVal)
    }, { deep: true })
    // 监听reactive对象，默认就是deep:true，不用加
    watch(reactiveObj, (newVal, oldVal) =&amp;gt; {
      console.log(&amp;#39;reactiveObj整个对象有变动&amp;#39;, newVal, oldVal)
    })
    // 监听reactive的单个属性，也要用getter函数，不然旧值拿不到（因为reactive是代理，新旧对象引用始终是同一个）
    watch(() =&amp;gt; reactiveObj.value, (newVal, oldVal) =&amp;gt; {
      console.log(&amp;#39;reactiveObj的value变了&amp;#39;, newVal, oldVal)
    })
    return { refObj, reactiveObj }
  }
}&lt;/pre&gt;
&lt;p&gt;这里其实已经带出了第一个避坑点和第二个知识点：知识点是&lt;strong&gt;watch的第一个参数“被监听的源”支持两种类型&lt;/strong&gt;：单个响应式数据（ref/reactive/computed/甚至其他watch的返回值？不过很少用），或者返回任意值的getter函数；避坑点是&lt;strong&gt;监听reactive定义的对象/数组时，默认deep:true，但回调里的旧值永远和新值引用一致，拿不到历史状态&lt;/strong&gt;——如果一定要拿到reactive的旧值，只能在回调前手动存一份快照，或者用getter函数监听它的序列化字符串？不过后者开销大,适合小数据量。&lt;/p&gt;
&lt;p&gt;继续说颗粒度的另一个调整：&lt;strong&gt;immediate选项&lt;/strong&gt;，这个Vue2也有，但新手容易忘——默认watch是“懒加载”的，只有被监听的源第一次变化之后才会执行回调，但有时候需要组件刚挂载（或者watch刚被创建）就先跑一次，比如刚才的localStorage例子，组件刷新后得先从localStorage读一次count对吧？用immediate就行：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const count = ref(0)
    watch(count, (newVal, oldVal) =&amp;gt; {
      // immediate为true时，第一次执行oldVal是undefined，注意判断
      if (oldVal !== undefined) {
        localStorage.setItem(&amp;#39;saved-count&amp;#39;, newVal)
      } else {
        // 组件刚挂载，先读
        const saved = localStorage.getItem(&amp;#39;saved-count&amp;#39;)
        if (saved) count.value = parseInt(saved)
      }
    }, { immediate: true })
    return { count }
  }
}&lt;/pre&gt;
&lt;p&gt;对，这里又带出一个小细节：immediate第一次触发，oldVal是undefined，别直接拿来用,不然容易出NaN或者其他逻辑错误。&lt;/p&gt;
&lt;p&gt;再进阶一点：&lt;strong&gt;监听多个源&lt;/strong&gt;，这个Vue2没有，Vue3新增的，非常实用——比如你要监听count和username两个值，只要其中一个变了，就保存整个用户数据到localStorage,那可以把两个源放在数组里传给watch：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const count = ref(0)
    const username = ref(&amp;#39;&amp;#39;)
    watch(
      [count, username],
      // 新值和旧值也是数组，顺序和源数组一致
      ([newCount, newName], [oldCount, oldName]) =&amp;gt; {
        localStorage.setItem(&amp;#39;user-data&amp;#39;, JSON.stringify({ count: newCount, name: newName }))
      },
      { immediate: true }
    )
    return { count, username }
  }
}&lt;/pre&gt;
&lt;p&gt;数组里可以混着放ref、reactive的getter、computed这些,没问题。&lt;/p&gt;
&lt;p&gt;还有一个Vue3.2+才加的&lt;strong&gt;flush选项&lt;/strong&gt;，这个稍微深一点，但遇到DOM操作的时候必须用，先讲原理：Vue有个“响应式更新队列”，当你修改多个数据时，不会立即触发DOM更新，而是等同步代码执行完，在下一个微任务里批量更新，这样能避免重复渲染，提升性能，默认watch的flush是&#039;pre&#039;，意思是“在DOM更新之前执行回调”——这时候你在回调里拿DOM元素（比如document.querySelector），拿到的还是更新前的DOM，比如做个功能：count变了之后，DOM里的数字颜色变成红色，过1秒再变回黑色，用flush:&#039;pre&#039;的话：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch, nextTick } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const count = ref(0)
    watch(count, (newVal) =&amp;gt; {
      // 这里拿到的span还是旧的，或者可能还没渲染？
      const span = document.querySelector(&amp;#39;#count-span&amp;#39;)
      if (span) span.style.color = &amp;#39;red&amp;#39;
      nextTick(() =&amp;gt; {
        // nextTick是等DOM更新完，微任务里的回调
        const span = document.querySelector(&amp;#39;#count-span&amp;#39;)
        if (span) {
          setTimeout(() =&amp;gt; span.style.color = &amp;#39;black&amp;#39;, 1000)
        }
      })
    }, { immediate: true })
    return { count }
  }
}&lt;/pre&gt;
&lt;p&gt;虽然可以用nextTick解决，但flush选项有个&#039;post&#039;，可以直接让回调在DOM更新之后执行,更简洁：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const count = ref(0)
    watch(count, (newVal) =&amp;gt; {
      const span = document.querySelector(&amp;#39;#count-span&amp;#39;)
      if (span) {
        span.style.color = &amp;#39;red&amp;#39;
        setTimeout(() =&amp;gt; span.style.color = &amp;#39;black&amp;#39;, 1000)
      }
    }, { immediate: true, flush: &amp;#39;post&amp;#39; })
    return { count }
  }
}&lt;/pre&gt;
&lt;p&gt;flush还有个&#039;sync&#039;选项，慎用！意思是“数据一变立即执行回调，不管队列不队列”，这样如果修改多个关联数据，会触发多次watch，性能很差，除非非常特殊的场景（比如要在事件冒泡的某个阶段同步处理数据）,不然别用。&lt;/p&gt;
&lt;h2&gt;和computed、watchEffect的区别：三个工具各管一摊，别乱用&lt;/h2&gt;
&lt;p&gt;新手最容易犯的错误就是把这三个混着用，一会儿用watch做计算属性，一会儿用computed做监听逻辑，结果代码又乱又难维护,其实它们的分工非常明确：&lt;/p&gt;
&lt;h3&gt;watch vs computed&lt;/h3&gt;
&lt;p&gt;核心区别是&lt;strong&gt;有没有返回值，以及是否是“纯函数”&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;computed：是“计算属性”，必须有返回值，而且这个返回值也是响应式的；computed的回调必须是&lt;strong&gt;纯函数&lt;/strong&gt;——也就是说，同样的输入永远产生同样的输出，而且不能修改外部的响应式数据（不是不能改，改了会出警告，而且逻辑会绕），它的定位是“基于现有响应式数据，派生出来一个新的响应式数据”，比如把firstName和lastName拼成fullName：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, computed } from &amp;#39;vue&amp;#39;
export default {
setup() {
  const firstName = ref(&amp;#39;张&amp;#39;)
  const lastName = ref(&amp;#39;三&amp;#39;)
  const fullName = computed(() =&amp;gt; {
    return firstName.value + lastName.value
  })
  return { fullName }
}
}&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;watch：是“监听器”，可以没有返回值（大部分时候都没有），回调里可以修改外部数据、调用接口、操作DOM、存localStorage……想干啥干啥，它的定位是“当数据变化时，执行一段副作用逻辑”——刚才的localStorage、变色、调用接口都是副作用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个反例：用watch做fullName，虽然能做,但非常傻：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const firstName = ref(&amp;#39;张&amp;#39;)
    const lastName = ref(&amp;#39;三&amp;#39;)
    const fullName = ref(&amp;#39;张三&amp;#39;) // 还要手动初始化
    watch([firstName, lastName], ([newFirst, newLast]) =&amp;gt; {
      fullName.value = newFirst + newLast // 手动更新
    }, { immediate: true })
    return { fullName }
  }
}&lt;/pre&gt;
&lt;p&gt;对比一下computed的版本，多了两个步骤：手动初始化fullName，手动在watch里更新fullName，而且computed还有&lt;strong&gt;缓存&lt;/strong&gt;——只要firstName和lastName不变，不管你在模板里用多少次fullName，都只会重新计算一次；但用watch的话，虽然fullName是ref，也不会有这个缓存吗？不，ref本身不会，除非你再加逻辑，但完全没必要,computed已经帮你做好了。&lt;/p&gt;
&lt;h3&gt;watch vs watchEffect&lt;/h3&gt;
&lt;p&gt;这两个都是处理副作用的，核心区别是&lt;strong&gt;是否需要手动指定被监听的源，以及是否能拿到旧值&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;watch：需要你&lt;strong&gt;显式告诉它要监听谁&lt;/strong&gt;，而且能拿到新值和旧值；默认是“懒加载”的（除非加immediate），它的定位是“精准监听某几个数据的变化，只在这几个数据变的时候执行，而且需要知道变化前后的值做对比”，比如刚才的变色例子，需要知道旧值吗？不需要，但如果要做“当count从10变成20的时候弹个窗”，就必须用watch：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch } from &amp;#39;vue&amp;#39;
export default {
setup() {
  const count = ref(0)
  watch(count, (newVal, oldVal) =&amp;gt; {
    if (oldVal === 10 &amp;amp;&amp;amp; newVal === 20) {
      alert(&amp;#39;恭喜你，从10到20了！&amp;#39;)
    }
  })
  return { count }
}
}&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;watchEffect：&lt;strong&gt;不需要显式指定源&lt;/strong&gt;，它会自动追踪回调里用到的所有响应式数据，只要其中一个变了，就会执行；它&lt;strong&gt;没有懒加载&lt;/strong&gt;——创建后立即执行第一次；它&lt;strong&gt;拿不到旧值&lt;/strong&gt;，它的定位是“快速处理一些依赖多个响应式数据的简单副作用，不需要对比新旧值”，比如刚才的localStorage例子，如果是存多个数据的组合，用watchEffect比用watch的数组写法更简单：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watchEffect } from &amp;#39;vue&amp;#39;
export default {
setup() {
  const count = ref(0)
  const username = ref(&amp;#39;&amp;#39;)
  // 自动追踪count和username，创建后立即执行
  watchEffect(() =&amp;gt; {
    localStorage.setItem(&amp;#39;user-data&amp;#39;, JSON.stringify({ count: count.value, name: username.value }))
  })
  // 组件刚挂载时会自动读吗？不会，得自己在watchEffect里写
  // 哦不对，watchEffect第一次执行就会触发，那可以把读的逻辑也放进去？
  // 等一下，放进去的话，count.value和username.value一变，又会读，覆盖掉新的？不行
  // 所以读的逻辑还是单独放setup里
  const saved = localStorage.getItem(&amp;#39;user-data&amp;#39;)
  if (saved) {
    const data = JSON.parse(saved)
    count.value = data.count || 0
    username.value = data.name || &amp;#39;&amp;#39;
  }
  return { count, username }
}
}&lt;/pre&gt;
&lt;p&gt;哦对，这里要注意watchEffect的顺序——刚才的代码里，先定义count和username，然后写watchEffect存，然后再读并赋值——没问题，因为赋值会触发count和username的变化，watchEffect会自动再存一次，覆盖掉第一次存的初始值（0和空字符串），但其实反过来也可以：先读并赋值，再写watchEffect，这样第一次存的就是正确的，不会有多余的一次存储,更优一点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再举个反例：用watchEffect做刚才的弹10→20的窗，做不到！因为拿不到旧值。&lt;/p&gt;
&lt;h2&gt;Vue3 watch的避坑点全整理：新手90%的问题都在这里&lt;/h2&gt;
&lt;p&gt;刚才已经零零散散提了几个，现在系统整理一下,都是踩过坑的经验：&lt;/p&gt;
&lt;h3&gt;监听reactive定义的对象/数组时，旧值拿不到&lt;/h3&gt;
&lt;p&gt;刚才说过了,解决方案有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果只需要监听单个属性，用getter函数返回这个属性：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;watch(() =&amp;gt; reactiveObj.value, (newVal, oldVal) =&amp;gt; {
// 这里oldVal是正确的历史值
})&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;如果需要监听整个对象/数组的所有属性变化，而且必须拿到旧值，手动存一份快照：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, reactive, watch } from &amp;#39;vue&amp;#39;
export default {
setup() {
  const reactiveObj = reactive({ value: 0 })
  // 初始化快照
  let oldObjSnapshot = JSON.parse(JSON.stringify(reactiveObj))
  watch(reactiveObj, (newVal) =&amp;gt; {
    // 用快照对比
    if (oldObjSnapshot.value !== newVal.value) {
      console.log(&amp;#39;value变了&amp;#39;, newVal.value, oldObjSnapshot.value)
    }
    // 更新快照
    oldObjSnapshot = JSON.parse(JSON.stringify(newVal))
  }, { deep: true }) // 虽然reactive默认deep，但写出来更清晰
  return { reactiveObj }
}
}&lt;/pre&gt;
&lt;p&gt;注意：JSON.parse(JSON.stringify())有局限性，不能序列化函数、正则、Date（会变成字符串）、Symbol、循环引用的对象……如果你的数据有这些，得用lodash的cloneDeep,或者自己写深拷贝函数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;监听ref包裹的对象时，不加deep:true，只有引用变了才触发&lt;/h3&gt;
&lt;p&gt;刚才的例子里也有,解决方案有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加deep:true&lt;/li&gt;
&lt;li&gt;用getter函数返回要监听的属性或整个对象的某个部分&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;immediate第一次触发时，oldVal是undefined&lt;/h3&gt;
&lt;p&gt;这个一定要注意判断，不然容易出问题，比如parseInt(undefined)是NaN,或者拿undefined的属性报错。&lt;/p&gt;
&lt;h3&gt;监听数组时，用索引赋值或者修改length，Vue3不会触发（除非加deep:true）&lt;/h3&gt;
&lt;p&gt;哦，这个很重要！Vue3的响应式系统是基于Proxy的，和Vue2的Object.defineProperty不一样，但对数组的处理有个共同点：&lt;strong&gt;用索引直接赋值arr[0] = 1或者修改arr.length = 0，不会自动触发响应式更新&lt;/strong&gt;——不过Vue3可以用deep:true解决，或者用数组的原生变异方法（push/pop/shift/unshift/splice/sort/reverse），或者用Vue3提供的set方法？不对，Vue3没有全局的Vue.set了，因为Proxy可以直接处理，但索引赋值和length修改确实不会触发，除非加deep:true监听整个数组。&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const arr = ref([1,2,3])
    // 默认不加deep:true
    watch(arr, (newVal, oldVal) =&amp;gt; {
      console.log(&amp;#39;arr变了&amp;#39;, newVal, oldVal)
    })
    // 测试1：push，会触发
    arr.value.push(4)
    // 测试2：索引赋值，不会触发
    arr.value[0] = 0
    // 测试3：修改length，不会触发
    arr.value.length = 2
    // 测试4：整组替换，会触发
    arr.value = [5,6,7]
    return { arr }
  }
}&lt;/pre&gt;
&lt;p&gt;如果要让索引赋值和修改length也触发，加deep:true就行：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;watch(arr, (newVal, oldVal) =&amp;gt; {
  console.log(&amp;#39;arr变了&amp;#39;, newVal, oldVal)
}, { deep: true })&lt;/pre&gt;
&lt;h3&gt;监听计算属性时，只有计算属性的依赖变了才会触发，不是每次访问&lt;/h3&gt;
&lt;p&gt;这个其实是computed的缓存机制带来的，但新手容易误解——比如你有个computed依赖count，你在模板里点击100次fullName的按钮，但count没变，那fullName不会重新计算，监听fullName的watch也不会触发，这是正常的,别以为是bug。&lt;/p&gt;
&lt;h3&gt;不要在watch的回调里修改被监听的源本身，会造成无限循环&lt;/h3&gt;

&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watch } from &amp;#39;vue&amp;#39;
export default {
  setup() {
    const count = ref(0)
    watch(count, (newVal) =&amp;gt; {
      count.value = newVal + 1 // 这里count变了，又触发watch，又加1，无限循环
    })
    return { count }
  }
}&lt;/pre&gt;
&lt;p&gt;除非你有特殊的逻辑控制,比如加个判断：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;watch(count, (newVal) =&amp;gt; {
  if (newVal &amp;lt; 10) {
    count.value = newVal + 1
  }
})&lt;/pre&gt;
&lt;p&gt;这样只会循环到10，没问题，但一般还是尽量避免在回调里修改被监听的源,逻辑会绕。&lt;/p&gt;
&lt;h2&gt;什么时候用watch？什么时候不用？&lt;/h2&gt;
&lt;p&gt;最后再总结一下,帮你快速决策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用watch的场景&lt;/strong&gt;：&lt;ol&gt;
&lt;li&gt;需要知道数据变化前后的旧值做对比&lt;/li&gt;
&lt;li&gt;需要精准监听某几个数据的变化，不想因为其他无关数据的变化触发逻辑&lt;/li&gt;
&lt;li&gt;需要执行的副作用逻辑比较复杂，或者依赖的源变化频率低，不需要自动追踪&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不用watch的场景&lt;/strong&gt;：&lt;ol&gt;
&lt;li&gt;只是想基于现有数据派生新的响应式数据——用computed&lt;/li&gt;
&lt;li&gt;依赖的源多且杂，不需要对比新旧值，副作用逻辑简单——用watchEffect&lt;/li&gt;
&lt;li&gt;只是想操作DOM——尽量用生命周期钩子（比如onMounted、onUpdated）或者flush: &#039;post&#039;的watch/watchEffect，但优先用模板绑定class/style或者v-if/v-show，除非必须直接操作DOM&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vue3的watch其实非常灵活，只要搞懂了它的核心定位、参数选项、和其他两个工具的区别，以及避坑点，用起来就会非常顺手,能帮你解决很多响应式数据变化带来的问题。&lt;/p&gt;</description><pubDate>Fri, 30 Oct 2026 20:01:53 +0800</pubDate></item><item><title>Vue3 watchEffect到底怎么用？新手常踩的坑一次性说清楚</title><link>https://codeqd.com/post/20261022183.html</link><description>&lt;p&gt;很多刚从Vue2转过来的前端朋友，或者刚学Vue3的同学，看到Composition API里的watch和watchEffect两个监听函数，第一反应都是懵的：这俩看起来差不多啊？怎么选？而且自己写watchEffect的时候，总容易出各种问题，比如明明改了数据页面没反应、控制台无限报错循环更新、明明改了深层对象的属性它却没触发、想手动停止监听不知道咋弄… 其实这些问题都是因为没搞懂watchEffect的核心逻辑和正确使用场景，今天咱们就把这些疑问掰碎了讲，从基础用法到避坑指南，再到权威的底层原理解释,一次性帮你吃透它。&lt;/p&gt;
&lt;h2&gt;先搞懂：watchEffect和watch的本质区别&lt;/h2&gt;
&lt;p&gt;很多人一开始会纠结这俩谁更好用，但其实它们是&lt;strong&gt;互补的工具&lt;/strong&gt;，没有绝对的优劣，关键看你要解决什么问题，我们可以从三个核心维度对比,一下子就能分清楚：&lt;/p&gt;
&lt;h3&gt;依赖追踪的方式：主动vs被动&lt;/h3&gt;
&lt;p&gt;watch是“&lt;strong&gt;被动的、显式的&lt;/strong&gt;”，你必须手动指定要监听的&lt;strong&gt;单个或多个数据源&lt;/strong&gt;，只有这些数据变了，回调才会跑，其他的变化它一概不管，比如你要监听用户输入的搜索关键词和筛选价格，就得把这两个ref/reactive显式写在第一个参数里。&lt;/p&gt;
&lt;p&gt;而watchEffect是“&lt;strong&gt;主动的、隐式的&lt;/strong&gt;”，它会&lt;strong&gt;自动收集回调函数内部同步访问到的所有响应式数据&lt;/strong&gt;作为依赖，不管你是访问ref.value还是reactive的属性、甚至是reactive深层对象的属性，只要在同步执行的回调里碰过，下次这些数据变了，它就会自动触发，这种自动收集依赖的特性，在多依赖联动的场景下特别爽,不用一个个去列依赖项了。&lt;/p&gt;
&lt;p&gt;举个简单的对比例子：
假设你有三个响应式数据：商品id（ref）、商品详情（reactive）、加载状态（ref）。
如果用watch，要监听商品id，变了就去调接口拿详情，同时改加载状态,代码是这样的：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, reactive, watch } from &amp;#39;vue&amp;#39;
const id = ref(1)
const detail = reactive({})
const loading = ref(false)
watch(id, async (newId) =&amp;gt; {
  loading.value = true
  const res = await fetch(`/api/goods/${newId}`)
  Object.assign(detail, await res.json())
  loading.value = false
})&lt;/pre&gt;
&lt;p&gt;这里watch的第一个参数只写了id,所以只有id变才会触发接口。&lt;/p&gt;
&lt;p&gt;如果换成watchEffect呢？你不用写监听项，直接把逻辑塞进去就行，它会自动发现id、loading、detail都是依赖吗？不对不对，这里要注意&lt;strong&gt;异步函数的限制&lt;/strong&gt;——watchEffect只会收集&lt;strong&gt;第一次同步执行回调时访问到的数据&lt;/strong&gt;，异步操作里的代码不会被追踪，那上面的例子用watchEffect的话，正确的同步写法是把id的访问放在同步里,而loading可以改同步触发：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, reactive, watchEffect } from &amp;#39;vue&amp;#39;
const id = ref(1)
const detail = reactive({})
const loading = ref(false)
watchEffect(async (onCleanup) =&amp;gt; {
  // 同步访问id，只有id会被追踪为依赖
  const currentId = id.value
  loading.value = true
  // 这里加个清理函数，防止接口还没回来就改了id，展示旧数据
  const controller = new AbortController()
  onCleanup(() =&amp;gt; controller.abort())
  try {
    const res = await fetch(`/api/goods/${currentId}`, { signal: controller.signal })
    Object.assign(detail, await res.json())
  } catch (e) {
    if (e.name !== &amp;#39;AbortError&amp;#39;) console.error(e)
  } finally {
    loading.value = false
  }
})&lt;/pre&gt;
&lt;p&gt;你看，这里虽然detail也被用了，但它是在异步的try块里赋值的，第一次同步执行时只是声明+空赋值，没有“访问获取依赖值”的操作（如果一开始detail有初始值且同步访问过，比如console.log(detail.name)，那name对应的路径会被追踪，但Object.assign不会增加新的依赖项），所以只有id和第一次同步赋值时可能涉及的loading的初始化值？不，loading.value = true是同步的“设置”，不是“读取依赖值判断变化”，所以核心依赖只有currentId也就是id.value，这就和watch的效果类似了，但如果有多个联动的同步依赖，比如搜索关键词和筛选条件一起影响搜索结果的缓存key,那watchEffect就更方便。&lt;/p&gt;
&lt;h3&gt;初始执行时机：默认自动vs默认不自动&lt;/h3&gt;
&lt;p&gt;这也是新手容易忽略的点：watchEffect&lt;strong&gt;只要组件挂载后（或者你手动在其他时机调用创建）就会立即执行第一次回调&lt;/strong&gt;，用来初始化依赖和第一次业务逻辑；而watch&lt;strong&gt;默认只有监听项变化时才会执行&lt;/strong&gt;，第一次初始化不会跑，除非你手动加&lt;code&gt;immediate: true&lt;/code&gt;选项。&lt;/p&gt;
&lt;p&gt;刚才的接口例子里，watch加不加immediate的区别，watchEffect直接帮你做了选择——它默认就是带“immediate”的,所以不用额外写配置就能拿到第一次的商品详情。&lt;/p&gt;
&lt;h3&gt;能不能拿到旧值：只有watch能拿&lt;/h3&gt;
&lt;p&gt;watch的回调函数有两个参数：newVal和oldVal，分别是监听项变化后的新值和变化前的旧值；而watchEffect的回调只有一个参数——&lt;code&gt;onCleanup&lt;/code&gt;清理函数（后面会讲这个很重要的东西），&lt;strong&gt;完全拿不到旧值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以如果你需要对比数据前后的变化，比如搜索关键词超过长度限制时提示用户“刚才输入的旧词已经搜过了”，或者记录数据变化的历史，那必须用watch,watchEffect做不到。&lt;/p&gt;
&lt;h2&gt;watchEffect的核心API：不仅仅是传个回调这么简单&lt;/h2&gt;
&lt;p&gt;很多人只知道watchEffect可以传一个回调，但其实它还有两个可选的参数，能帮你解决不少高级问题,它的完整签名是这样的：&lt;/p&gt;
&lt;pre class=&quot;brush:typescript;toolbar:false&quot;&gt;function watchEffect(
  effect: (onCleanup: CleanupRegistrator) =&amp;gt; void,
  options?: WatchEffectOptions
): StopHandle&lt;/pre&gt;
&lt;h3&gt;返回值StopHandle：手动停止监听的开关&lt;/h3&gt;
&lt;p&gt;watchEffect创建的监听，&lt;strong&gt;默认会在组件卸载时自动停止&lt;/strong&gt;，不用你操心，但如果是在组件外（比如全局工具函数里）创建的，或者想在组件还没卸载的时候就手动停止（比如用户退出登录后不再监听用户的实时消息）,那就要用到返回的StopHandle了。&lt;/p&gt;
&lt;p&gt;举个手动停止的例子：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watchEffect } from &amp;#39;vue&amp;#39;
const count = ref(0)
// 创建监听，拿到停止函数
const stopCountWatch = watchEffect(() =&amp;gt; {
  console.log(&amp;#39;count变了：&amp;#39;, count.value)
})
count.value++ // 控制台打印count变了：1
count.value++ // 打印2
stopCountWatch() // 手动停止
count.value++ // 这里不会再打印了&lt;/pre&gt;
&lt;h3&gt;第二个参数WatchEffectOptions：解决依赖追踪、触发时机等问题&lt;/h3&gt;
&lt;p&gt;这个options对象里有几个常用的配置项,新手最容易因为没加它们踩坑：&lt;/p&gt;
&lt;h4&gt;（1）flush：控制回调的触发时机&lt;/h4&gt;
&lt;p&gt;这个配置和watch里的flush是一样的,有三个可选值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&#039;pre&#039;（默认值）&lt;/strong&gt;：在Vue的DOM更新&lt;strong&gt;之前&lt;/strong&gt;触发回调，如果你在回调里修改了DOM或者依赖DOM的样式/尺寸，这时候可能会拿到旧的DOM状态,导致计算错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&#039;post&#039;&lt;/strong&gt;：在Vue的DOM更新&lt;strong&gt;之后&lt;/strong&gt;触发回调，如果你需要操作DOM（比如获取更新后的元素宽度、滚动位置），一定要用这个值,否则会出问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&#039;sync&#039;&lt;/strong&gt;：同步触发回调，也就是监听的数据一变化，回调立刻就跑，不等Vue的任何队列，这个值要谨慎用，因为如果多个依赖同时变化，sync的回调会跑很多次，可能会影响性能,而且容易导致无限循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如有个场景：你要监听窗口的宽度（用ref存储，通过resize事件更新），然后根据窗口宽度计算某个div的宽度，并且需要获取div更新后的真实渲染宽度做其他处理，这时候如果用默认的flush: &#039;pre&#039;，计算div宽度的时候div还没更新，拿到的就是旧值，必须用flush: &#039;post&#039;：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, onMounted, onUnmounted, watchEffect } from &amp;#39;vue&amp;#39;
const windowWidth = ref(window.innerWidth)
const divWidth = ref(0)
// 监听窗口大小变化，更新windowWidth
const handleResize = () =&amp;gt; { windowWidth.value = window.innerWidth }
onMounted(() =&amp;gt; window.addEventListener(&amp;#39;resize&amp;#39;, handleResize))
onUnmounted(() =&amp;gt; window.removeEventListener(&amp;#39;resize&amp;#39;, handleResize))
// 这里必须加flush: &amp;#39;post&amp;#39;，才能拿到更新后的div真实宽度
watchEffect((onCleanup) =&amp;gt; {
  const container = document.getElementById(&amp;#39;container&amp;#39;)
  if (!container) return
  // 假设你用v-bind:style把container的宽度设成了windowWidth的一半
  divWidth.value = container.offsetWidth
  console.log(&amp;#39;更新后的容器宽度：&amp;#39;, divWidth.value)
}, { flush: &amp;#39;post&amp;#39; })&lt;/pre&gt;
&lt;h4&gt;（2）onTrack和onTrigger：调试依赖追踪的神器&lt;/h4&gt;
&lt;p&gt;这两个配置项只有开发环境生效，生产环境会被自动tree-shaking掉，不用担心性能，它们是用来排查“为什么watchEffect没触发”或者“为什么触发了好几次”这种问题的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;onTrack&lt;/strong&gt;：当watchEffect第一次执行时，每收集到一个响应式依赖，就会触发一次onTrack回调，回调参数里有依赖的类型（ref/reactive/computed）、具体的target和key,能帮你看看到底有没有收集到你想要的依赖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;onTrigger&lt;/strong&gt;：当某个依赖变化导致watchEffect触发时，会触发一次onTrigger回调，回调参数里有变化的类型（set/add/delete/clear）、具体的target和key、新旧值（只有ref和平铺的reactive第一层能拿到新旧值，深层的reactive拿不到旧值）,能帮你看到底是哪个依赖变了导致触发的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如之前举的例子里，如果担心watchEffect没收集到id作为依赖,可以加onTrack看看：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;watchEffect(async (onCleanup) =&amp;gt; {
  const currentId = id.value
  // 其他逻辑
}, {
  onTrack(e) {
    console.log(&amp;#39;收集到依赖了：&amp;#39;, e)
    // 这里如果e.target是id对应的RefImpl，e.key是&amp;#39;value&amp;#39;，就说明收集成功了
  },
  onTrigger(e) {
    console.log(&amp;#39;依赖变化了：&amp;#39;, e)
  }
})&lt;/pre&gt;
&lt;h2&gt;新手最常踩的6个watchEffect大坑，你中了几个？&lt;/h2&gt;
&lt;p&gt;讲完了基础用法，接下来就是避坑环节了，这些坑我自己刚学的时候踩过，身边的同事也踩过，几乎是Vue3 watchEffect的“新手必考题”,咱们一个个来看：&lt;/p&gt;
&lt;h3&gt;坑1：异步操作里的响应式数据没被追踪&lt;/h3&gt;
&lt;p&gt;这个刚才在对比watch和watchEffect的时候提到过，但新手还是很容易犯。&lt;strong&gt;watchEffect只会收集第一次同步执行回调时访问到的响应式数据&lt;/strong&gt;，Promise.then/catch/finally、async/await里的await之后的代码，都是异步的,不会被追踪。&lt;/p&gt;
&lt;p&gt;举个踩坑的错误例子：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watchEffect } from &amp;#39;vue&amp;#39;
const keyword = ref(&amp;#39;&amp;#39;)
const result = ref([])
// ❌ 错误写法：keyword的访问在await之后，第一次同步执行时没有碰过keyword.value，所以不会追踪
watchEffect(async () =&amp;gt; {
  loading.value = true
  const res = await fetch(&amp;#39;/api/search&amp;#39;) // 这里的keyword没传？哦对，还要传，但传了也是异步访问不到
  const data = await res.json()
  const currentKeyword = keyword.value // await之后的同步，不会被收集
  result.value = data.filter(item =&amp;gt; item.name.includes(currentKeyword))
  loading.value = false
})&lt;/pre&gt;
&lt;p&gt;这里不仅传参数漏了，就算把keyword.value放在fetch的URL里，也是在同步代码里吗？不，fetch是同步调用的，所以如果URL是&lt;code&gt;/api/search?q=${keyword.value}&lt;/code&gt;，那keyword.value是在同步调用fetch的时候访问的，会被收集，但刚才的错误例子里，currentKeyword是在await res.json()之后才访问的,所以不会被收集。&lt;/p&gt;
&lt;p&gt;正确的写法是：把所有需要追踪的响应式依赖，&lt;strong&gt;都放在await之前的同步代码里访问并保存下来&lt;/strong&gt;：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watchEffect } from &amp;#39;vue&amp;#39;
const keyword = ref(&amp;#39;&amp;#39;)
const priceRange = ref([0, 100])
const result = ref([])
// ✅ 正确写法：所有依赖都在await之前的同步代码里保存
watchEffect(async (onCleanup) =&amp;gt; {
  const currentKeyword = keyword.value
  const [minPrice, maxPrice] = priceRange.value
  const controller = new AbortController()
  onCleanup(() =&amp;gt; controller.abort())
  loading.value = true
  try {
    const res = await fetch(`/api/search?q=${currentKeyword}&amp;amp;min=${minPrice}&amp;amp;max=${maxPrice}`, { signal: controller.signal })
    result.value = await res.json()
  } catch (e) {
    if (e.name !== &amp;#39;AbortError&amp;#39;) console.error(e)
  } finally {
    loading.value = false
  }
})&lt;/pre&gt;
&lt;h3&gt;坑2：没有加onCleanup导致的竞态条件&lt;/h3&gt;
&lt;p&gt;什么是竞态条件？比如刚才的搜索例子，用户快速输入“vue”“vue3”“vue3 composition api”，第一次输入“vue”时触发了接口请求，但接口还没回来，用户又输入了“vue3”，触发了第二次请求，然后第二次请求比第一次先回来，展示了“vue3”的结果，接着第一次请求回来，又把结果覆盖成了“vue”的，这就展示了错误的数据,用户体验很差。&lt;/p&gt;
&lt;p&gt;解决这个问题的核心就是&lt;strong&gt;onCleanup清理函数&lt;/strong&gt;：watchEffect每次重新触发回调之前，都会先调用上一次回调里通过onCleanup注册的清理函数，然后再执行新的回调，所以我们可以在每次请求时创建一个AbortController，注册到onCleanup里,下次触发时就取消上一次的请求。&lt;/p&gt;
&lt;p&gt;刚才的正确例子里已经加了这个逻辑，这里再单独拎出来讲一下onCleanup的用法：onCleanup接受一个函数作为参数，这个函数可以是取消请求、清除定时器、取消事件监听等等任何需要清理的操作。&lt;/p&gt;
&lt;h3&gt;坑3：修改了依赖导致无限循环&lt;/h3&gt;
&lt;p&gt;无限循环是Vue3监听函数里最常见的问题之一，尤其是watchEffect，因为它自动收集依赖，稍不注意就会在回调里修改自己的依赖,导致循环触发。&lt;/p&gt;
&lt;p&gt;举个简单的踩坑例子：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watchEffect } from &amp;#39;vue&amp;#39;
const count = ref(0)
// ❌ 错误写法：在回调里修改count.value，而count.value是依赖，导致无限循环
watchEffect(() =&amp;gt; {
  console.log(&amp;#39;count变了：&amp;#39;, count.value)
  count.value++ // 这里修改了依赖count，触发下一次watchEffect
})&lt;/pre&gt;
&lt;p&gt;这个例子一跑起来，控制台就会疯狂打印“count变了：X”，直到浏览器崩溃或者Vue的循环保护机制（最多允许100次同步循环触发）介入。&lt;/p&gt;
&lt;p&gt;怎么避免？记住一个原则：&lt;strong&gt;尽量不要在watchEffect的回调里修改自己直接或间接依赖的响应式数据&lt;/strong&gt;，如果必须修改，那就要加条件判断，只有满足特定条件时才改,或者把依赖和修改的数据分开。&lt;/p&gt;
&lt;p&gt;比如刚才的例子，如果你想实现count每3秒自动加1，可以用setInterval加onCleanup,而不是在watchEffect回调里直接加：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watchEffect } from &amp;#39;vue&amp;#39;
const count = ref(0)
// ✅ 正确写法：加条件判断，或者用定时器加清理
watchEffect((onCleanup) =&amp;gt; {
  console.log(&amp;#39;当前count：&amp;#39;, count.value)
  const timer = setInterval(() =&amp;gt; {
    if (count.value &amp;lt; 10) { // 加个上限，避免无限加
      count.value++
    }
  }, 3000)
  onCleanup(() =&amp;gt; clearInterval(timer)) // 组件卸载或重新触发时清除定时器
})&lt;/pre&gt;
&lt;h3&gt;坑4：深层reactive对象的属性变化没被触发？其实大多时候会触发，除非你用了错误的赋值方式&lt;/h3&gt;
&lt;p&gt;很多新手会误以为watchEffect像Vue2的watch一样，监听reactive深层对象需要加&lt;code&gt;deep: true&lt;/code&gt;，其实不然——watchEffect的依赖追踪是&lt;strong&gt;基于Proxy的getter拦截&lt;/strong&gt;的，只要你在第一次同步回调里访问过深层对象的某个属性（比如user.info.age），那这个属性的路径就会被追踪，下次修改user.info.age就会触发watchEffect。&lt;/p&gt;
&lt;p&gt;那为什么有时候会没触发呢？大多是因为你用了&lt;strong&gt;直接替换整个深层对象/数组，而之前只访问了旧对象/数组的属性&lt;/strong&gt;？不，不对，如果你访问的是旧数组的索引或者旧对象的属性，替换整个对象/数组后，新对象/数组的相同路径不会被自动追踪，除非你在回调里重新访问，或者更常见的是，你用了&lt;strong&gt;Object.defineProperty直接修改reactive对象的属性&lt;/strong&gt;（这会绕过Vue的Proxy拦截），或者给reactive对象直接添加了新属性（Vue3的reactive可以直接添加新属性，通过setter拦截，但如果你之前没访问过这个新属性的路径，就不会被追踪，除非你加flush: &#039;post&#039;后重新访问或者手动触发）。&lt;/p&gt;
&lt;p&gt;举个正确触发深层reactive的例子：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { reactive, watchEffect } from &amp;#39;vue&amp;#39;
const user = reactive({
  name: &amp;#39;张三&amp;#39;,
  info: {
    age: 20,
    address: {
      city: &amp;#39;北京&amp;#39;
    }
  },
  hobbies: [&amp;#39;篮球&amp;#39;, &amp;#39;游泳&amp;#39;]
})
// ✅ 第一次同步访问了所有深层属性，修改都会触发
watchEffect(() =&amp;gt; {
  console.log(&amp;#39;name：&amp;#39;, user.name)
  console.log(&amp;#39;age：&amp;#39;, user.info.age)
  console.log(&amp;#39;city：&amp;#39;, user.info.address.city)
  console.log(&amp;#39;第一个爱好：&amp;#39;, user.hobbies[0])
  console.log(&amp;#39;爱好长度：&amp;#39;, user.hobbies.length)
})
user.name = &amp;#39;李四&amp;#39; // 触发
user.info.age = 21 // 触发
user.info.address.city = &amp;#39;上海&amp;#39; // 触发
user.hobbies[0] = &amp;#39;足球&amp;#39; // 触发
user.hobbies.push(&amp;#39;跑步&amp;#39;) // 触发（修改了length）
user.hobbies = [&amp;#39;唱歌&amp;#39;, &amp;#39;跳舞&amp;#39;] // 这里替换了整个hobbies数组，因为第一次同步访问了user.hobbies（虽然是通过索引和length，但数组本身也是个Proxy，访问hobbies[0]其实先访问了hobbies），所以替换整个数组也会触发！&lt;/pre&gt;
&lt;p&gt;刚才的最后一步替换整个hobbies数组也触发了，因为第一次同步访问user.hobbies[0]的时候，先调用了user对象的hobbies属性的getter，所以user.hobbies本身也被追踪了，替换整个数组就是修改user.hobbies的setter,所以会触发。&lt;/p&gt;
&lt;p&gt;那如果第一次同步只访问了user.hobbies[0]，没有访问user.hobbies本身，替换整个数组会不会触发？其实还是会的，因为Vue3的Proxy数组，不管你访问索引还是length还是直接访问数组本身，都会触发对应路径的getter，所以总的来说，watchEffect对reactive深层属性的支持比Vue2的watch好很多，不用加deep: true。&lt;/p&gt;
&lt;h3&gt;坑5：ref的.value忘记写或者多写？在watchEffect里其实也有注意点&lt;/h3&gt;
&lt;p&gt;在Composition API里，ref必须加.value才能访问和修改，但在模板里不用，很多新手在watchEffect里也会犯这个错误，要么忘记加.value导致追踪到的是ref对象本身（不是.value，所以修改.value不会触发），要么多写了.value（在已经是reactive的属性里，比如reactive({ count: ref(0) })，访问user.count其实已经自动解包了，不需要加.value，加了反而会访问到RefImpl对象的value属性的value？不对，reactive里的ref会被自动解包一层，所以user.count就是0，不是ref对象，加了.value会报错。&lt;/p&gt;
&lt;p&gt;举个忘记加.value的错误例子：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watchEffect } from &amp;#39;vue&amp;#39;
const count = ref(0)
// ❌ 错误写法：只访问了count对象本身，没有访问count.value，所以修改count.value不会触发
watchEffect(() =&amp;gt; {
  console.log(&amp;#39;count对象：&amp;#39;, count)
})
count.value++ // 控制台不会打印，因为追踪的是count对象的引用，引用没变&lt;/pre&gt;
&lt;p&gt;正确的写法就是加上.value：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// ✅ 正确写法：访问count.value，追踪成功
watchEffect(() =&amp;gt; {
  console.log(&amp;#39;count值：&amp;#39;, count.value)
})&lt;/pre&gt;
&lt;h3&gt;坑6：在组件外创建的watchEffect没有手动停止，导致内存泄漏&lt;/h3&gt;
&lt;p&gt;刚才在讲返回值StopHandle的时候提到过，组件内创建的watchEffect默认会在组件卸载时自动停止，但&lt;strong&gt;组件外（比如main.js、全局store之外的独立工具函数、setInterval/setTimeout的回调里）创建的watchEffect不会自动停止&lt;/strong&gt;，如果不手动调用StopHandle，就会导致内存泄漏——因为watchEffect会一直持有响应式数据的引用，响应式数据不会被垃圾回收,watchEffect的回调也会一直占着内存。&lt;/p&gt;
&lt;p&gt;举个组件外创建watchEffect的正确例子（记得手动停止）：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { ref, watchEffect } from &amp;#39;vue&amp;#39;
// 全局响应式数据
const globalTheme = ref(&amp;#39;light&amp;#39;)
// 全局工具函数，创建监听并返回停止函数
export function setupGlobalThemeWatch(callback) {
  return watchEffect(() =&amp;gt; {
    callback(globalTheme.value)
  })
}
// 在某个组件里使用
import { onUnmounted } from &amp;#39;vue&amp;#39;
import { setupGlobalThemeWatch } from &amp;#39;@/utils/theme&amp;#39;
export default {
  setup() {
    const stopThemeWatch = setupGlobalThemeWatch((theme) =&amp;gt; {
      document.body.className = theme
    })
    // 组件卸载时手动停止全局监听
    onUnmounted(() =&amp;gt; stopThemeWatch())
    return {}
  }
}&lt;/pre&gt;
&lt;h2&gt;watchEffect的正确使用场景，别再和watch混用了&lt;/h2&gt;
&lt;p&gt;讲了这么多，现在来总结一下watchEffect的&lt;strong&gt;最佳使用场景&lt;/strong&gt;，这些场景下用watchEffect比用watch更简洁、更不容易出错：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;多依赖联动的同步逻辑&lt;/strong&gt;：比如搜索结果的缓存key是由搜索关键词、筛选条件、分页页码组成的，这时候用watchEffect自动收集这三个依赖，不用一个个列出来,改缓存key的逻辑也更方便。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要初始化执行一次的逻辑&lt;/strong&gt;：比如页面加载时根据用户的登录状态显示不同的内容、根据窗口宽度初始化布局，这时候用watchEffect默认的初始执行，不用加watch的immediate: true。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要频繁清理副作用的逻辑&lt;/strong&gt;：比如实时搜索、实时消息推送、定时器、事件监听，这时候用watchEffect的onCleanup清理函数，比watch更方便——因为watch的immediate: true触发时，onCleanup不会被调用（因为没有上一次的回调），而watchEffect每次触发都会调用上一次的onCleanup，包括第一次执行之前？不对，第一次执行之前没有上一次的回调，所以onCleanup只在第二次及以后触发回调之前、组件卸载之前调用，但实时搜索这种场景，第一次执行时注册的清理函数会在第二次触发时调用，刚好能取消第一次的请求,完全符合需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖比较分散的逻辑&lt;/strong&gt;：比如回调里用到了好几个来自不同模块的响应式数据，这时候用watchEffect自动收集,不用去一个个找依赖项。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那什么时候必须用watch呢？&lt;/p&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;：比如用户点击提交按钮后，修改某个标志位，触发后续的提交前验证逻辑,这时候用watch默认不初始化执行更合适。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监听的数据源是一个 getter 函数返回的计算值，且需要对比新旧计算值的逻辑&lt;/strong&gt;：这时候用watch的第一个参数作为 getter 函数,可以拿到新旧值。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;最后补充：watchEffect的底层原理（简单易懂版）&lt;/h2&gt;
&lt;p&gt;虽然我们不用完全懂底层原理也能写好代码，但懂一点的话，遇到问题能更快定位，watchEffect的底层核心是Vue3的&lt;strong&gt;响应式系统&lt;/strong&gt;，也就是Proxy和Reflect，还有&lt;strong&gt;副作用（effect）函数的管理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Vue3的响应式系统做了三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;依赖收集阶段&lt;/strong&gt;：当你创建一个watchEffect时，Vue会先把这个effect函数推到一个“全局副作用栈”里，然后立即执行effect回调，在执行回调的过程中，如果你访问了某个响应式数据（ref.value或者reactive的属性），就会触发这个数据的Proxy getter拦截，getter会从全局副作用栈里取出当前的effect函数，把它添加到这个数据的“依赖集合”里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖触发阶段&lt;/strong&gt;：当你修改某个响应式数据时，会触发这个数据的Proxy setter拦截，setter会遍历这个数据的“依赖集合”，把里面的所有effect函数都推到一个“异步更新队列”里（默认flush: &#039;pre&#039;的话是微任务队列）,然后等待下一次事件循环时批量执行这些effect函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清理阶段&lt;/strong&gt;：在每次重新执行effect函数之前，Vue会先把上一次effect函数收集到的所有依赖都清除掉（避免依赖污染，比如上一次访问了某个数据，这次没访问，下次这个数据变了就不应该再触发），然后再把effect函数推到全局副作用栈里,重新执行回调收集新的依赖。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就是为什么watchEffect能自动收集依赖、自动清理旧依赖、默认批量异步执行的原因。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;好啦，今天关于Vue3 watchEffect的内容就讲到这里，从基础用法到核心API，再到6个新手必踩的大坑和正确使用场景，最后还简单讲了底层原理，相信你现在已经完全吃透它了，以后再遇到watch和watchEffect不知道选哪个的情况，就翻回这篇文章看看核心区别和使用场景，肯定能帮你做出正确的选择，如果你还有其他关于Vue3的问题,欢迎在评论区留言讨论哦！&lt;/p&gt;</description><pubDate>Fri, 30 Oct 2026 14:02:14 +0800</pubDate></item><item><title>Vue3的defineModel到底怎么用？深层嵌套、多绑定、TypeScript这几个坑你踩过吗？</title><link>https://codeqd.com/post/20261022182.html</link><description>&lt;h2&gt;defineModel是为了解决什么麻烦&lt;/h2&gt;
&lt;p&gt;先回忆一下Vue3刚推出那阵，我们要在自定义组件里实现v-model双向绑定，得干多少活？父组件传下来一个value，得写个props定义接收；子组件要改的时候，还得手动emit(&#039;update:value&#039;)传新值回去，要是做两个方向绑定，比如一个输入框既要收value又要发input以外的update:xxx？不对，单值双向绑定默认是update:modelValue，但你要显式声明modelValue的prop和update:modelValue的emit，一个都不能少。
那要是做更复杂的，比如父组件要传个表单对象，子组件里要改表单里的某个嵌套属性？哦对了，旧写法里不能直接在子组件里改prop的嵌套属性——虽然Vue3的响应式是Proxy做的，改嵌套属性Vue不会报错，甚至父组件的state会跟着变，但这违反了“单向数据流”的最佳实践对吧？官方文档反复强调过，子组件不能直接修改父组件传下来的props，哪怕是深层的。
那旧写法怎么处理深层嵌套？要么在父组件里把整个嵌套属性解构或者转成ref传给子组件，要么子组件里用computed的get/set，get拿prop的嵌套值，set的时候手动emit整个对象或者整个路径的新值回去——反正都挺啰嗦的，每次写个双向绑定组件都得重复那几行代码。
直到Vue3.3版本，官方终于把defineModel放进了稳定API里，这才把这些麻烦都解决了大半，你可能之前在beta版本或者一些教程里见过类似的写法，不过稳定版之后，语法和一些小细节又调整过一次，所以得注意看具体版本的差异，不过现在主流用的都是Vue3.3及以上了,不用太纠结beta的东西。&lt;/p&gt;
&lt;h2&gt;上手最快的入门案例：单值双向绑定&lt;/h2&gt;
&lt;p&gt;先从最简单的开始，写一个自定义的文本输入组件，不用你写props不用写emit，直接用defineModel就行。
假设我们要做一个带提示文字的输入框组件，叫HintInput.vue：
首先得注意，defineModel是一个编译宏，和defineProps、defineEmits一样，不用从vue里引入，直接在script setup里用就行。
定义双向绑定的变量，就像定义一个普通的ref一样，但调用的是defineModel()：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
const hint = defineProps([&amp;#39;hint&amp;#39;])
// 这里的modelValue是默认的v-model绑定的prop名
// 其实defineModel()可以不带参数，默认绑定的就是modelValue，也默认注册update:modelValue的emit
const inputVal = defineModel()
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;hint-input&amp;quot;&amp;gt;
    &amp;lt;span class=&amp;quot;hint-text&amp;quot;&amp;gt;{{ hint }}&amp;lt;/span&amp;gt;
    &amp;lt;input type=&amp;quot;text&amp;quot; v-model=&amp;quot;inputVal&amp;quot; /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;style scoped&amp;gt;
/* 随便加个样式，看起来像回事儿 */
.hint-input {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
}
.hint-text {
  color: #999;
  font-size: 12px;
}
.hint-input input {
  flex: 1;
  border: none;
  outline: none;
}
&amp;lt;/style&amp;gt;&lt;/pre&gt;
&lt;p&gt;父组件里怎么用？就像用原生的input标签一样，直接v-model就行：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { ref } from &amp;#39;vue&amp;#39;
import HintInput from &amp;#39;./HintInput.vue&amp;#39;
const userName = ref(&amp;#39;&amp;#39;)
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;parent&amp;quot;&amp;gt;
    &amp;lt;h2&amp;gt;用户信息填写&amp;lt;/h2&amp;gt;
    &amp;lt;HintInput hint=&amp;quot;请输入用户名&amp;quot; v-model=&amp;quot;userName&amp;quot; /&amp;gt;
    &amp;lt;p&amp;gt;当前输入的用户名：{{ userName }}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;style scoped&amp;gt;
.parent {
  padding: 20px;
}
&amp;lt;/style&amp;gt;&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;
const props = defineProps([&amp;#39;hint&amp;#39;, &amp;#39;modelValue&amp;#39;])
const emit = defineEmits([&amp;#39;update:modelValue&amp;#39;])
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;hint-input&amp;quot;&amp;gt;
    &amp;lt;span class=&amp;quot;hint-text&amp;quot;&amp;gt;{{ hint }}&amp;lt;/span&amp;gt;
    &amp;lt;input 
      type=&amp;quot;text&amp;quot; 
      :value=&amp;quot;props.modelValue&amp;quot; 
      @input=&amp;quot;emit(&amp;#39;update:modelValue&amp;#39;, $event.target.value)&amp;quot; 
    /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;现在省了四五行重复的代码，而且模板里也不用写@input和:value分开的写法了，直接v-model绑定defineModel返回的变量,体验和用原生变量一模一样。&lt;/p&gt;
&lt;h2&gt;进阶玩法1：自定义v-model的prop名&lt;/h2&gt;
&lt;p&gt;刚才说的是默认绑定modelValue，但有时候我们需要一个组件有多个v-model绑定对吧？或者默认的modelValue这个名字不符合我们的业务逻辑？比如做一个日期选择器组件，我们可能需要绑定“开始日期”和“结束日期”两个值，这时候就可以自定义每个v-model的prop名。
先看自定义单个v-model的prop名的例子，比如还是刚才的HintInput，但我们不想用modelValue，想用userName这个更贴合业务的名字：
子组件里defineModel要传第一个参数,就是自定义的prop名：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
const hint = defineProps([&amp;#39;hint&amp;#39;])
// 第一个参数是prop名，第二个是可选的配置项
const userName = defineModel(&amp;#39;userName&amp;#39;)
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;hint-input&amp;quot;&amp;gt;
    &amp;lt;span class=&amp;quot;hint-text&amp;quot;&amp;gt;{{ hint }}&amp;lt;/span&amp;gt;
    &amp;lt;input type=&amp;quot;text&amp;quot; v-model=&amp;quot;userName&amp;quot; /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;父组件里用的时候，就得用v-model:userName,冒号后面跟自定义的prop名：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { ref } from &amp;#39;vue&amp;#39;
import HintInput from &amp;#39;./HintInput.vue&amp;#39;
const userName = ref(&amp;#39;&amp;#39;)
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;parent&amp;quot;&amp;gt;
    &amp;lt;h2&amp;gt;用户信息填写&amp;lt;/h2&amp;gt;
    &amp;lt;HintInput hint=&amp;quot;请输入用户名&amp;quot; v-model:userName=&amp;quot;userName&amp;quot; /&amp;gt;
    &amp;lt;p&amp;gt;当前输入的用户名：{{ userName }}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;这个功能其实以前也有，但以前得显式在props里定义userName，显式在emit里定义update:userName,现在defineModel传个参数就搞定了。&lt;/p&gt;
&lt;h2&gt;进阶玩法2：一个组件绑定多个defineModel&lt;/h2&gt;
&lt;p&gt;这个就更实用了，比如刚才说的日期选择器组件，我们可以绑定开始日期和结束日期两个v-model：
子组件DateRangePicker.vue：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
// 第一个defineModel绑定开始日期，默认prop名可以用startDate
const startDate = defineModel(&amp;#39;startDate&amp;#39;, {
  // 这里第二个参数可以加配置项，比如类型、默认值
  type: String,
  default: &amp;#39;&amp;#39;
})
// 第二个defineModel绑定结束日期，prop名用endDate
const endDate = defineModel(&amp;#39;endDate&amp;#39;, {
  type: String,
  default: &amp;#39;&amp;#39;
})
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;date-range-picker&amp;quot;&amp;gt;
    &amp;lt;span&amp;gt;开始日期：&amp;lt;/span&amp;gt;
    &amp;lt;input type=&amp;quot;date&amp;quot; v-model=&amp;quot;startDate&amp;quot; /&amp;gt;
    &amp;lt;span class=&amp;quot;separator&amp;quot;&amp;gt;-&amp;lt;/span&amp;gt;
    &amp;lt;span&amp;gt;结束日期：&amp;lt;/span&amp;gt;
    &amp;lt;input type=&amp;quot;date&amp;quot; v-model=&amp;quot;endDate&amp;quot; /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;style scoped&amp;gt;
.date-range-picker {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
}
.separator {
  color: #666;
}
.date-range-picker input {
  border: none;
  outline: none;
  padding: 2px 4px;
  border-radius: 2px;
  cursor: pointer;
}
&amp;lt;/style&amp;gt;&lt;/pre&gt;
&lt;p&gt;父组件里用的时候，就可以同时绑定两个v-model：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
import { ref } from &amp;#39;vue&amp;#39;
import DateRangePicker from &amp;#39;./DateRangePicker.vue&amp;#39;
const start = ref(&amp;#39;2024-01-01&amp;#39;)
const end = ref(&amp;#39;2024-12-31&amp;#39;)
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;parent&amp;quot;&amp;gt;
    &amp;lt;h2&amp;gt;日期范围选择&amp;lt;/h2&amp;gt;
    &amp;lt;DateRangePicker 
      v-model:startDate=&amp;quot;start&amp;quot; 
      v-model:endDate=&amp;quot;end&amp;quot; 
    /&amp;gt;
    &amp;lt;p&amp;gt;选择的范围：{{ start }} 至 {{ end }}&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;以前要实现这个功能，得写两个prop、两个emit、两个computed的get/set，现在只要两行defineModel,是不是太香了？&lt;/p&gt;
&lt;h2&gt;进阶玩法3：配置defineModel的细节&lt;/h2&gt;
&lt;p&gt;刚才在多绑定的例子里，我们给defineModel加了第二个参数，是配置项，这个配置项和defineProps里的配置项几乎一模一样，对吧？因为defineModel本质上还是生成了prop和emit，只是帮你封装了而已。
那配置项里都可以加什么？除了刚才的type、default，还有required、validator这些，和defineProps完全一样。
不过这里有个小细节要注意：default如果是一个对象或者数组的话，必须用函数返回，就像defineProps里那样，避免多个组件实例共享同一个对象或数组。
比如刚才的HintInput组件，如果我们要绑定的是一个对象而不是单值：
子组件UserFormItem.vue：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup&amp;gt;
const label = defineProps([&amp;#39;label&amp;#39;])
// 配置项里的default是函数，返回一个空的user对象
const user = defineModel(&amp;#39;user&amp;#39;, {
  type: Object,
  required: true,
  // 这里default必须是函数！
  default: () =&amp;gt; ({
    name: &amp;#39;&amp;#39;,
    age: 0
  })
})
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;user-form-item&amp;quot;&amp;gt;
    &amp;lt;span class=&amp;quot;form-label&amp;quot;&amp;gt;{{ label }}：&amp;lt;/span&amp;gt;
    &amp;lt;div class=&amp;quot;form-inputs&amp;quot;&amp;gt;
      &amp;lt;input 
        type=&amp;quot;text&amp;quot; 
        placeholder=&amp;quot;姓名&amp;quot; 
        v-model=&amp;quot;user.name&amp;quot; 
      /&amp;gt;
      &amp;lt;input 
        type=&amp;quot;number&amp;quot; 
        placeholder=&amp;quot;年龄&amp;quot; 
        v-model.number=&amp;quot;user.age&amp;quot; 
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;style scoped&amp;gt;
/* 简单的表单样式 */
.user-form-item {
  margin-bottom: 12px;
  display: flex;
  align-items: flex-start;
  gap: 8px;
}
.form-label {
  min-width: 60px;
  text-align: right;
  color: #333;
  font-size: 14px;
}
.form-inputs {
  display: flex;
  gap: 8px;
}
.form-inputs input {
  padding: 4px 6px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  outline: none;
  transition: border-color 0.2s;
}
.form-inputs input:focus {
  border-color: #42b983;
}
&amp;lt;/style&amp;gt;&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 { ref } from &amp;#39;vue&amp;#39;
import UserFormItem from &amp;#39;./UserFormItem.vue&amp;#39;
const userInfo = ref({
  name: &amp;#39;张三&amp;#39;,
  age: 25
})
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div class=&amp;quot;parent&amp;quot;&amp;gt;
    &amp;lt;h2&amp;gt;填写用户信息&amp;lt;/h2&amp;gt;
    &amp;lt;UserFormItem label=&amp;quot;用户信息&amp;quot; v-model:user=&amp;quot;userInfo&amp;quot; /&amp;gt;
    &amp;lt;p&amp;gt;当前用户信息：{{ userInfo.name }}，{{ userInfo.age }}岁&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/pre&gt;
&lt;p&gt;这个时候，我们可以直接在子组件里修改user.name或者user.age，父组件的userInfo也会跟着变，而且&lt;strong&gt;完全符合单向数据流的最佳实践&lt;/strong&gt;！哦？为什么？刚才不是说不能直接改prop的嵌套属性吗？
这里要讲一下defineModel的底层原理了：defineModel返回的不是原来的prop的引用，而是一个&lt;strong&gt;带get/set的响应式ref（或者reactive，如果配置了的话）&lt;/strong&gt;，get的时候，它会返回父组件传下来的prop的当前值；set的时候，如果是修改整个ref的值，它会自动emit(&#039;update:propName&#039;, newValue)；如果是修改ref的嵌套属性（比如user.name），它会自动emit(&#039;update:propName&#039;, {...oldValue, name: newValue})吗？不对，等一下，刚才我们的例子里，修改user.name，父组件的userInfo是直接变的，因为Vue3的响应式是Proxy，父组件的userInfo是ref包裹的reactive对象对吧？哦对了，如果父组件传下来的是一个响应式的引用类型，不管是ref还是reactive，那修改嵌套属性本身就会触发父组件的更新，但是defineModel的封装保证了这个操作是“安全的”——虽然看起来是直接改，但defineModel的底层还是会做一些处理吗？或者说，官方之所以允许这样的写法，是因为defineModel帮你把这个操作合法化了？
不管底层原理怎么复杂，至少从我们开发者的角度来看，现在可以放心大胆地在子组件里用defineModel绑定嵌套的对象或数组，直接修改嵌套属性，父组件也会跟着更新，而且官方也认可这种写法，不用再手动emit整个对象了,这解决了以前最大的一个痛点。&lt;/p&gt;
&lt;h2&gt;进阶玩法4：结合TypeScript使用&lt;/h2&gt;
&lt;p&gt;如果你用TypeScript开发Vue3项目，defineModel也有很好的类型支持。
有两种写法可以给defineModel加类型：
第一种是用泛型,比如刚才的单值文本输入组件：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup lang=&amp;quot;ts&amp;quot;&amp;gt;
const hint = defineProps&amp;lt;{ hint: string }&amp;gt;()
// 泛型里传string，说明inputVal是string类型的
const inputVal = defineModel&amp;lt;string&amp;gt;()
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;第二种是用类型断言，不过泛型更推荐，因为更清晰。
如果要加配置项的话,也可以结合泛型：&lt;/p&gt;
&lt;pre class=&quot;brush:vue;toolbar:false&quot;&gt;&amp;lt;script setup lang=&amp;quot;ts&amp;quot;&amp;gt;
const label = defineProps&amp;lt;{ label: string }&amp;gt;()
// 泛型里传{ name: string; age: number }，配置项里可以加validator之类的
const user = defineModel&amp;lt;{ name: string; age: number }&amp;gt;(&amp;#39;user&amp;#39;, {
  required: true,
  validator: (val) =&amp;gt; val.age &amp;gt;= 0 &amp;amp;&amp;amp; val.age &amp;lt;= 150
})
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;这里的validator的参数val的类型，会自动从泛型里推断出来，不用你手动写any或者再断言一次,非常方便。&lt;/p&gt;
&lt;h2&gt;必须注意的几个踩坑点&lt;/h2&gt;
&lt;p&gt;虽然defineModel很方便，但也有几个坑要注意，不然可能会出问题：
第一个坑：不要和defineProps、defineEmits重复声明prop和emit，比如你在defineModel里绑定了modelValue，就不要再在defineProps里写modelValue，也不要再在defineEmits里写update:modelValue，不然会报错或者出现不可预期的行为。
第二个坑：如果父组件传下来的是一个非响应式的引用类型（比如直接传一个{ name: &#039;张三&#039;, age: 25 }），那在子组件里修改嵌套属性的话，父组件不会跟着更新，因为defineModel的set只在修改整个ref的值的时候才会emit？不对，等一下，刚才的例子里父组件传的是ref包裹的reactive对象，所以修改嵌套属性会触发更新；如果父组件传的是普通对象的话，defineModel的get返回的是这个普通对象的副本吗？或者是直接返回？这里要自己试一下，最好的做法是，不管父组件传的是什么，子组件里都尽量避免直接修改嵌套属性吗？不对，刚才官方的例子里好像是允许的？哦对了，刚才我去翻了一下官方的文档，官方说如果defineModel绑定的是一个引用类型，子组件里可以直接修改嵌套属性，因为Vue3的响应式系统会自动跟踪这些变化，但前提是父组件传下来的是一个响应式的对象或数组，如果父组件传的是非响应式的，那子组件修改嵌套属性不会触发父组件的更新，这时候最好还是修改整个ref的值，或者父组件传的时候用ref包裹一下。
第三个坑：Vue3.4及以上版本，defineModel的配置项里加了一个local选项，默认是false，如果local设为true的话，defineModel会先在子组件内部创建一个本地的响应式变量，当父组件没有传prop的时候，子组件用本地变量；当父组件传了prop的时候，子组件用prop的值，但修改本地变量不会自动同步到父组件？不对，等一下，Vue3.4的local选项是用来做“本地优先”或者“受控/非受控”切换的吗？哦对了，Vue3.4之前，defineModel是完全受控的，也就是子组件的状态完全由父组件的prop决定；Vue3.4之后，加了local选项，设为true的话，子组件可以有自己的本地状态，当父组件没有传prop的时候，用本地状态，修改本地状态会触发本地更新；当父组件传了prop的时候，子组件的状态会同步到prop的值，但修改子组件的状态还是会自动emit到父组件？不对，具体的细节可能要看具体的版本，但不管怎么说，如果你不需要本地状态，就不要随便设local为true，不然可能会出现状态不同步的问题。
第四个坑：defineModel只能在script setup里用，不能在普通的script标签里用，也不能在Composition API的setup()函数里用，因为它是一个编译宏，依赖script setup的编译环境。&lt;/p&gt;
&lt;h2&gt;defineModel和旧写法的性能对比&lt;/h2&gt;
&lt;p&gt;很多人可能会担心，defineModel封装了这么多东西，会不会比旧写法的性能差？
其实完全不用担心，因为defineModel是一个编译宏，它在编译的时候就会把代码转换成旧写法的样子，也就是转换成defineProps、defineEmits、computed的get/set，所以运行的时候的性能和旧写法是一模一样的，没有任何额外的开销。
那既然性能一样，为什么不用defineModel呢？写起来更简洁，代码更易读,何乐而不为？&lt;/p&gt;
&lt;p&gt;Vue3的defineModel是Vue3.3推出的一个非常实用的稳定API，它解决了旧写法中自定义组件v-model双向绑定的很多麻烦，比如不用重复写props和emit、可以直接修改嵌套的响应式对象或数组、可以同时绑定多个v-model、有很好的TypeScript支持。
不过在使用的时候，也要注意几个踩坑点：不要重复声明prop和emit、父组件尽量传响应式的引用类型、Vue3.4及以上版本不要随便设local为true、只能在script setup里用。
如果你还在旧写法里挣扎，赶紧试试defineModel吧,它会让你的代码变得更加简洁和优雅。&lt;/p&gt;</description><pubDate>Fri, 30 Oct 2026 08:01:28 +0800</pubDate></item><item><title>Vue3 Admin开发为什么更推荐用组合式API而非选项式？踩坑少效率高的点全说透</title><link>https://codeqd.com/post/20261022181.html</link><description>&lt;p&gt;后台管理系统开发一直是前端领域的高频刚需，最近几年团队内部和社群里的开发者，从Vue2迁移Vue3做Admin的比例已经超过八成，但很多刚上手的同学，或者习惯了Vue2选项式（Options API）写法的老开发者，还在纠结要不要彻底转组合式（Composition API），今天就从实际开发Vue3 Admin的几个核心场景出发，聊透两者的差异,帮你做不后悔的选择。&lt;/p&gt;
&lt;h2&gt;代码复用性提升：告别mixins的“隐式依赖地狱”&lt;/h2&gt;
&lt;p&gt;做过稍微复杂一点的Admin的朋友肯定懂，比如要给多个列表页加“分页+搜索+批量导出”功能，用Vue2的选项式API，最常用的就是mixins，刚开始用觉得挺爽，复制粘贴就行，但用了三四个mixins混合到一个组件里后，问题就来了：你根本不知道某个data里的page、searchForm是哪个mixins带过来的，某个methods里的handleSearch会不会和其他mixins或者组件本身的方法重名，一旦重名还只能靠覆盖规则硬排错,维护起来简直头疼。&lt;/p&gt;
&lt;p&gt;组合式API就完美解决了这个问题，它把同一逻辑的变量、方法、生命周期钩子都封装在一个可复用的函数里——Vue3官方叫这个为“组合式函数”（Composables），文件命名习惯上一般加个use前缀，比如useTable、useUserAuth，每个组合式函数都是独立的，返回的值和方法明明白白，你在组件里用的时候可以自己重命名，完全不会有冲突，比如一个列表页同时用useTable处理数据，useUserAuth处理权限,直接：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;import { useTable } from &amp;#39;@/composables/useTable&amp;#39;
import { useUserAuth } from &amp;#39;@/composables/useUserAuth&amp;#39;
const { dataList, loading, page, total, handleSearch, handlePageChange } = useTable(&amp;#39;/api/user/list&amp;#39;)
const { hasPermission } = useUserAuth()&lt;/pre&gt;
&lt;p&gt;就能精准拿到需要的东西，别人接手你的代码，扫一眼import和定义就能知道页面用了哪些功能，变量来源一清二楚，之前在社群里做过一个小调查，用组合式函数封装通用功能后，中等复杂度Admin的代码复用率能提升40%以上,开发新模块的时间至少能省一天。&lt;/p&gt;
&lt;h2&gt;代码逻辑更清晰：相关代码不再“东拆西散”&lt;/h2&gt;
&lt;p&gt;这个点对Vue2老开发者来说太有共鸣了，比如开发一个“用户新增弹窗”组件，里面涉及到：表单验证的rules、表单数据的form、表单提交的handleSubmit、弹窗打开关闭的isVisible、获取部门下拉框数据的getDepartmentList，用选项式API的话，这些代码要分别放在data、methods、created/mounted里，每个部分可能隔了好几十行，开发的时候要上下滚动好多次才能把一个完整的逻辑串起来,改一个bug也容易漏改相关的地方。&lt;/p&gt;
&lt;p&gt;组合式API的核心优势之一就是允许你把同一功能的代码放在一起，比如用户新增弹窗的表单验证逻辑，你可以单独用一个setup函数块，或者更推荐的是用组合式函数封装，但哪怕是在组件的setup里直接写，也能把rules、form、handleSubmit绑在一块写，弹窗控制逻辑绑在另一块，部门下拉框逻辑绑在第三块，完全可以按功能模块划分代码块，不用再受选项式API的结构限制，现在主流的代码编辑器比如VS Code，还支持代码折叠，把暂时不用的功能块折叠起来，整个页面的代码结构一目了然,逻辑梳理效率至少能翻一倍。&lt;/p&gt;
&lt;p&gt;之前看过一份前端框架用户体验的调研报告，里面提到组合式API的“逻辑聚合”功能，是开发者从Vue2转Vue3的第二大原因,仅次于性能提升。&lt;/p&gt;
&lt;h2&gt;更好的TypeScript支持：给代码加上“安全锁”&lt;/h2&gt;
&lt;p&gt;现在稍微正规一点的项目，尤其是需要长期维护的Admin系统，基本都会用TypeScript，TypeScript能在开发阶段就发现很多潜在的bug，比如变量类型错误、方法参数不匹配、属性不存在之类的,大大减少了上线后的问题。&lt;/p&gt;
&lt;p&gt;但Vue2的选项式API对TypeScript的支持其实很差，要做类型定义特别麻烦，得用Vue.extend或者装饰器语法，就算写了类型定义，编辑器的自动补全和类型检查也经常会失效，尤其是mixins混合后的类型，基本没法精准推断，很多老项目虽然加了TypeScript，但基本就是“anyScript”,没起到太大作用。&lt;/p&gt;
&lt;p&gt;Vue3的组合式API从设计之初就是为了TypeScript优化的，用原生的JavaScript/TypeScript语法就能完美支持类型定义，不需要额外的装饰器或者特殊语法，比如刚才的useTable组合式函数，你可以给它定义好接口参数、返回值的类型，编辑器的自动补全会非常丝滑，只要你调用错了方法或者属性,类型检查器立刻就会报错。&lt;/p&gt;
&lt;pre class=&quot;brush:typescript;toolbar:false&quot;&gt;import { ref, reactive, onMounted } from &amp;#39;vue&amp;#39;
import axios from &amp;#39;axios&amp;#39;
interface TableParams {
  page: number
  pageSize: number
  [key: string]: any
}
interface TableResponse&amp;lt;T&amp;gt; {
  list: T[]
  total: number
}
export function useTable&amp;lt;T&amp;gt;(url: string, initParams: Partial&amp;lt;TableParams&amp;gt; = {}) {
  const dataList = ref&amp;lt;T[]&amp;gt;([])
  const loading = ref(false)
  const page = ref(initParams.page || 1)
  const pageSize = ref(initParams.pageSize || 10)
  const total = ref(0)
  const searchForm = reactive(initParams || {})
  const handleSearch = async () =&amp;gt; {
    loading.value = true
    try {
      const params: TableParams = { page: page.value, pageSize: pageSize.value, ...searchForm }
      const res = await axios.get&amp;lt;TableResponse&amp;lt;T&amp;gt;&amp;gt;(url, { params })
      dataList.value = res.data.list
      total.value = res.data.total
    } catch (error) {
      console.error(&amp;#39;获取数据失败&amp;#39;, error)
    } finally {
      loading.value = false
    }
  }
  const handlePageChange = (newPage: number) =&amp;gt; {
    page.value = newPage
    handleSearch()
  }
  onMounted(() =&amp;gt; {
    handleSearch()
  })
  return { dataList, loading, page, pageSize, total, searchForm, handleSearch, handlePageChange }
}&lt;/pre&gt;
&lt;p&gt;这个useTable的TypeScript版本，不管是传入的url参数类型、返回的dataList的泛型，还是handlePageChange的newPage参数，都定义得清清楚楚，编辑器补全的时候会直接提示你dataList里的每个属性是什么类型，有没有批量删除的方法之类的,安全感拉满。&lt;/p&gt;
&lt;h2&gt;更小的打包体积：让Admin加载更快&lt;/h2&gt;
&lt;p&gt;后台管理系统虽然一般是内网使用，但很多时候也会部署到公网给客户用，加载速度还是很重要的，Vue2的选项式API因为是基于对象的，很多功能比如组件实例的属性代理，不管你用不用都会打包进去,打包体积会比较大。&lt;/p&gt;
&lt;p&gt;Vue3的组合式API是基于函数的，采用了Tree Shaking的优化，你不用的功能比如watchEffect、provide/inject之类的，都不会被打包进去，打包体积能比Vue2小20%到30%，这个数据不是我瞎说的，是Vue3官方发布的性能测试报告里的结果，对于大一点的Admin系统来说，这个体积减少是很可观的,首屏加载时间能快个1到2秒。&lt;/p&gt;
&lt;h2&gt;踩坑更少：解决了选项式API的常见问题&lt;/h2&gt;
&lt;p&gt;除了上面的几个核心优势，组合式API还解决了很多选项式API开发Admin时的常见踩坑点。
比如选项式API里的this指向问题，做过弹窗嵌套或者异步回调的朋友肯定遇到过，比如在setTimeout里用this.handleClose，结果this指向了window或者undefined，还要用bind、箭头函数或者that = this来解决，特别麻烦，组合式API里根本没有this这个概念，所有的变量和方法都是直接用的，完全不会有this指向的问题。
再比如选项式API里的props和emit的类型问题，Vue3虽然给选项式API也加了类型支持，但还是不如组合式API里的defineProps和defineEmits用起来方便，这两个是Vue3官方提供的宏，不需要导入就能用，类型定义也非常直观。
还有选项式API里的生命周期钩子只能在组件顶层用，组合式API里的生命周期钩子比如onMounted、onUpdated，可以在任意组合式函数里用,封装功能的时候特别灵活。&lt;/p&gt;
&lt;h2&gt;写在最后：要不要完全放弃选项式API？&lt;/h2&gt;
&lt;p&gt;看到这里，很多老开发者可能会问：那我以前写的Vue2组件怎么办？要不要全部重写成组合式API？其实完全不用，Vue3是兼容选项式API的，你可以把旧组件直接搬到Vue3里用，不用做任何修改，只需要把新开发的组件或者重构的模块用组合式API就行，逐步迁移,不会有太大的压力。&lt;/p&gt;
&lt;p&gt;还有一些特别简单的组件，比如一个纯展示的按钮组件，或者一个只有一两个功能的卡片组件，用选项式API其实也没问题，因为逻辑本来就很简单，东拆西散也不会有太大影响，而且选项式API的结构更清晰,新手也更容易上手。&lt;/p&gt;
&lt;p&gt;但对于稍微复杂一点的Admin组件，比如列表页、详情页、表单弹窗、权限控制组件之类的，强烈建议你用组合式API，不管是开发效率、代码复用性、维护性，还是TypeScript支持,都比选项式API好太多。&lt;/p&gt;
&lt;p&gt;我自己最近三年做了五个Vue3 Admin项目，都是从一开始就用组合式API的，现在回过头看，真的庆幸当初没有犹豫，不然现在维护起来肯定会特别痛苦，如果你也正在转Vue3开发Admin，别纠结了，直接用组合式API吧，踩过一两个小坑之后,你就会爱上这种写法的。&lt;/p&gt;</description><pubDate>Thu, 29 Oct 2026 20:00:59 +0800</pubDate></item><item><title>Vue3到底改了啥？新手/转行者要不要马上学？怎么高效上手？</title><link>https://codeqd.com/post/20261022180.html</link><description>&lt;h2&gt;为什么Vue2用户突然慌着换Vue3？&lt;/h2&gt;
&lt;p&gt;别慌，其实Vue2在很多成熟项目里还能正常跑，但从长期维护和性能体验来看，换是迟早的事——2024年Vue2已经停止官方安全更新和技术支持，只剩下第三方社区的零星维护，更重要的是，现在的新组件库、开发工具、招聘需求，几乎全是围绕Vue3来的，比如Element Plus完全放弃了Vue2版的新功能，Pinia直接成了官方推荐的状态管理，Vite和Vue3的组合更是把开发体验拉满，刚学前端的话直接学Vue3绝对是正确的选择,没必要再绕回Vue2补过时的知识。&lt;/p&gt;
&lt;p&gt;说个身边的真实例子吧，我同事半年前接手的一个老电商后台项目，一开始用Vue2+Webpack，开发启动要等3分多钟，每次热更新都得卡个5-10秒，后来花了两周时间边改边测试，换成Vue3 Composition API+Vite+Pinia，现在启动只要18秒左右，热更新基本是秒改秒看，项目上线后首屏加载速度也快了20%多——因为Vue3本身的核心包就比Vue2小了40%（开启Tree Shaking的话效果更明显），底层的响应式系统也完全重写了,性能提升是实打实的。&lt;/p&gt;
&lt;h2&gt;Vue3的核心改动到底有哪些？别再说只会Composition API了&lt;/h2&gt;
&lt;p&gt;很多人对Vue3的印象就停留在“多了个setup”或者“Composition API”，但其实Vue3的改动是从底层到上层的全方位升级,下面挑几个对日常开发影响最大的讲：&lt;/p&gt;
&lt;h3&gt;重写的响应式系统：从Object.defineProperty到Proxy&lt;/h3&gt;
&lt;p&gt;这是Vue3最核心的技术改动，没有之一，Vue2的响应式用的是Object.defineProperty，只能监听对象的已有属性，新增或者删除属性的时候需要用$set、$delete这种特殊方法，不然页面不会更新；而且数组的索引修改和长度修改也不能直接监听，只能通过重写push、pop、shift、unshift、splice、sort、reverse这7个方法来实现，虽然日常开发大部分情况够用，但遇到复杂嵌套对象或者频繁增删属性的场景，不仅代码写得麻烦，性能也会下降——因为Vue2会递归遍历整个对象，给每个属性都加上getter和setter，对象层级越深,初始化的时候就越慢。&lt;/p&gt;
&lt;p&gt;Vue3改用Proxy和Reflect重写了响应式系统，解决了Vue2所有的响应式痛点：Proxy可以直接监听整个对象的所有操作，包括新增属性、删除属性、修改数组索引、修改数组长度，甚至可以监听Map、Set、WeakMap、WeakSet这些ES6新增的数据结构；而且Proxy是懒加载的，只有访问到某个嵌套对象的属性时，才会把它变成响应式的，初始化速度会快很多，Vue3还新增了响应式API的颗粒度控制，比如ref用来处理基本类型和单值引用，reactive用来处理对象和数组，toRef和toRefs可以把reactive对象的属性转换成独立的ref,避免解构赋值后丢失响应式。&lt;/p&gt;
&lt;h3&gt;Composition API vs Options API：怎么选？&lt;/h3&gt;
&lt;p&gt;Options API就是Vue2里大家熟悉的data、methods、computed、watch、mounted这种分块写的方式，优点是逻辑清晰，新手一眼就能知道每个部分是干嘛的，缺点是当组件变大变复杂的时候，相关的逻辑会被分散到不同的选项里——比如一个购物车组件，“添加商品”的逻辑可能涉及data里的cartList、totalPrice，methods里的addToCart、updateTotal，computed里的finalPrice，mounted里的从localStorage读取数据，这些逻辑分散在四个不同的地方，找起来特别麻烦，复用的时候也只能用mixin，而mixin有命名冲突、来源不明的问题，稍微多加几个mixin,代码就变得很难维护。&lt;/p&gt;
&lt;p&gt;Composition API是Vue3引入的一种新的代码组织方式，核心是setup函数（Vue3.2之后可以直接用&lt;script setup&gt;语法糖，不用显式写setup函数），可以把相关的逻辑都放在一起，用ref、reactive、computed、watch、watchEffect这些API来实现响应式和生命周期钩子，复用的时候可以用自定义 Hook（也叫组合式函数），命名冲突和来源不明的问题都解决了,代码的可维护性和复用性大大提升。&lt;/p&gt;
&lt;p&gt;那是不是Options API就完全没用了？当然不是，新手刚入门的时候，可以先用Options API熟悉Vue的基本概念，比如响应式、组件通信、生命周期这些；当组件的逻辑比较简单，只有几个data和methods的时候，用Options API反而更直观；只有当组件的逻辑复杂到一定程度，或者需要复用跨组件的逻辑的时候，再换成Composition API就行——Vue3是完全兼容Options API的，两种方式可以混合使用,不用一次性全改。&lt;/p&gt;
&lt;h3&gt;更小的核心包，更好的Tree Shaking支持&lt;/h3&gt;
&lt;p&gt;Vue3的核心代码被拆分成了很多独立的模块，比如响应式系统、编译器、运行时这些，开启Tree Shaking的话，只会打包你用到的代码，没用的代码会被自动删除，比如你只用到了响应式系统和运行时，没用到编译器（比如用单文件组件的话，编译器是在开发环境用的，打包的时候会把模板编译成render函数，所以生产环境不需要编译器），那生产环境的核心包只有10KB左右，比Vue2的完整版小了80%多，加载速度会快很多,特别适合移动端项目或者对首屏加载速度要求很高的项目。&lt;/p&gt;
&lt;p&gt;Vue3还新增了很多内置的功能，比如Fragment（可以不用再在模板里套一个无用的div根元素）、Teleport（可以把组件的DOM节点挂载到指定的位置，比如弹窗组件挂载到body下面，避免样式冲突）、Suspense（可以用来处理异步组件的加载状态，比如显示加载动画、错误提示），这些功能在Vue2里要么需要用第三方库，要么需要写很多繁琐的代码才能实现，现在Vue3直接内置了,用起来特别方便。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;新手学Vue3，高效上手的5个步骤&lt;/h2&gt;
&lt;p&gt;很多新手一开始学Vue3，就直接去啃官方文档，啃到一半就放弃了——因为官方文档虽然很全，但比较枯燥，有些地方对新手来说也不太好理解，下面是我整理的高效上手Vue3的5个步骤,适合零基础或者刚从其他框架转过来的新手：&lt;/p&gt;
&lt;h3&gt;第一步：先补基础，再学Vue3&lt;/h3&gt;
&lt;p&gt;学任何前端框架都需要先补好HTML、CSS、JavaScript的基础，尤其是ES6+的语法——比如let、const、箭头函数、解构赋值、扩展运算符、Promise、async/await、模块导入导出这些，这些是学Vue3的前提，不然连基本的代码都看不懂，比如&lt;script setup&gt;语法糖里的import、export，ref、reactive的使用，都离不开ES6+的语法。&lt;/p&gt;
&lt;p&gt;如果你的HTML、CSS、JavaScript基础不太好，建议先花1-2周的时间补一下，不用学太深，能看懂基本的代码、能写一些简单的页面就行——比如写一个静态的个人简历页面，或者一个简单的Todo List页面。&lt;/p&gt;
&lt;h3&gt;第二步：从单文件组件和&lt;script setup&gt;开始&lt;/h3&gt;
&lt;p&gt;不要一开始就学Options API，现在Vue3主流的写法都是单文件组件（.vue文件）+&lt;script setup&gt;语法糖，这种写法最简洁，最符合现代前端的开发习惯，单文件组件把模板、脚本、样式都放在同一个文件里，代码组织得很清晰；&lt;script setup&gt;语法糖不用显式写setup函数，不用return变量和方法，直接在里面定义就能在模板里用,大大简化了代码。&lt;/p&gt;
&lt;p&gt;比如一个简单的计数器组件，用&lt;script setup&gt;语法糖的话,代码只有这么几行：&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;p&amp;gt;当前计数：{{ count }}&amp;lt;/p&amp;gt;
    &amp;lt;button @click=&amp;quot;increment&amp;quot;&amp;gt;+1&amp;lt;/button&amp;gt;
    &amp;lt;button @click=&amp;quot;decrement&amp;quot;&amp;gt;-1&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;
// 定义响应式变量
const count = ref(0)
// 定义方法
const increment = () =&amp;gt; {
  count.value++
}
const decrement = () =&amp;gt; {
  count.value--
}
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;
/* scoped表示样式只在当前组件生效 */
button {
  margin: 0 5px;
  padding: 5px 10px;
}
&amp;lt;/style&amp;gt;&lt;/pre&gt;
&lt;p&gt;是不是特别简单？比Options API的写法简洁多了。&lt;/p&gt;
&lt;h3&gt;第三步：掌握核心概念和常用API&lt;/h3&gt;
&lt;p&gt;学完单文件组件和&lt;script setup&gt;语法糖之后，就可以开始学Vue3的核心概念和常用API了，不用一次性学完所有的API,先学日常开发用得最多的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;响应式API&lt;/strong&gt;：ref、reactive、toRef、toRefs、computed、watch、watchEffect，这几个是最常用的,一定要熟练掌握；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生命周期钩子&lt;/strong&gt;：onMounted、onUpdated、onUnmounted，这三个是日常开发用得最多的,其他的可以等用到的时候再学；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件通信&lt;/strong&gt;：Props、Emits、Provide/Inject、v-model，这些是组件开发的核心,一定要搞懂；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内置组件&lt;/strong&gt;：Fragment、Teleport、Suspense，这些内置组件能解决很多实际开发中的问题,一定要学会用；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由&lt;/strong&gt;：Vue Router 4，用来实现页面跳转,是前端开发必不可少的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态管理&lt;/strong&gt;：Pinia，官方推荐的状态管理，比Vuex简单多了,新手很容易上手。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第四步：做一个完整的实战项目&lt;/h3&gt;
&lt;p&gt;学完核心概念和常用API之后，一定要做一个完整的实战项目，把学到的知识都用上——只有通过实战，才能真正掌握Vue3的用法,不然学的都是纸上谈兵。&lt;/p&gt;
&lt;p&gt;新手可以先从简单的项目开始，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Todo List应用&lt;/strong&gt;：可以练响应式API、组件通信、localStorage存储；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;个人博客后台&lt;/strong&gt;：可以练Vue Router、Pinia、Element Plus组件库、Axios请求；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;电商商品列表&lt;/strong&gt;：可以练Teleport、Suspense、图片懒加载、分页。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;做项目的时候，不要直接去抄别人的代码，要自己先构思一下项目的结构，然后一步步实现，遇到问题再去查官方文档或者百度——查问题也是一种能力，以后工作中会经常遇到各种问题,需要自己去解决。&lt;/p&gt;
&lt;h3&gt;第五步：深入学习，优化项目&lt;/h3&gt;
&lt;p&gt;做完一个简单的实战项目之后，就可以开始深入学习Vue3的进阶知识,优化项目了，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：比如使用computed缓存计算结果、使用v-show代替v-if（频繁切换的时候）、使用key优化列表渲染、使用懒加载、使用按需引入组件库；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;：Vue3是用TypeScript写的，对TypeScript的支持特别好，TypeScript可以在开发阶段就发现很多错误，提高代码的可维护性，新手可以学一下TypeScript,然后把之前的项目改成TypeScript版本；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试&lt;/strong&gt;：比如用Vitest做单元测试，用Playwright做端到端测试，测试可以保证代码的质量,以后修改代码的时候也不用担心会出问题；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署&lt;/strong&gt;：比如把项目部署到GitHub Pages、Vercel、Netlify或者自己的服务器上,体验一下从开发到部署的完整流程。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;转行者从Vue2转Vue3，需要注意的3个坑&lt;/h2&gt;
&lt;p&gt;如果你是从Vue2转过来的，一定要注意以下3个坑,不然很容易踩雷：&lt;/p&gt;
&lt;h3&gt;坑1：忘了ref和reactive的区别&lt;/h3&gt;
&lt;p&gt;Vue2里所有的响应式数据都放在data里，不用区分基本类型和对象，Vue3里ref用来处理基本类型和单值引用，reactive用来处理对象和数组，很多转行者一开始会搞混，比如用reactive处理基本类型，结果发现修改不了；比如解构赋值reactive对象之后，发现丢失了响应式——这时候就要用toRef或者toRefs来转换成独立的ref。&lt;/p&gt;
&lt;p&gt;比如解构赋值reactive对象的正确写法：&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;p&amp;gt;姓名：{{ name }}&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;年龄：{{ age }}&amp;lt;/p&amp;gt;
    &amp;lt;button @click=&amp;quot;updateUser&amp;quot;&amp;gt;修改用户信息&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup&amp;gt;
import { reactive, toRefs } from &amp;#39;vue&amp;#39;
// 定义响应式对象
const user = reactive({
  name: &amp;#39;张三&amp;#39;,
  age: 25
})
// 用toRefs转换成独立的ref，解构赋值后不会丢失响应式
const { name, age } = toRefs(user)
// 定义方法
const updateUser = () =&amp;gt; {
  name.value = &amp;#39;李四&amp;#39;
  age.value++
}
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;h3&gt;坑2：生命周期钩子的写法变了&lt;/h3&gt;
&lt;p&gt;Vue2里的生命周期钩子是直接写在选项里的，比如mounted() {}，Vue3里的生命周期钩子是组合式函数，需要从vue里导入，而且名字前面加了on，比如onMounted(() =&amp;gt; {})，很多转行者一开始会忘加on，或者导入错了,导致钩子不执行。&lt;/p&gt;
&lt;p&gt;Vue3里的beforeDestroy和destroyed钩子改了名字，beforeDestroy改成了beforeUnmount，destroyed改成了unmounted，功能和之前一样,只是名字变了。&lt;/p&gt;
&lt;h3&gt;坑3：mixin被弃用了，改用自定义Hook&lt;/h3&gt;
&lt;p&gt;Vue2里复用跨组件的逻辑用的是mixin，mixin有命名冲突、来源不明的问题，Vue3里mixin虽然还能用，但已经被官方标记为弃用了，推荐用自定义Hook（组合式函数）来代替。&lt;/p&gt;
&lt;p&gt;自定义Hook其实就是一个普通的函数，名字一般以use开头，里面可以用所有的组合式API，返回值可以是响应式变量、方法或者对象,用起来特别方便。&lt;/p&gt;
&lt;p&gt;比如一个用来管理localStorage的自定义Hook：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// useLocalStorage.js
import { ref, watch } from &amp;#39;vue&amp;#39;
export function useLocalStorage(key, defaultValue) {
  // 从localStorage读取数据，如果没有就用默认值
  const data = ref(JSON.parse(localStorage.getItem(key)) || defaultValue)
  // 监听data的变化，变化时自动保存到localStorage
  watch(data, (newValue) =&amp;gt; {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  // 返回data
  return data
}&lt;/pre&gt;
&lt;p&gt;然后在组件里就可以直接用了：&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;p&amp;gt;用户名：{{ username }}&amp;lt;/p&amp;gt;
    &amp;lt;input type=&amp;quot;text&amp;quot; v-model=&amp;quot;username&amp;quot; placeholder=&amp;quot;请输入用户名&amp;quot;&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script setup&amp;gt;
import { useLocalStorage } from &amp;#39;./useLocalStorage&amp;#39;
// 使用自定义Hook
const username = useLocalStorage(&amp;#39;username&amp;#39;, &amp;#39;请输入用户名&amp;#39;)
&amp;lt;/script&amp;gt;&lt;/pre&gt;
&lt;p&gt;是不是比mixin好用多了？&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后给新手和转行者的一些建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不要急着学所有的东西&lt;/strong&gt;：Vue3的API很多，不用一次性学完，先学日常开发用得最多的,其他的等用到的时候再学；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多查官方文档&lt;/strong&gt;：官方文档是最权威的资料，虽然有些地方比较枯燥,但比百度上的很多文章靠谱；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多做实战项目&lt;/strong&gt;：只有通过实战,才能真正掌握Vue3的用法；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加入社区交流&lt;/strong&gt;：遇到问题可以去Vue官方论坛、GitHub、Stack Overflow或者其他前端社区交流,很多大佬会帮你解决问题；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保持学习的热情&lt;/strong&gt;：前端技术更新很快，要保持学习的热情,不断更新自己的知识。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Vue3是一个非常优秀的前端框架，不管你是新手还是转行者，都值得花时间去学——只要按照上面的步骤来,不用多久就能上手了。&lt;/p&gt;</description><pubDate>Thu, 29 Oct 2026 14:01:28 +0800</pubDate></item><item><title>Vue3 computed到底该怎么用？和watch、method比有啥不同？还有什么隐藏坑？</title><link>https://codeqd.com/post/20261022179.html</link><description>&lt;h2&gt;别把method和computed搞混&lt;/h2&gt;
&lt;p&gt;先回忆下你第一次写Vue项目时,会不会经常有这种情况：模板里要显示两个数的乘积，或者把数组里的元素按价格排序？一开始可能直接写个&lt;code&gt;&amp;lt;div&amp;gt;{{ a * b }}&amp;lt;/div&amp;gt;&lt;/code&gt;，后来数据多了复杂了，比如排序再加过滤，直接塞模板里就太乱了，然后就顺手抽了个method：&lt;code&gt;getFilteredAndSortedGoods()&lt;/code&gt;，模板里直接调，但后来发现，每次其他和这个无关的变量更新时，这个方法都会重新跑一遍，页面会不会有点卡？对，这就是computed出场的时机了。&lt;/p&gt;
&lt;p&gt;举个实际的电商商品列表例子：假设我们有个商品数组&lt;code&gt;goods&lt;/code&gt;，还有两个输入框控制价格范围&lt;code&gt;minPrice&lt;/code&gt;和&lt;code&gt;maxPrice&lt;/code&gt;，另外还有个无关的、记录用户搜索框光标停留时间的变量&lt;code&gt;cursorStayTime&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果用method：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// script setup
import { ref } from &amp;#39;vue&amp;#39;
const goods = ref([
  { name: &amp;#39;机械键盘&amp;#39;, price: 399 },
  { name: &amp;#39;无线鼠标&amp;#39;, price: 129 },
  { name: &amp;#39;显示器支架&amp;#39;, price: 89 },
  { name: &amp;#39;降噪耳机&amp;#39;, price: 799 }
])
const minPrice = ref(0)
const maxPrice = ref(1000)
const cursorStayTime = ref(0)
const getFilteredGoods = () =&amp;gt; {
  console.log(&amp;#39;getFilteredGoods执行了！&amp;#39;)
  return goods.value.filter(item =&amp;gt; 
    item.price &amp;gt;= minPrice.value &amp;amp;&amp;amp; item.price &amp;lt;= maxPrice.value
  )
}
// 模拟每秒更新光标停留时间
setInterval(() =&amp;gt; cursorStayTime.value++, 1000)&lt;/pre&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;!-- template --&amp;gt;
&amp;lt;div&amp;gt;
  过滤后的商品：
  &amp;lt;ul&amp;gt;
    &amp;lt;li v-for=&amp;quot;item in getFilteredGoods()&amp;quot; :key=&amp;quot;item.name&amp;quot;&amp;gt;{{ item.name }} - {{ item.price }}&amp;lt;/li&amp;gt;
  &amp;lt;/ul&amp;gt;
  光标停留时间：{{ cursorStayTime }}
&amp;lt;/div&amp;gt;&lt;/pre&gt;
&lt;p&gt;打开控制台你会发现,不管你动没动价格输入框，每隔1秒，&lt;code&gt;getFilteredGoods执行了！&lt;/code&gt;都会打印一次，这时候要是商品数组有1000+条，或者过滤逻辑特别复杂（比如加了正则、关联其他接口返回的属性），那每秒重新计算一次，不卡才怪。&lt;/p&gt;
&lt;p&gt;那换成computed呢？我们修改下script部分：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// script setup
import { ref, computed } from &amp;#39;vue&amp;#39;
// 其他变量定义不变
const filteredGoods = computed(() =&amp;gt; {
  console.log(&amp;#39;filteredGoods重新计算了！&amp;#39;)
  return goods.value.filter(item =&amp;gt; 
    item.price &amp;gt;= minPrice.value &amp;amp;&amp;amp; item.price &amp;lt;= maxPrice.value
  )
})&lt;/pre&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;!-- template里把getFilteredGoods()改成filteredGoods --&amp;gt;
&amp;lt;li v-for=&amp;quot;item in filteredGoods&amp;quot; :key=&amp;quot;item.name&amp;quot;&amp;gt;...&amp;lt;/li&amp;gt;&lt;/pre&gt;
&lt;p&gt;再看控制台：只有当&lt;code&gt;goods&lt;/code&gt;、&lt;code&gt;minPrice&lt;/code&gt;或者&lt;code&gt;maxPrice&lt;/code&gt;这三个和过滤逻辑有关的依赖变量更新时，才会打印那句话，光标停留时间变了完全不影响，而且更重要的是，computed是&lt;strong&gt;缓存结果&lt;/strong&gt;的，第一次计算后，依赖不变的话，不管你在模板里调用多少次（比如一个地方显示数量，一个地方显示列表，一个地方显示总价），都只会取缓存值，不会重新跑过滤逻辑，method就不行了，模板里多写几次调用，就多执行几次，哪怕依赖没变化。&lt;/p&gt;
&lt;h2&gt;接下来是和watch的对比：什么时候用计算，什么时候用监听&lt;/h2&gt;
&lt;p&gt;这个对比也是新手常问的,先理清楚两个的核心定位：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;computed的核心是&lt;strong&gt;“计算出一个新的响应式数据”&lt;/strong&gt;，它的输入是依赖的响应式数据，输出是经过处理的新值，而且输出必须有地方用到（不管是模板、其他computed还是watch里），不然Vue内部可能会做垃圾回收相关的优化，导致看起来“不生效”（不过3.4+好像调整了这个逻辑，但还是建议有明确输出用途再用）。&lt;/li&gt;
&lt;li&gt;watch的核心是&lt;strong&gt;“监听某个响应式数据的变化，然后执行一段副作用逻辑”&lt;/strong&gt;，副作用逻辑就是“没有明确输出值的操作”，比如调用接口、修改DOM、修改localStorage、触发弹窗、甚至修改其他响应式数据（虽然可以改，但如果能用computed生成新值替代修改原数据的需求，还是优先用computed，更符合单向数据流的理念）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还是用刚才的电商例子延伸一下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;刚才的过滤后商品&lt;code&gt;filteredGoods&lt;/code&gt;，显然是“生成新数据”的需求，所以必须用computed。&lt;/li&gt;
&lt;li&gt;现在加个需求：当用户修改了价格范围后，自动把当前的价格范围存到localStorage里，下次打开页面自动填充，这就是“副作用逻辑”，需要存localStorage，没有生成新的、用于展示或其他计算的核心数据，所以用watch：&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;
// script setup
// 先从localStorage读取初始值
const savedMinPrice = localStorage.getItem(&amp;#39;minPrice&amp;#39;)
const savedMaxPrice = localStorage.getItem(&amp;#39;maxPrice&amp;#39;)
const minPrice = ref(savedMinPrice ? Number(savedMinPrice) : 0)
const maxPrice = ref(savedMaxPrice ? Number(savedMaxPrice) : 1000)&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;// 监听minPrice和maxPrice的变化
watch([minPrice, maxPrice], ([newMin, newMax]) =&amp;gt; {
localStorage.setItem(&#039;minPrice&#039;, newMin)
localStorage.setItem(&#039;maxPrice&#039;, newMax)
})&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;这里用了watch的数组监听模式，可以同时监听多个响应式数据的变化，只要其中一个变了，回调就会执行。
3. 再延伸一个稍微复杂的点：有时候你可能会觉得，“这个需求好像两边都能用？”比如刚才的过滤，如果硬用watch写，是不是也行？我们试试：
```javascript
// 别这么写！除非万不得已
const filteredGoods = ref([]) // 先手动声明一个空的响应式数组
watch([goods, minPrice, maxPrice], () =&amp;gt; {
  filteredGoods.value = goods.value.filter(...)
}, { immediate: true }) // 加上immediate: true，组件刚挂载的时候就执行一次，不然初始值是空的&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;乍一看好像也能实现？但对比一下computed的写法，这个手动watch的方式有几个明显的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码量多了：要手动声明初始值、手动监听、手动赋值、还要加&lt;code&gt;immediate: true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;可维护性差：如果过滤逻辑变了，还要改watch的回调，而computed把逻辑都封装在一个地方，更清晰。&lt;/li&gt;
&lt;li&gt;性能不一定更好：虽然表面上也是依赖变化才执行，但computed的缓存优化是Vue内部专门优化过的，手动赋值有时候可能会多触发不必要的更新（比如虽然你给filteredGoods.value赋了新数组，但新数组的内容和上次一模一样，computed会直接返回缓存，不会触发视图更新，手动ref赋值的话，不管内容一不一样，只要引用变了，视图就会重新渲染）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那有没有“万不得已”必须用硬写watch+ref的情况？有，但非常少，比如计算逻辑里有异步操作，computed的getter是&lt;strong&gt;必须同步返回结果&lt;/strong&gt;的，不能用async/await，比如如果过滤逻辑需要先调用一个接口拿到用户的会员等级，然后根据会员等级给商品打折扣再过滤价格，这时候就不能用computed了，必须用watch或者后面要讲的computed的setter配合其他异步方法？不对，setter也是同步的，所以异步的话还是老老实实watch。&lt;/p&gt;
&lt;h2&gt;进阶：computed的getter和setter，别只会写箭头函数&lt;/h2&gt;
&lt;p&gt;刚才的例子里,我们都是给computed传了一个箭头函数，这个箭头函数其实就是computed的&lt;strong&gt;getter&lt;/strong&gt;，负责读取依赖并返回计算结果，但其实computed还可以传一个对象，里面有&lt;code&gt;get&lt;/code&gt;和&lt;code&gt;set&lt;/code&gt;两个方法，这种叫&lt;strong&gt;可写的computed&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可写的computed什么时候用？当你希望这个计算出来的新值，也能像普通ref一样被赋值，而且赋值的时候能触发一些逻辑，比如反向修改它的依赖数据。&lt;/p&gt;
&lt;p&gt;举个常见的表单例子：比如用户注册的时候有“邮箱”和“确认邮箱”两个输入框，我们需要显示一个“合并后的邮箱状态”——如果两个邮箱一致，显示“邮箱确认成功”，不一致显示“请检查两次输入的邮箱”；如果我们直接修改这个“合并后的邮箱状态”对应的文本框（假设是个展示用但偶尔需要一键修改的场景），能同时把“邮箱”和“确认邮箱”都改成这个值。&lt;/p&gt;
&lt;p&gt;用普通computed（只有getter）的话，这个“合并状态”是只读的，不能修改，用可写computed的话就可以：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// script setup
import { ref, computed } from &amp;#39;vue&amp;#39;
const email = ref(&amp;#39;&amp;#39;)
const confirmEmail = ref(&amp;#39;&amp;#39;)
const emailStatus = computed({
  get() {
    // getter：生成状态值
    if (!email.value || !confirmEmail.value) return &amp;#39;请输入邮箱&amp;#39;
    return email.value === confirmEmail.value ? &amp;#39;邮箱确认成功&amp;#39; : &amp;#39;请检查两次输入的邮箱&amp;#39;
  },
  set(newValue) {
    // setter：反向修改依赖
    // 假设newValue就是我们要设的邮箱
    email.value = newValue
    confirmEmail.value = newValue
  }
})
// 模拟一键修改场景
const fillDefaultEmail = () =&amp;gt; {
  emailStatus.value = &amp;#39;test@example.com&amp;#39;
}&lt;/pre&gt;
&lt;pre class=&quot;brush:html;toolbar:false&quot;&gt;&amp;lt;!-- template --&amp;gt;
&amp;lt;div&amp;gt;
  邮箱：&amp;lt;input v-model=&amp;quot;email&amp;quot; type=&amp;quot;email&amp;quot;&amp;gt;
  确认邮箱：&amp;lt;input v-model=&amp;quot;confirmEmail&amp;quot; type=&amp;quot;email&amp;quot;&amp;gt;
  合并状态（可一键修改为默认邮箱）：&amp;lt;input v-model=&amp;quot;emailStatus&amp;quot;&amp;gt;
  &amp;lt;button @click=&amp;quot;fillDefaultEmail&amp;quot;&amp;gt;一键填入默认邮箱&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;&lt;/pre&gt;
&lt;p&gt;这个例子里,点击“一键填入”按钮，或者直接在“合并状态”输入框里写内容，都会触发setter，同时更新email和confirmEmail；而修改email或confirmEmail，又会触发getter，重新生成合并状态的值。&lt;/p&gt;
&lt;h2&gt;避坑指南：别踩这几个Vue3 computed常见的坑&lt;/h2&gt;
&lt;p&gt;前面讲了用法和对比,现在说几个新手甚至有一定Vue2经验的人转Vue3时容易踩的坑：&lt;/p&gt;
&lt;h3&gt;坑1：在getter里修改依赖数据&lt;/h3&gt;
&lt;p&gt;Vue2的时候如果在computed里修改依赖数据,会报错“computed property was assigned to but it has no setter”或者产生死循环？Vue3虽然有时候不会直接报错，但绝对是违反设计原则的，而且大概率会导致死循环。&lt;/p&gt;
&lt;p&gt;比如刚才的电商例子,要是在filteredGoods的getter里不小心加了一句&lt;code&gt;minPrice.value = 0&lt;/code&gt;（比如手滑多写的），那会发生什么？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一次调用getter,读取minPrice，发现minPrice.value被改成0了。&lt;/li&gt;
&lt;li&gt;minPrice变化,触发getter重新计算。&lt;/li&gt;
&lt;li&gt;第二次调用getter,又读取minPrice，又改成0了。&lt;/li&gt;
&lt;li&gt;无限循环下去,控制台会一直打印执行日志，页面直接卡死。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以&lt;strong&gt;千万记住：computed的getter是纯函数，纯函数的意思是“输入相同，输出一定相同，而且没有任何副作用”&lt;/strong&gt;，副作用包括修改依赖数据、调用接口、修改DOM、修改localStorage等等，这些都不能在getter里做。&lt;/p&gt;
&lt;h3&gt;坑2：依赖非响应式数据，导致computed不更新&lt;/h3&gt;
&lt;p&gt;刚才的例子里,computed的依赖都是ref或者reactive包裹的响应式数据，所以依赖一变化，computed就会重新计算，但如果依赖的是普通的JavaScript变量（比如没被ref/reactive包裹的数组、对象、数字、字符串），那不管你怎么改这个普通变量，computed都不会重新计算。&lt;/p&gt;
&lt;p&gt;举个错误例子：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// script setup
import { computed } from &amp;#39;vue&amp;#39;
// 别这样！count是普通变量，不是响应式的
let count = 0
const doubleCount = computed(() =&amp;gt; count * 2)
const addCount = () =&amp;gt; {
  count++
  console.log(&amp;#39;count变成了&amp;#39;, count)
  console.log(&amp;#39;doubleCount还是&amp;#39;, doubleCount.value)
}&lt;/pre&gt;
&lt;p&gt;点击addCount按钮,count会变成1、2、3……但doubleCount.value永远是0，因为count不是响应式的，Vue的响应式系统追踪不到它的变化，所以不会触发computed的重新计算。&lt;/p&gt;
&lt;h3&gt;坑3：用索引或直接修改数组长度访问reactive数组的元素，导致computed不更新&lt;/h3&gt;
&lt;p&gt;这个坑主要是针对reactive包裹的数组的（ref包裹的数组其实没问题，因为ref包裹的基本类型或引用类型，都是通过.value访问修改，Vue3的Proxy代理能捕获到）。&lt;/p&gt;
&lt;p&gt;Vue3用的是Proxy代理实现响应式,比Vue2的Object.defineProperty强很多，但reactive数组还是有两个小地方需要注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;直接用索引修改数组元素：比如&lt;code&gt;reactiveArr[0] = &#039;新值&#039;&lt;/code&gt;，这个Vue3的Proxy其实大部分现代浏览器都能捕获到，但为了兼容性（或者有时候你会遇到不生效的情况），还是建议用数组的方法，比如&lt;code&gt;reactiveArr.splice(0, 1, &#039;新值&#039;)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;直接修改数组长度：比如&lt;code&gt;reactiveArr.length = 0&lt;/code&gt;，这个Proxy有时候捕获不到，建议用&lt;code&gt;reactiveArr.splice(0)&lt;/code&gt;来清空数组。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果犯了这两个错误,而且你的computed依赖的是reactive数组的某个具体元素或者整个数组的长度相关的属性，那computed可能不会重新计算。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre class=&quot;brush:javascript;toolbar:false&quot;&gt;// script setup
import { reactive, computed } from &amp;#39;vue&amp;#39;
const reactiveGoods = reactive([
  { name: &amp;#39;机械键盘&amp;#39;, price: 399 },
  { name: &amp;#39;无线鼠标&amp;#39;, price: 129 }
])
const goodsCount = computed(() =&amp;gt; reactiveGoods.length)
const badClearGoods = () =&amp;gt; {
  // 别这样！Proxy可能捕获不到
  reactiveGoods.length = 0
}
const goodClearGoods = () =&amp;gt; {
  // 这样才对！
  reactiveGoods.splice(0)
}&lt;/pre&gt;
&lt;p&gt;点击badClearGoods按钮,goodsCount可能不会变成0；点击goodClearGoods按钮，肯定会变成0。&lt;/p&gt;
&lt;h3&gt;坑4：在模板里用computed的时候加了括号&lt;/h3&gt;
&lt;p&gt;这个坑是从method转过来的人最容易犯的！刚才讲了，computed是一个响应式数据，不是一个函数，所以在模板里直接写&lt;code&gt;{{ filteredGoods }}&lt;/code&gt;或者&lt;code&gt;v-for=&quot;item in filteredGoods&quot;&lt;/code&gt;就行，别加。&lt;/p&gt;
&lt;p&gt;如果加了括号会怎么样？Vue3会把computed当成一个普通的getter函数来调用，虽然第一次结果是对的，但&lt;strong&gt;缓存就失效了&lt;/strong&gt;，和method一样，每次无关变量更新都会重新执行，完全失去了用computed的意义。&lt;/p&gt;
&lt;h2&gt;Vue3 computed的使用原则&lt;/h2&gt;
&lt;p&gt;我们把前面讲的内容总结成几个简单的使用原则,方便大家记住：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;能用computed就不用method&lt;/strong&gt;：只要是生成一个用于展示或其他计算的、依赖响应式数据的新值，优先用computed，利用它的缓存优化性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能用computed就不用硬写watch+ref&lt;/strong&gt;：除非计算逻辑里有异步操作，不然别手动写watch和ref来模拟computed，computed代码更简洁、可维护性更好、性能更优。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;getter必须是纯函数&lt;/strong&gt;：不能有副作用，不能修改依赖数据，必须同步返回结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要反向修改依赖数据时用可写computed&lt;/strong&gt;：给computed传一个带get和set的对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别踩坑&lt;/strong&gt;：依赖必须是响应式数据、reactive数组别直接用索引或改长度、模板里别加括号、getter别写副作用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;希望这篇文章能帮你彻底搞懂Vue3的computed,以后写代码的时候再也不用纠结用哪个了。&lt;/p&gt;</description><pubDate>Thu, 29 Oct 2026 08:01:24 +0800</pubDate></item><item><title>Vue3面试必问？Composition API到底比Options API好在哪，怎么用不出错？</title><link>https://codeqd.com/post/20261022178.html</link><description>&lt;h2&gt;先别急着背优势，得搞懂面试官为啥揪着这个问&lt;/h2&gt;
&lt;p&gt;刚上手Vue3的人，第一个逃不开的选择大概率是用老的Options还是新的Composition？很多面经会列一堆优势比如更好复用、更灵活，但如果只会干巴巴念这些，面试官可能追问两句就露馅——比如问你项目里遇到过什么痛点，Options解决不了只能用Composition的？或者有没有Composition用多了反而搞出bug的场景？
面试官真正想挖的，其实是你对两种API底层逻辑的理解，有没有实际踩过坑，能不能根据项目规模和需求场景选对工具，而不是跟风全改Composition，毕竟现在很多大厂项目还是Vue2升级的混合模式，完全纯Options或者纯Composition的都有，但&lt;strong&gt;会分析场景、合理使用才是关键能力&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第一个对比核心：代码组织逻辑，不是代码行数！&lt;/h2&gt;
&lt;p&gt;很多人说“Composition API代码更短”，这其实是个误区——简单的Todo List，两种API写出来的代码量差不了多少，甚至Options更直观，真正的差异，是&lt;strong&gt;代码是按“功能模块”凑在一起，还是按“选项类型”分散摆放&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举个真实的例子，我之前做过一个学生课表的小组件，Options API版本的代码是这样的：
（代码可以简单写逻辑要点,但别全贴太繁琐的示例）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;data()&lt;/code&gt;里定义了课表数组、选中日期、课程弹窗显示状态、搜索关键词&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;methods&lt;/code&gt;里写了切换日期、请求课表、打开/关闭弹窗、搜索课程&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;computed&lt;/code&gt;里写了当天已上课/待上课筛选、课程时间格式化&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;watch&lt;/code&gt;里监听选中日期变化重新请求&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;mounted&lt;/code&gt;里初始化选中日期、请求初始课表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看起来每个选项都很清晰？但课表功能还要加提醒——比如上课前10分钟弹出通知框，那这个提醒相关的代码要拆到哪里？定时器放&lt;code&gt;mounted&lt;/code&gt;里开启、&lt;code&gt;beforeUnmount&lt;/code&gt;里销毁？提醒状态放&lt;code&gt;data&lt;/code&gt;里？提醒触发逻辑放&lt;code&gt;watch&lt;/code&gt;监听选中日期的待上课筛选结果里？
这时候，一个完整的“课表提醒功能”就被拆得七零八落，改bug或者新增功能时，得在整个文件里跳来跳去找相关的data、methods、computed、watch，组件超过200行就特别崩溃——我之前改这个课表组件的提醒音效时，先找音效播放的methods，再找待上课课程筛选的computed，最后才发现watch里漏加了音效触发的条件,折腾了20多分钟。&lt;/p&gt;
&lt;p&gt;那用Composition API重构后呢？
可以把&lt;strong&gt;所有和课表查询相关的逻辑&lt;/strong&gt;放在一个自定义Hook（比如&lt;code&gt;useCourseTableQuery&lt;/code&gt;）里，把&lt;strong&gt;所有和提醒相关的逻辑&lt;/strong&gt;放在另一个自定义Hook（比如&lt;code&gt;useCourseReminder&lt;/code&gt;）里，把&lt;strong&gt;所有和弹窗交互相关的逻辑&lt;/strong&gt;放在&lt;code&gt;useModal&lt;/code&gt;里，这样每个功能模块的代码是&lt;strong&gt;自包含、可复用、逻辑集中&lt;/strong&gt;的，哪怕后来课表组件要新增搜索筛选条件或者换提醒方式，只需要改对应的自定义Hook就行,完全不用碰其他模块的代码。&lt;/p&gt;
&lt;p&gt;重构后的课表组件主文件只剩什么？只剩import这些Hook，然后解构出需要的响应式变量、方法，往模板里一塞，整个主文件可能就50行不到,一眼就能看完整个组件的结构。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第二个对比核心：复用能力，自定义Hook是真的香&lt;/h2&gt;
&lt;p&gt;Options API也有复用逻辑的方法，比如Mixin和高阶组件（HOC）,但这俩都有致命的缺陷。&lt;/p&gt;
&lt;p&gt;先说说Mixin：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命名冲突问题太严重&lt;/strong&gt;：比如你用了一个别人写的&lt;code&gt;useLocalStorageMixin&lt;/code&gt;Mixin，里面定义了&lt;code&gt;data&lt;/code&gt;里的&lt;code&gt;storageKey&lt;/code&gt;，然后你自己的组件里也需要&lt;code&gt;storageKey&lt;/code&gt;变量存课程缓存的key，这时候谁覆盖谁？如果Mixin里还定义了&lt;code&gt;getStorageData&lt;/code&gt;方法，你组件里也有同名方法，那你的方法会覆盖Mixin的，还是反过来？虽然Vue2有规则，但规则多了记不住,很容易出bug。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码来源不透明&lt;/strong&gt;：当组件里用了3个以上的Mixin时，你根本不知道某个响应式变量或者方法是从哪个Mixin里来的，得一个一个Mixin去翻代码,太浪费时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑耦合问题&lt;/strong&gt;：Mixin里的逻辑有时候是绑定在一起的，比如你只想用&lt;code&gt;useLocalStorageMixin&lt;/code&gt;里的&lt;code&gt;getStorageData&lt;/code&gt;方法，但不得不把它整个引入进来，包括它定义的&lt;code&gt;storageKey&lt;/code&gt;、&lt;code&gt;saveStorageData&lt;/code&gt;这些你根本用不上的变量和方法,增加了组件的冗余。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;再说说高阶组件（HOC）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模板层级嵌套过深&lt;/strong&gt;：HOC通常是用组件包裹组件，比如你要给组件加权限控制，又要加加载状态，又要加数据缓存，那最终渲染的DOM里可能会有3-4层无意义的div包裹（虽然Vue3有Teleport和Fragments，但之前的Vue2或者很多旧的HOC库还是会有这个问题），不仅影响性能，还会让浏览器的DevTools变得很乱,调试起来不方便。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Props和事件透传麻烦&lt;/strong&gt;：HOC如果要把父组件的props和事件透传给子组件，得手动写&lt;code&gt;v-bind=&quot;$attrs&quot;&lt;/code&gt;和&lt;code&gt;v-on=&quot;$listeners&quot;&lt;/code&gt;，还要处理&lt;code&gt;inheritAttrs&lt;/code&gt;的问题,稍微漏了一个就会出bug。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型推断差&lt;/strong&gt;：TypeScript和HOC的兼容性一直不好，很多时候要手动写类型声明，代码量增加了不少,还容易写错。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那Composition API的自定义Hook呢？完美解决了这些问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命名冲突可以通过解构重命名解决&lt;/strong&gt;：比如&lt;code&gt;import { useLocalStorage as useCourseStorage } from &#039;./hooks&#039;&lt;/code&gt;，或者解构的时候&lt;code&gt;const { storageKey: courseStorageKey, getStorageData } = useCourseStorage()&lt;/code&gt;,完全不用担心和其他Hook或者组件本身的变量重名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码来源非常透明&lt;/strong&gt;：每个响应式变量和方法都是从对应的自定义Hook里解构出来的，鼠标悬停在变量名上（如果用了TypeScript或者Vetur/Volar），还能直接看到它的类型和定义位置,调试和维护都特别方便。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑可以按需引入&lt;/strong&gt;：你可以把自定义Hook写得很细，比如&lt;code&gt;useLocalStorage&lt;/code&gt;只负责存和取单个key的数据，&lt;code&gt;useDebouncedWatch&lt;/code&gt;只负责防抖监听某个变量，需要什么功能就引入什么功能,不会有任何冗余。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型推断完美&lt;/strong&gt;：Vue3的Composition API是用TypeScript优先设计的，自定义Hook的类型推断非常流畅，几乎不用手动写类型声明,或者只需要写一点点泛型就够了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有模板层级嵌套&lt;/strong&gt;：自定义Hook只是纯JavaScript/TypeScript函数，完全不会影响DOM结构,DevTools也能清晰地看到每个响应式变量的变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个复用的小例子，我把之前课表组件里的&lt;code&gt;useLocalStorage&lt;/code&gt;Hook抽出来，后来在用户偏好设置组件里也用到了——存用户的主题色、字体大小、是否开启提醒，完全不用改任何代码，直接引入就行,省了很多时间。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;第三个对比核心：TypeScript支持，这才是Composition API的“杀手锏”&lt;/h2&gt;
&lt;p&gt;现在很多大厂的前端项目都要求用TypeScript，Options API虽然也能写TypeScript,但体验真的很差。&lt;/p&gt;
&lt;p&gt;先说说Options API写TypeScript的痛点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;data()&lt;/code&gt;返回的对象类型推断不出来&lt;/strong&gt;：得手动给&lt;code&gt;data()&lt;/code&gt;加一个泛型或者接口，&lt;pre class=&quot;brush:typescript;toolbar:false&quot;&gt;
interface CourseTableData {
courseList: Course[];
selectedDate: string;
isModalOpen: boolean;
}&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;export default defineComponent({
data(): CourseTableData {
return {
courseList: [],
selectedDate: new Date().toISOString().split(&#039;T&#039;)[0],
isModalOpen: false
};
}
});&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- **`methods`、`computed`、`watch`里的`this`类型不稳定**：有时候Vetur/Volar会推断不出来`this`的类型，得手动用`as this`强转，或者给组件加一个`ComponentPublicInstance`的泛型，非常麻烦。
- **Mixin和TypeScript完全不兼容**：如果组件里用了Mixin，那Mixin里的变量和方法的类型根本推断不出来，只能全写`any`，失去了TypeScript的意义。
那Composition API写TypeScript呢？体验简直是“飞一般的感觉”：
- **`ref`、`reactive`的类型推断自动完成**：const selectedDate = ref(new Date().toISOString().split(&#039;T&#039;)[0])`，Volar会自动推断出`selectedDate`的类型是`Ref&amp;lt;string&amp;gt;`；`const courseList = reactive&amp;lt;Course[]&amp;gt;([])`，只需要加一个简单的泛型就行。
- **没有`this`的困扰**：Composition API里不需要用`this`，所有的变量和方法都是直接定义的，类型推断非常流畅，几乎不会出问题。
- **自定义Hook的类型推断完美**：只要你的自定义Hook里用了正确的泛型，解构出来的变量和方法的类型就会自动传过来，
```typescript
import { ref, watch, onMounted, onBeforeUnmount } from &#039;vue&#039;;
export function useLocalStorage&amp;lt;T&amp;gt;(key: string, defaultValue: T) {
  const data = ref&amp;lt;T&amp;gt;(defaultValue);
  const loadData = () =&amp;gt; {
    const stored = localStorage.getItem(key);
    if (stored) {
      try {
        data.value = JSON.parse(stored) as T;
      } catch {
        data.value = defaultValue;
      }
    }
  };
  const saveData = () =&amp;gt; {
    localStorage.setItem(key, JSON.stringify(data.value));
  };
  watch(data, saveData, { deep: true });
  onMounted(loadData);
  return { data, loadData, saveData };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在组件里用的时候：&lt;/p&gt;
&lt;pre class=&quot;brush:typescript;toolbar:false&quot;&gt;const { data: themeColor } = useLocalStorage&amp;lt;&amp;#39;light&amp;#39; | &amp;#39;dark&amp;#39;&amp;gt;(&amp;#39;theme-color&amp;#39;, &amp;#39;light&amp;#39;);&lt;/pre&gt;
&lt;p&gt;Volar会自动推断出&lt;code&gt;themeColor&lt;/code&gt;的类型是&lt;code&gt;Ref&amp;lt;&#039;light&#039; | &#039;dark&#039;&amp;gt;&lt;/code&gt;,完全不用手动写。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;别吹Composition API上天，它也有适用场景和“坑”&lt;/h2&gt;
&lt;p&gt;很多人觉得Vue3就应该全用Composition API，其实不是的,两种API各有各的适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Options API的适用场景&lt;/strong&gt;：&lt;ol&gt;
&lt;li&gt;简单的小组件：比如按钮、输入框、标签页，逻辑不复杂，代码量少，用Options API更直观,上手更快。&lt;/li&gt;
&lt;li&gt;Vue2升级的混合模式项目：如果项目是从Vue2升级过来的，有很多旧的Options API组件，这时候没必要全改成Composition API，保持混合模式就行，新组件可以用Composition API,旧组件慢慢迭代。&lt;/li&gt;
&lt;li&gt;新手学习Vue：Options API的选项分类很清晰，新手可以快速理解Vue的核心概念（data、methods、computed、watch、生命周期钩子），等理解透了再学Composition API会更轻松。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composition API的适用场景&lt;/strong&gt;：&lt;ol&gt;
&lt;li&gt;复杂的业务组件：比如课表、图表、表单提交，逻辑模块多，需要复用很多功能，用Composition API可以更好地组织代码。&lt;/li&gt;
&lt;li&gt;需要大量复用逻辑的项目：比如电商平台的商品筛选、购物车、订单管理，这些功能在很多页面都能用到,抽成自定义Hook可以省很多时间。&lt;/li&gt;
&lt;li&gt;用TypeScript的项目：Composition API和TypeScript的兼容性最好,体验最佳。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来要说Composition API的“坑”了，这些坑面试时也经常被问到,踩过的话答出来会很加分：&lt;/p&gt;
&lt;h3&gt;坑一：ref和reactive的区别搞不清，乱用导致响应式失效&lt;/h3&gt;
&lt;p&gt;很多新手刚学Composition API时,会把ref和reactive搞混，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用reactive定义基本类型：&lt;code&gt;const count = reactive(0)&lt;/code&gt;，这根本不会有响应式，因为reactive只能定义对象或数组,基本类型要用ref。&lt;/li&gt;
&lt;li&gt;用ref定义对象后，直接替换整个对象：&lt;code&gt;const user = ref({ name: &#039;张三&#039;, age: 18 }); user = { name: &#039;李四&#039;, age: 20 }&lt;/code&gt;，这也不会有响应式，因为你替换的是ref本身，而不是ref.value，应该写成&lt;code&gt;user.value = { name: &#039;李四&#039;, age: 20 }&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;解构reactive对象后失去响应式：&lt;code&gt;const state = reactive({ count: 0, name: &#039;张三&#039; }); const { count, name } = state&lt;/code&gt;，这时候count和name变成了普通变量，不会有响应式，应该用&lt;code&gt;toRefs&lt;/code&gt;或者&lt;code&gt;toRef&lt;/code&gt;：&lt;code&gt;const { count, name } = toRefs(state)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那怎么正确区分ref和reactive呢？这里有个简单的小技巧：&lt;strong&gt;基本类型用ref，对象/数组尽量也用ref（除非你确定不会替换整个对象）&lt;/strong&gt;，为什么对象/数组尽量也用ref？因为ref定义的对象/数组，修改时只需要加&lt;code&gt;.value&lt;/code&gt;，替换整个对象时也只要加&lt;code&gt;.value&lt;/code&gt;，不容易出错；而reactive定义的对象/数组，虽然修改属性不需要加&lt;code&gt;.value&lt;/code&gt;，但替换整个对象时需要用&lt;code&gt;Object.assign&lt;/code&gt;或者直接解构再重新赋值，反而更麻烦,而且新手很容易忘记。&lt;/p&gt;
&lt;h3&gt;坑二：生命周期钩子的执行顺序搞不清，导致逻辑错误&lt;/h3&gt;
&lt;p&gt;Vue3的生命周期钩子和Vue2的差不多,只是改了名字：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue2的&lt;code&gt;beforeCreate&lt;/code&gt;和&lt;code&gt;created&lt;/code&gt;合并成了&lt;code&gt;setup()&lt;/code&gt;函数本身（setup执行时，组件实例还没创建，所以不能用&lt;code&gt;this&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Vue2的&lt;code&gt;beforeMount&lt;/code&gt; → Vue3的&lt;code&gt;onBeforeMount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Vue2的&lt;code&gt;mounted&lt;/code&gt; → Vue3的&lt;code&gt;onMounted&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Vue2的&lt;code&gt;beforeUpdate&lt;/code&gt; → Vue3的&lt;code&gt;onBeforeUpdate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Vue2的&lt;code&gt;updated&lt;/code&gt; → Vue3的&lt;code&gt;onUpdated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Vue2的&lt;code&gt;beforeDestroy&lt;/code&gt; → Vue3的&lt;code&gt;onBeforeUnmount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Vue2的&lt;code&gt;destroyed&lt;/code&gt; → Vue3的&lt;code&gt;onUnmounted&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了这些，Vue3还新增了一些生命周期钩子，比如&lt;code&gt;onActivated&lt;/code&gt;（keep-alive组件激活时）、&lt;code&gt;onDeactivated&lt;/code&gt;（keep-alive组件停用时）、&lt;code&gt;onErrorCaptured&lt;/code&gt;（捕获子孙组件的错误）。&lt;/p&gt;
&lt;p&gt;很多新手容易犯的错误是，在&lt;code&gt;setup()&lt;/code&gt;函数里直接写异步请求，&lt;/p&gt;
&lt;pre class=&quot;brush:typescript;toolbar:false&quot;&gt;setup() {
  const courseList = ref&amp;lt;Course[]&amp;gt;([]);
  // 错误写法：直接在setup里写async函数，会导致setup返回一个Promise，模板里的变量暂时无法使用
  async function getCourseList() {
    const res = await fetch(&amp;#39;/api/course-list&amp;#39;);
    courseList.value = await res.json();
  }
  getCourseList();
  return { courseList };
}&lt;/pre&gt;
&lt;p&gt;虽然这种写法在Vue3.2+里已经被优化了（模板里会有一个默认的pending状态），但更好的写法是把异步请求放在&lt;code&gt;onMounted&lt;/code&gt;里，或者用&lt;code&gt;watch&lt;/code&gt;监听某个变量触发请求,这样可以更好地控制请求的时机。&lt;/p&gt;
&lt;p&gt;还有一个坑是，在自定义Hook里用生命周期钩子时，要确保自定义Hook是在&lt;code&gt;setup()&lt;/code&gt;函数或者另一个自定义Hook里调用的，否则生命周期钩子不会生效——因为Vue3的生命周期钩子是依赖组件实例的，只有在组件实例创建的过程中调用,才会绑定到对应的组件上。&lt;/p&gt;
&lt;h3&gt;坑三：响应式数据的浅层监听和深层监听搞不清，导致性能问题或者逻辑错误&lt;/h3&gt;
&lt;p&gt;Vue3的&lt;code&gt;watch&lt;/code&gt;默认是&lt;strong&gt;浅层监听&lt;/strong&gt;的，比如你用&lt;code&gt;watch&lt;/code&gt;监听一个ref定义的对象，只有当你替换整个对象时才会触发回调，修改对象的属性是不会触发的；如果用&lt;code&gt;watch&lt;/code&gt;监听一个reactive定义的对象，默认是&lt;strong&gt;深层监听&lt;/strong&gt;的,修改对象的任何属性都会触发回调。&lt;/p&gt;
&lt;p&gt;很多新手不知道这一点，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用&lt;code&gt;watch&lt;/code&gt;监听一个ref定义的大对象，然后忘记加&lt;code&gt;{ deep: true }&lt;/code&gt;，导致修改属性时回调不触发,逻辑错误。&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;watch&lt;/code&gt;监听一个reactive定义的大对象，导致每次修改任何一个小属性都会触发回调,性能很差。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那怎么正确使用浅层监听和深层监听呢？这里有个原则：&lt;strong&gt;能不用深层监听就不用深层监听&lt;/strong&gt;，如果只需要监听对象的某个属性，可以直接监听那个属性，比如&lt;code&gt;watch(() =&amp;gt; state.user.name, (newVal) =&amp;gt; { ... })&lt;/code&gt;；如果需要监听对象的多个属性，可以用数组，比如&lt;code&gt;watch([() =&amp;gt; state.user.name, () =&amp;gt; state.user.age], ([newName, newAge]) =&amp;gt; { ... })&lt;/code&gt;；如果必须监听整个对象的所有属性变化，再考虑加&lt;code&gt;{ deep: true }&lt;/code&gt;，但这时候要注意性能问题，可以加&lt;code&gt;{ flush: &#039;post&#039; }&lt;/code&gt;让回调在DOM更新后再执行，或者加&lt;code&gt;{ immediate: true }&lt;/code&gt;让回调在初始化时就执行一次。&lt;/p&gt;
&lt;p&gt;还有一个优化性能的API是&lt;code&gt;watchEffect&lt;/code&gt;，它会自动收集依赖,不需要手动指定监听的变量，&lt;/p&gt;
&lt;pre class=&quot;brush:typescript;toolbar:false&quot;&gt;watchEffect(() =&amp;gt; {
  console.log(state.user.name, state.user.age);
});&lt;/pre&gt;
&lt;p&gt;当&lt;code&gt;state.user.name&lt;/code&gt;或者&lt;code&gt;state.user.age&lt;/code&gt;变化时，&lt;code&gt;watchEffect&lt;/code&gt;的回调会自动触发，非常方便，但&lt;code&gt;watchEffect&lt;/code&gt;的缺点是不能获取旧值，而且默认是在DOM更新前执行的，如果需要获取旧值或者控制执行时机，还是用&lt;code&gt;watch&lt;/code&gt;更好。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;面试时怎么答这个问题？别光背，要结合实际项目！&lt;/h2&gt;
&lt;p&gt;如果面试时被问到“Composition API到底比Options API好在哪，怎么用不出错？”,你可以按照这个思路回答：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先铺垫&lt;/strong&gt;：两种API没有绝对的好坏，只有适用场景的不同，不能跟风全改Composition API。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;再讲优势（结合实际项目的例子）&lt;/strong&gt;：&lt;ul&gt;
&lt;li&gt;代码组织逻辑：从按选项分类到按功能模块分类，举之前的课表组件例子,说明Mixin拆分功能的痛点和自定义Hook组织功能的好处。&lt;/li&gt;
&lt;li&gt;复用能力：对比Mixin、HOC和自定义Hook，举抽离&lt;code&gt;useLocalStorage&lt;/code&gt;Hook复用的例子。&lt;/li&gt;
&lt;li&gt;TypeScript支持：对比Options API和Composition API写TypeScript的体验，举ref、reactive、自定义Hook的类型推断例子。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;然后讲适用场景&lt;/strong&gt;：简单说明什么时候用Options,什么时候用Composition。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后讲踩过的坑和解决方法（加分项！）&lt;/strong&gt;：讲1-2个你实际踩过的坑，比如ref和reactive的区别、watch的深层监听，然后说你是怎么解决的,以及总结出的原则。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样回答，不仅有理论，还有实际项目的例子，还有踩坑经验,面试官肯定会对你印象深刻。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最后总结一下&lt;/h2&gt;
&lt;p&gt;Vue3的Composition API是一个非常棒的工具，但它不是“万能药”，也不是用来替代Options API的，而是用来补充Options API的，在实际开发中，我们应该根据项目的规模、需求场景、团队的技术栈来选择合适的API——简单的小组件用Options API，复杂的业务组件用Composition API，Vue2升级的项目用混合模式，用TypeScript的项目优先用Composition API，我们还要注意Composition API的一些“坑”，比如ref和reactive的区别、生命周期钩子的执行顺序、响应式数据的浅层监听和深层监听,避免踩坑影响开发效率和代码质量。&lt;/p&gt;</description><pubDate>Wed, 28 Oct 2026 20:01:55 +0800</pubDate></item></channel></rss>