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

Vue3 this watch能用吗

terry 6小时前 阅读数 87 #Vue

刚从Vue2转到Vue3的开发者,打开项目第一个踩的坑大概率有它——明明照着Vue2的写法在setup()里敲了this.$watch,结果直接报错说this是undefined?或者有的人用了Options API(就是传统的export default { data() {}, methods() {} }),突然又想试试setup(),但搞不清能不能混着用watch和this?别慌,这篇文章把Vue3里的“this”和“watch”揉碎了说,从能不能用到怎么用最合适,都给你讲明白。

Vue2和Vue3 this的本质区别

在解决Vue3 this watch能不能用的问题前,得先搞懂——为什么Vue2能用this?Vue3的this又去哪了?这不是Vue团队故意改得难用,而是因为Vue3底层从Options API的“构造函数实例化”模式,改成了Composition API的“函数式上下文”模式。

Vue2的this是什么?

Vue2的Options API,本质是你给Vue构造函数传了一个“配置对象”,当组件被挂载时,Vue会把这个配置对象的data、props、computed、methods等属性,全合并到一个Vue实例对象上,并且用proxy(Vue2是Object.defineProperty)把它们变成响应式的,你写的this,就是这个合并后的实例对象,所以不管是methods里、mounted生命周期里,还是data的初始化函数里(除了箭头函数),都能拿到this.$data、this.$props、this.$watch这些API。

Vue3的this消失了?

Vue3的setup()函数,是组件初始化的入口函数,它在组件实例创建之前就已经执行了——所以你在setup()里当然拿不到还没出生的实例对象this,而且Composition API提倡“函数式复用”,比如你可以把处理搜索、分页的逻辑抽成一个单独的useSearch、usePagination函数,在多个组件里复用,这些复用的函数本来就不该依赖某个特定组件的this,否则复用性会大打折扣。

如果你还是想用传统的Options API写法,或者在Options API里混合一点Composition API的逻辑,那this还是存在的——在Options API的data、methods、mounted、computed等选项里,this依然是当前组件的实例对象,而且也能通过this调用到Vue3保留的一些旧API,比如this.$watch、this.$nextTick。

Vue3 this watch具体能用在哪些场景?

搞懂了this的变化,现在可以直接回答标题的问题了:Vue3的this watch能用,但有非常明确的适用场景限制——只能在Options API的选项里用,不能在setup()函数、或者Composition API的复用函数(比如use开头的函数)里用

纯Options API的Vue3项目

如果你刚从Vue2转到Vue3,暂时不想学Composition API,直接把之前的Vue2代码复制到Vue3项目里(当然要注意Vue3的一些小改动,比如v-model的语法、移除了.filter、移除了$children这些),那this.$watch是完全能正常运行的,和Vue2的写法、功能、API参数一模一样——比如监听基本类型、监听对象的某个属性、深度监听、立即执行,这些都没问题。

举个纯Options API的例子:

export default {
  data() {
    return {
      userName: '张三',
      userInfo: {
        age: 25,
        address: '北京市朝阳区'
      }
    };
  },
  mounted() {
    // 监听基本类型userName
    this.$watch('userName', (newVal, oldVal) => {
      console.log(`用户名从${oldVal}变成了${newVal}`);
    });
    // 监听对象的单个属性userInfo.age
    this.$watch('userInfo.age', (newVal, oldVal) => {
      console.log(`年龄从${oldVal}变成了${newVal}`);
    });
    // 深度监听整个userInfo对象(不管里面哪个属性变了都触发)
    this.$watch('userInfo', (newVal) => {
      console.log('用户信息变了', newVal);
    }, { deep: true, immediate: true });
  }
};

把这段代码放到Vue3的单文件组件里,运行起来完全正常,immediate参数会在组件刚挂载的时候就触发一次深度监听的回调,输出用户的初始信息,修改userName、userInfo.age、userInfo.address也会分别触发对应的回调。

Options API里混合了setup()函数

有的开发者可能会在Vue3里混合使用两种API——比如用Options API的data定义响应式数据,用methods定义简单的点击事件,然后用setup()函数抽一部分复杂的逻辑复用,这种情况下,Options API的选项里还是能用this.$watch,但setup()函数里依然不能用,而且setup()函数里暴露的响应式数据(比如用ref、reactive定义的),能不能在Options API的this.$watch里监听呢?

答案是可以,但有个前提条件——必须用return语句把setup()里的响应式数据暴露出去,因为setup()函数返回的对象,会被合并到Options API的Vue实例对象上,所以暴露出去的ref会自动解包(不需要加.value),reactive会保留原样,就像你在data里定义的一样,直接用字符串路径监听就行。

举个混合使用的例子:

import { ref, reactive } from 'vue';
export default {
  data() {
    return {
      // Options API定义的响应式数据
      theme: 'light'
    };
  },
  setup() {
    // Composition API定义的响应式数据
    const darkModeSwitch = ref(false);
    const userSettings = reactive({
      fontSize: 16,
      lineHeight: 1.5
    });
    // 必须return出去才能在Options API里拿到
    return {
      darkModeSwitch,
      userSettings
    };
  },
  mounted() {
    // 监听Options API定义的theme
    this.$watch('theme', (newVal) => {
      document.body.className = newVal;
    });
    // 监听setup()暴露的ref(自动解包,不用加.value)
    this.$watch('darkModeSwitch', (newVal) => {
      this.theme = newVal? 'dark' : 'light';
    });
    // 监听setup()暴露的reactive的单个属性
    this.$watch('userSettings.fontSize', (newVal) => {
      document.body.style.fontSize = `${newVal}px`;
    });
  }
};

这段代码也能正常运行——点击组件里的darkModeSwitch(要在template里绑定),会触发watch修改theme,theme的变化又会触发另一个watch修改body的className,完美实现了两种API数据之间的联动。

为什么Vue3官方不推荐用this watch?

虽然Vue3保留了this.$watch这个API,但官方文档里明确说了,推荐使用Composition API的watch或者watchEffect函数,而不是Options API的this.$watch,这不是官方强制要求,而是因为Composition API的watch和watchEffect有几个this.$watch比不了的优势,更适合Vue3的开发模式。

更好的类型推导

Vue3是用TypeScript重写的,官方也非常推荐用TypeScript开发Vue项目——而Composition API的watch和watchEffect,天生就有很好的类型推导能力,不需要你额外加很多类型注解,或者用Vue.extend这种麻烦的方式,而Options API的this.$watch,类型推导就比较弱,因为它依赖于Vue实例的类型,如果你混合了setup()暴露的数据,类型推导可能会出错。

比如用TypeScript写纯Options API的this.$watch,你可能要这样写(不然会有类型警告):

import { defineComponent, WatchOptions } from 'vue';
interface UserInfo {
  age: number;
  address: string;
}
export default defineComponent({
  data() {
    return {
      userName: '张三',
      userInfo: {
        age: 25,
        address: '北京市朝阳区'
      } as UserInfo
    };
  },
  mounted() {
    // 必须明确指定类型,不然newVal和oldVal的类型是any
    this.$watch('userInfo.age', (newVal: number, oldVal: number) => {
      console.log(`年龄从${oldVal}变成了${newVal}`);
    });
  }
});

而用Composition API的watch,就简单多了,类型推导自动完成:

import { defineComponent, ref, reactive, watch } from 'vue';
interface UserInfo {
  age: number;
  address: string;
}
export default defineComponent({
  setup() {
    const userName = ref('张三');
    const userInfo = reactive<UserInfo>({
      age: 25,
      address: '北京市朝阳区'
    });
    // 这里newVal和oldVal的类型自动是string和number,不用加注解
    watch(userName, (newVal, oldVal) => {
      console.log(`用户名从${oldVal}变成了${newVal}`);
    });
    watch(() => userInfo.age, (newVal, oldVal) => {
      console.log(`年龄从${oldVal}变成了${newVal}`);
    });
    return { userName, userInfo };
  }
});

更灵活的监听方式

Composition API的watch,除了能像this.$watch一样监听字符串路径,还能直接监听ref、reactive对象、甚至是一个返回值的函数,这就比this.$watch灵活多了,比如你要监听两个不同的数据,不管哪个变了都触发同一个回调,用this.$watch的话,得写两个watch,或者把这两个数据放到一个对象里再深度监听;而用Composition API的watch,直接传一个数组就行。

举个灵活监听的例子:

import { ref, reactive, watch } from 'vue';
export default {
  setup() {
    const searchKeyword = ref('');
    const pageNum = ref(1);
    const filters = reactive({
      status: 'all',
      category: ''
    });
    // 监听多个数据,不管哪个变了都触发
    watch([searchKeyword, pageNum, () => filters.status, () => filters.category], 
      ([newKeyword, newPage, newStatus, newCategory]) => {
        // 这里可以写获取列表数据的逻辑
        console.log('参数变了', { newKeyword, newPage, newStatus, newCategory });
      },
      { immediate: true }
    );
    return { searchKeyword, pageNum, filters };
  }
};

要是用this.$watch的话,要么得写四个独立的watch,把获取列表的逻辑抽成一个函数,每个watch都调用它;要么得把这四个参数放到一个computed里,再深度监听这个computed——显然不如直接传数组方便。

更方便的函数式复用

刚才说过,Composition API提倡“函数式复用”,而watch和watchEffect是可以直接写在复用函数里的,不需要依赖某个特定组件的this,比如你可以把刚才的搜索、分页逻辑抽成一个useList函数:

import { ref, reactive, watch } from 'vue';
// 复用函数useList,接收API请求函数作为参数
export function useList(fetchData) {
  const searchKeyword = ref('');
  const pageNum = ref(1);
  const filters = reactive({
    status: 'all',
    category: ''
  });
  const listData = ref([]);
  const loading = ref(false);
  const total = ref(0);
  // 监听参数变化,触发请求
  watch([searchKeyword, pageNum, () => filters.status, () => filters.category], 
    async () => {
      loading.value = true;
      try {
        const res = await fetchData({
          keyword: searchKeyword.value,
          page: pageNum.value,
          status: filters.status,
          category: filters.category
        });
        listData.value = res.data.list;
        total.value = res.data.total;
      } catch (error) {
        console.error('获取列表失败', error);
      } finally {
        loading.value = false;
      }
    },
    { immediate: true }
  );
  // 提供重置参数的方法
  const resetParams = () => {
    searchKeyword.value = '';
    pageNum.value = 1;
    filters.status = 'all';
    filters.category = '';
  };
  // 暴露需要的数据和方法
  return {
    searchKeyword,
    pageNum,
    filters,
    listData,
    loading,
    total,
    resetParams
  };
}

