Code前端首页关于Code前端联系我们

Vue3写代码突然弹出watch is not defined?一文帮你全流程踩坑+避坑

terry 5小时前 阅读数 71 #Vue

刚切换到Vue3的Composition API写组件,手顺敲完data、setup之后准备加个watch监听数据,控制台一刷红底白字就蹦出来“ReferenceError: watch is not defined”?别慌,这不是什么复杂到啃不动的Vue3底层bug,大概率是你 Composition API 入门路上的几个小细节没注意到——要么没正确引入这个API,要么引入方式不对版本不匹配,要么使用场景踩了Composition API的“雷区”,今天就从你打开编辑器的那一刻说起,全流程拆解这个问题的所有可能原因,再给你一套避坑指南,以后再遇到类似的“XXX is not defined”都能自己解决。

先理清楚:为什么Vue3的watch不能像Vue2那样直接用?

很多从Vue2转过来的开发者遇到这个问题的第一反应是:“之前写Vue2的时候,data、methods、watch都是直接在组件选项里写的,从来没管过引入啊,怎么Vue3就不行了?”这个疑问其实刚好戳中了Vue2和Vue3最大的架构差异之一。

Vue2用的是Options API(选项式API),这套架构把所有功能都封装在预定义好的“选项盒子”里——data放响应式数据,methods放方法,watch放监听器,computed放计算属性……当你把组件注册到Vue实例的时候,Vue内部会自动帮你把这些选项盒子里的内容挂载到this上,同时也会把对应的核心方法(watch、$emit)挂在this上,所以你不需要手动去import,直接写就能用。

但Vue3不一样,它为了解决Options API的痛点(比如逻辑分散、代码复用难、TypeScript支持不友好),重构了核心,推出了Composition API(组合式API),这套架构的核心逻辑是“按需引入,组合使用”——你想用什么功能,就从vue包里引入对应的函数,然后在setup函数(或者<script setup>语法糖里)调用这些函数,Vue内部不会再自动帮你全局挂载所有API了,换句话说,Vue3把选择权完全交给了你,你可以只引入你需要的API,这样打包出来的代码体积会更小,Tree Shaking(摇树优化)的效率会更高。

知道了这个底层逻辑,“watch is not defined”的第一个(也是最常见的一个)原因就浮出水面了:你根本没从vue包里引入watch这个函数。

完全忘记引入watch函数

这个原因真的太普遍了,尤其是刚从Vue2转过来、还没养成“写之前先引入”习惯的开发者,甚至很多用惯了<script setup>但偶尔手滑写错的老鸟也会犯。

怎么判断是这个原因?

打开你的组件文件,不管是用普通的setup函数还是<script setup>语法糖,先看文件顶部有没有import { watch } from 'vue'这行代码——如果没有,那99%就是这个问题。

怎么解决?

很简单,补上这行引入代码就行,不过要注意,如果你同时引入了多个API,比如ref、reactive、computed、watchEffect,要把它们放在同一个花括号里,用逗号分隔,

// 普通setup函数写法的引入
import { ref, reactive, computed, watch, watchEffect } from 'vue';
export default {
  setup() {
    const count = ref(0);
    watch(count, (newVal, oldVal) => {
      console.log(`count变了:${oldVal} → ${newVal}`);
    });
    return { count };
  }
}
<!-- <script setup>语法糖的引入更简单,直接加在开头就行 -->
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newVal, oldVal) => {
  console.log(`count变了:${oldVal} → ${newVal}`);
});
</script>

很多新手刚解决这个问题,又会遇到一个新的疑问:“我明明引入了watch啊,为什么还是报错?”别急,下面还有几个原因等着你排查。

引入了但写错了变量名

别笑,这个原因虽然听起来很“低级”,但在开发者赶进度、或者注意力不集中的时候真的非常容易犯——比如把watch写成了wacthwatchewatchr,甚至写成了Vue2的$watch

怎么判断是这个原因?

先看控制台的报错信息里的“未定义变量名”是不是你写错的那个——比如报错是“ReferenceError: wacth is not defined”,那肯定是拼错了;如果报错是“ReferenceError: $watch is not defined”,那就是还在用Vue2的写法,再仔细看你import花括号里的变量名,和你调用的变量名是不是完全一致(包括大小写,Vue3的API都是严格区分大小写的,比如Watch和watch是完全不同的两个变量)。

怎么解决?

