Vue3 this watch能用吗
刚从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的监听源可以是:
- 单个ref
- 单个reactive对象(会自动深度监听)
- 单个返回值的函数(常用于监听reactive对象的单个属性,或者监听computed的返回值)
- 三种类型组成的数组(监听多个数据)
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前端网发表,如需转载,请注明页面地址。
code前端网