然后在任何需要列表的组件里,直接调用这个useList函数就行,不用重复写watch和请求逻辑:

import { useList } from './useList';
import { getProductList } from './api'; // 假设这是你的产品列表API
export default {
  setup() {
    // 直接传入产品列表的API函数,就能拿到所有需要的数据和方法
    const {
      searchKeyword,
      pageNum,
      filters,
      listData,
      loading,
      total,
      resetParams
    } = useList(getProductList);
    return {
      searchKeyword,
      pageNum,
      filters,
      listData,
      loading,
      total,
      resetParams
    };
  }
};

这种复用方式,在Vue2里是很难实现的——Vue2的复用方式主要是mixins,但mixins有很多问题,比如命名冲突、来源不清晰、类型推导差,而Composition API的复用函数完美解决了这些问题。

Vue3 watch和watchEffect的区别(新手必看)

既然官方推荐用Composition API的watch和watchEffect,那这两个函数有什么区别呢?很多刚学Composition API的开发者都会搞混,这里给你简单对比一下,方便你选择用哪个。

watch:有明确的监听源,默认不立即执行

watch和Vue2的this.$watch很像,需要你明确指定“要监听哪个数据”(监听源),然后指定“数据变化后要做什么”(回调函数),默认情况下,watch只会在监听源的数据从初始值变化之后才会执行回调,不会在组件刚挂载的时候执行——除非你加上immediate: true参数。

watch的监听源可以是:

  1. 单个ref
  2. 单个reactive对象(会自动深度监听)
  3. 单个返回值的函数(常用于监听reactive对象的单个属性,或者监听computed的返回值)
  4. 三种类型组成的数组(监听多个数据)

watchEffect:没有明确的监听源,自动追踪依赖,默认立即执行

watchEffect就不一样了,你不需要明确指定要监听哪个数据,只需要写一个“副作用函数”——在这个函数里,你用到了哪些响应式数据,watchEffect就会自动追踪这些数据作为依赖,当任何一个依赖的数据变化时,副作用函数就会重新执行,而且watchEffect默认会在组件刚挂载的时候立即执行一次,不需要加任何参数。

举个对比的例子:

import { ref, watch, watchEffect } from 'vue';
export default {
  setup() {
    const count = ref(0);
    const doubleCount = ref(0);
    // 用watch:明确指定监听count,数据变化后修改doubleCount,加上immediate才会立即执行
    watch(count, (newVal) => {
      doubleCount.value = newVal * 2;
      console.log('watch触发:count变了');
    }, { immediate: true });
    // 用watchEffect:不用明确指定,自动追踪count作为依赖,默认立即执行
    watchEffect(() => {
      doubleCount.value = count.value * 2;
      console.log('watchEffect触发:count变了');
    });
    // 测试一下:点击按钮修改count,两个都会触发
    const increment = () => {
      count.value++;
    };
    return { count, doubleCount, increment };
  }
};

把这段代码放到组件里,刚打开页面的时候,watch和watchEffect都会各输出一次“触发”,doubleCount都是0;点击一次按钮,count变成1,watch和watchEffect又会各输出一次,doubleCount变成2。

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

  • 如果你需要获取数据变化前后的旧值,或者需要手动控制什么时候开始/停止监听(比如只有在某个条件满足的时候才监听),或者需要监听reactive对象的单个属性且不需要深度监听,那你就用watch。
  • 如果你不需要旧值,只需要在依赖的数据变化时自动执行副作用(比如修改DOM、发送请求、保存数据到localStorage),而且副作用函数里用到的依赖很明确,那你就用watchEffect,更简洁。

总结的问题,你应该已经有了非常清晰的答案:

  • Vue3的this watch能用,但只能在Options API的选项里用(比如data的非箭头函数初始化、methods、mounted等),不能在setup()函数或者Composition API的复用函数里用。
  • 如果你的项目是纯Options API的Vue3项目,或者在Options API里混合了setup(),那可以继续用this watch,但官方更推荐用Composition API的watch或者watchEffect,因为它们有更好的类型推导、更灵活的监听方式、更方便的函数式复用。
  • watch和watchEffect的区别在于:watch有明确的监听源,默认不立即执行,能获取旧值;watchEffect没有明确的监听源,自动追踪依赖,默认立即执行,不能获取旧值。

刚从Vue2转到Vue3的开发者,不用急着把所有的this.$watch都改成watch或者watchEffect,可以先在新项目或者新功能里尝试用Composition API,慢慢适应,等熟悉了之后再逐步重构旧代码——毕竟Vue3保留Options API,就是为了给开发者一个平滑的过渡过程。

版权声明

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

热门