把变量名改成正确的watch就行,这里给你一个小技巧:很多现代编辑器(比如VS Code、WebStorm)都有自动补全功能,当你输入import { wa的时候,编辑器会自动弹出watchwatchEffect这些选项,你直接按Tab或者Enter键选择就行,这样就不会拼错了。

引入方式不对——混用了Options API和Composition API

有些开发者可能刚接触Vue3,觉得Options API和Composition API可以随便混着用——比如在Options API的methods里直接调用watch,或者在setup函数外面写watch,这两种情况都会报错“watch is not defined”。

第一种混用错误:在setup函数外面写watch

先看一个错误示例:

import { ref, watch } from 'vue';
export default {
  data() {
    return {
      count: 0
    }
  },
  // 错误!setup函数外面的watch是Options API的watch,
  // 不是Composition API引入的那个函数!
  // 这里如果用Options API的写法,不需要import,但要注意是对象/函数形式
  // 但如果是这里写成了函数调用watch(...),就会报错,因为这个作用域里没有引入的watch
  watch(count, (newVal) => { // 这里的count也会报错,因为data里的count要通过this.count访问,但Options API的watch不能这样写
    console.log(newVal);
  }),
  setup() {
    // ...
  }
}

很多开发者会在这里搞混——Options API的watch是一个选项,不是函数,它的写法是:

export default {
  data() {
    return { count: 0 }
  },
  // Options API的正确写法,不需要import
  watch: {
    count(newVal, oldVal) {
      console.log(`count变了:${oldVal} → ${newVal}`);
    },
    // 或者监听计算属性、嵌套属性
    'user.name'(newVal) {
      console.log(`用户名变了:${newVal}`);
    }
  }
}

而Composition API的watch是一个函数,必须在setup函数(或者<script setup>语法糖里)调用,因为只有在setup函数的作用域里,你引入的watch才是有效的。

第二种混用错误:在Options API的methods/mounted等钩子函数里调用Composition API的watch

再看一个错误示例:

import { ref, watch } from 'vue';
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    // 错误!methods的作用域里没有setup函数引入的watch
    // setup函数里的变量(包括引入的API)默认不会挂载到this上,除非你return出去
    addCount() {
      this.count++;
      watch(this.count, (newVal) => {
        console.log(newVal);
      });
    }
  },
  setup() {
    // 这里引入的watch只在setup作用域有效
    return {};
  }
}

有些开发者可能会说:“那我把watch return出去,然后在methods里用this.watch不就行了?”理论上可以,但非常不推荐——因为Composition API和Options API混用的时候,逻辑会变得非常混乱,而且Tree Shaking也会失效,还不如全用Options API或者全用Composition API。

怎么解决混用问题?

最好的解决办法是统一一套API风格——如果你习惯了Vue2的写法,想快速上手Vue3,那就全用Options API(Vue3完全兼容Options API);如果你想体验Composition API的优势,比如更好的逻辑复用、更好的TypeScript支持,那就全用Composition API,尤其是推荐用<script setup>语法糖,因为它更简洁,不需要写export default和return。

如果确实需要混用(比如维护老项目,想慢慢重构),那就要注意:

  1. Options API的选项(比如watch、computed)必须写在export default的对象里,不能写成函数调用;
  2. Composition API的函数(比如watch、ref、reactive)必须在setup函数里调用,引入的API只在setup作用域有效;
  3. 如果要在Options API里访问setup函数里的响应式数据,可以把数据return出去,然后通过this.$setup.xxx访问(或者直接this.xxx,但这取决于Vue的版本和配置,推荐用this.$setup.xxx更稳妥);
  4. 如果要在setup函数里访问Options API的this,可以用getCurrentInstance()函数,但要注意,getCurrentInstance()只能在setup函数或者生命周期钩子函数里调用,不能在异步函数里直接调用(除非在异步函数调用前保存instance)。

这里顺便提一下getCurrentInstance()的用法,避免你在混用时踩另一个坑:

import { ref, watch, getCurrentInstance } from 'vue';
export default {
  data() {
    return { optionsCount: 0 }
  },
  setup() {
    const compCount = ref(0);
    // 获取当前组件实例,注意只在setup/生命周期钩子有效
    const instance = getCurrentInstance();
    // 可以在setup里通过instance.proxy访问Options API的this
    // 比如instance.proxy.optionsCount、instance.proxy.$emit等
    watch(() => instance.proxy.optionsCount, (newVal) => {
      console.log(`Options API的count变了:${newVal}`);
    });
    // 可以把compCount return出去,在Options API里通过this.compCount访问
    return { compCount };
  },
  mounted() {
    console.log(this.compCount); // 可以访问到setup里return的compCount
  }
}

Vue版本太低,不支持某些watch的特性?不,是根本不支持按需引入的Composition API?

有些开发者可能用的是Vue3的早期版本,比如Vue 3.0.0-beta.x甚至更早的版本,会不会出现“watch is not defined”的情况?其实不会——因为Composition API在Vue 3.0.0正式版里就已经是核心功能了,watch作为Composition API的核心监听器,从一开始就支持按需引入。

但如果你用的是Vue 2.x的版本,想体验Composition API的话,就需要单独引入@vue/composition-api这个插件,而且引入方式和Vue3不一样——不是从vue包里引入,而是从@vue/composition-api包里引入,而且要先在入口文件里注册这个插件。

很多刚接触Vue生态的开发者可能会搞混Vue2和Vue3的Composition API引入方式,这也是导致“watch is not defined”的一个原因。

怎么判断是Vue版本的问题?

先看你项目的package.json文件里的vue依赖版本:

  • 如果是^2.6.x或者更低的版本,那你用的是Vue2,不能直接从vue包里引入watch;
  • 如果是^3.0.0或者更高的版本,那你用的是Vue3,可以直接从vue包里引入。

怎么解决Vue2引入Composition API的问题?

如果你用的是Vue2,想体验Composition API,步骤如下:

  1. 安装@vue/composition-api插件:

    npm install @vue/composition-api --save
    # 或者用yarn
    yarn add @vue/composition-api
  2. 在项目的入口文件(比如main.js)里注册这个插件:

    // main.js
    import Vue from 'vue';
    import App from './App.vue';
    // 引入插件
    import VueCompositionAPI from '@vue/composition-api';
    // 注册插件
    Vue.use(VueCompositionAPI);
    new Vue({
      render: h => h(App)
    }).$mount('#app');
  3. 在组件里从@vue/composition-api包里引入watch(以及其他Composition API):

    <!-- Vue2 + @vue/composition-api的组件写法 -->
    <template>
      <div>{{ count }}</div>
    </template>
    <script>
    // 注意!是从@vue/composition-api引入,不是从vue引入!
    import { ref, watch } from '@vue/composition-api';
    export default {
      setup() {
        const count = ref(0);
        watch(count, (newVal) => {
          console.log(newVal);
        });
        // Vue2的@vue/composition-api也需要return才能在template里用
        return { count };
      }
    }
    </script>

这里要注意,Vue2的@vue/composition-api插件里的API和Vue3的核心Composition API几乎完全一致,只有少数几个API有差异(比如Vue3的definePropsdefineEmits<script setup>语法糖的专属,Vue2的插件里没有;Vue2的插件里的getCurrentInstance()返回的实例和Vue3的也略有不同),但watch这个API是完全一致的,所以你不用担心写法问题。

Tree Shaking配置有问题?不太可能,但可以排查一下

有些开发者可能会担心:“是不是我项目的Tree Shaking配置有问题,把watch给摇掉了?”其实这个原因出现的概率非常低——因为Tree Shaking只会摇掉“未被使用的代码”,如果你已经在代码里引入了watch并且调用了它,Tree Shaking是不会把它摇掉的。

但如果你确实想排查一下,可以检查你项目的打包工具配置:

  1. 如果用的是Vue CLI创建的项目,默认的Tree Shaking配置是没问题的,不需要修改;
  2. 如果用的是Vite创建的项目,默认的Tree Shaking配置也是没问题的,因为Vite基于Rollup,Rollup的Tree Shaking效率比Webpack更高;
  3. 如果用的是自己配置的Webpack,要确保你开启了生产模式(mode: 'production'),并且用的是ES6的模块语法(import/export),而不是CommonJS的模块语法(require/module.exports)——因为Tree Shaking只对ES6的模块语法有效。

避坑指南:以后再遇到“XXX is not defined”怎么办?

其实不仅是watch,Vue3 Composition API里的所有API(比如ref、reactive、computed、watchEffect、onMounted、defineProps、defineEmits)如果出现“XXX is not defined”的报错,都可以用下面这套通用的避坑指南来排查:

第一步:检查引入是否正确

  • 首先看文件顶部有没有从vue(Vue3)或者@vue/composition-api(Vue2)包里引入对应的API;
  • 然后看引入的变量名和调用的变量名是否完全一致(包括大小写);
  • 最后看引入的API是否是你需要的那个——比如别把watchEffect写成watch,别把onMounted写成mounted

第二步:检查调用的作用域是否正确

  • Composition API的所有函数必须在setup函数(或者<script setup>语法糖里)调用;
  • <script setup>语法糖里的definePropsdefineEmitsdefineExposewithDefaults是编译器宏,不需要引入,但也只能在<script setup>里调用;
  • 不要在setup函数外面、或者Options API的methods/mounted等钩子函数里调用Composition API的函数(除非你确实需要混用,并且知道怎么处理作用域)。

第三步:检查Vue版本是否正确

  • 看package.json里的vue依赖版本,确保是Vue3或者安装了@vue/composition-api插件的Vue2;
  • 如果是Vue2,要从@vue/composition-api包里引入API,而不是从vue包里引入;
  • 如果是Vue3,要确保是^3.0.0或者更高的版本。

第四步:检查是否有拼写错误

  • 很多时候报错都是因为拼写错误,比如把ref写成rfe,把reactive写成reactiv
  • 利用编辑器的自动补全功能,可以有效避免拼写错误。

扩展一下:Vue3的watch和watchEffect有什么区别?

既然讲到了watch,那顺便提一下Vue3里的另一个监听器watchEffect——很多新手也会搞混这两个API,虽然它们不会导致“watch is not defined”的报错,但了解它们的区别可以让你更好地使用Vue3的监听器。

watch的特点

  • 需要明确指定监听的数据源(可以是ref、reactive、数组、函数返回值);
  • 只有当指定的数据源变化时才会执行回调函数;
  • 回调函数可以拿到新值和旧值
  • 默认是懒执行的——也就是组件初始化的时候不会执行,只有当数据源第一次变化时才会执行(可以通过配置immediate: true改为立即执行);
  • 可以通过配置deep: true来监听嵌套对象的变化。

watchEffect的特点

  • 不需要明确指定监听的数据源,它会自动追踪回调函数里用到的所有响应式数据;
  • 只要回调函数里用到的任何一个响应式数据变化,就会执行回调函数;
  • 回调函数拿不到旧值
  • 默认是立即执行的——也就是组件初始化的时候会执行一次,之后当响应式数据变化时再执行;
  • 不需要配置deep: true,因为它自动追踪的是深层的响应式数据(只要你在回调函数里用到了嵌套对象的属性)。

什么时候用watch,什么时候用watchEffect?

  • 如果你需要明确知道哪个数据源变化了,或者需要拿到旧值,或者需要懒执行(默认不执行,第一次变化才执行),那就用watch;
  • 如果你只需要在响应式数据变化时执行一些副作用(比如发送请求、修改DOM),不需要知道哪个数据源变化了,也不需要拿到旧值,那就用watchEffect。

举个例子:

<script setup>
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
const name = ref('张三');
// watch:明确监听count,拿到新值和旧值,懒执行
watch(count, (newVal, oldVal) => {
  console.log(`count变了:${oldVal} → ${newVal}`);
});
// watchEffect:自动追踪count和name,立即执行,拿不到旧值
watchEffect(() => {
  console.log(`当前count:${count.value},当前name:${name.value}`);
});
// 点击按钮后,count和name都会变化
const handleClick = () => {
  count.value++;
  name.value = name.value === '张三' ? '李四' : '张三';
};
</script>
<template>
  <div>count: {{ count }}</div>
  <div>name: {{ name }}</div>
  <button @click="handleClick">点击修改</button>
</template>

当你第一次打开页面的时候,watchEffect会立即执行一次,输出“当前count:0,当前name:张三”,但watch不会执行;当你点击按钮的时候,count和name都会变化,watch会执行一次(因为只监听count),输出“count变了:0 → 1”,watchEffect也会执行一次,输出“当前count:1,当前name:李四”。

Vue3出现“watch is not defined”的报错,本质上是因为你没有掌握Composition API“按需引入,组合使用”的核心逻辑,今天我们全流程拆解了这个问题的所有可能原因——完全忘记引入、写错变量名、混用Options API和Composition API、Vue版本不对(Vue2没装插件)、Tree Shaking配置问题(概率极低),并且给了一套通用的避坑指南,以后再遇到类似的“XXX is not defined”都能自己解决,最后我们还扩展了Vue3的watch和watchEffect的区别,帮助你更好地使用Vue3的监听器。

其实Vue3的Composition API并没有想象中那么难,只要你掌握了它的核心逻辑,多写多练,很快就能上手,如果你还有其他Vue3的问题,欢迎在评论区留言讨论。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门