一、生命周期
核心变化
1.组合式API的引入
- setup()替代beforeCreate和created。所有组合式API逻辑在此函数中初始化,替代Vue2的data、method等选项式配置
- 钩子函数前缀on。生命周期函数需要从Vue显示导入,如onMounted,且只能在setup()或
<script setup>
中使用。
2.钩子函数重命名
- beforeDestroy -> onBeforeUnmount
- destroyed -> onUnmounted
3.新增钩子
- onServerPrefetch。服务端渲染(SSR)期间异步获取数据。
- 调试钩子。onRenderTracked(跟踪响应式依赖)、OnRenderTriggered(响应式变更触发)。
生命周期阶段详解
1.初始化阶段
- setup()。替代beforeCreated和created,初始化响应式数据、方法等。注意:无法访问this。
- 选项式API兼容。替代beforeCreated和created仍可用,但避免与setup()混用。
2.挂载阶段
- onBeforeMount。组件挂载到DOM前调用,此时虚拟DOM已生成但未渲染。
- onMounted。组件挂载完成,可操作DOM或发起网络请求。
3.更新阶段
- onBeforeUpdate。数据变化导致DOM更新前触发,适合获取更新前的DOM状态。
- onUpdated。DOM更新执行后,避免在此修改状态,可能导致无限循环。
4.卸载阶段
- onBeforeUnmount。组件卸载前调用,清理定时器,取消网络请求,移除事件监听。
- onUnMounted。组件卸载后触发,此时子组件已全部卸载。
5.其他钩子
- onErrorCaptured。捕获子孙组件错误,可返回false阻止冒泡。
- onActivated/onDeactivated。
<KeepAlive>
缓存组件激活/停用时调用。
高频面试题
1.父子组件生命周期执行顺序
- 挂载阶段:
父onBeforeMount->子onBeforeMount->子onMounted->父onMounted
- 更新阶段:
父onBeforeUpdate->子onBeforeUpdate->子onUpdated->父onUpdated
- 卸载阶段:
父onBeforeUnmount->子onBeforeUnmount->子onUnmounted->父onUnmounted
2.在setup()中如何访问this?
- setup()中没有this,响应式数据通过ref/reactive定义,方法直接申明。
3.异步请求放在哪个钩子?
- 客户端渲染(CSR):onMounted(确保DOM可用)。
- 服务端渲染(SSR):onServerPrefetch。
二、组件通信
1.Props/Emits(父子通信)
props(父->子)
1<!-- 父组件 -->
2<Child :title="data" />
3<!-- 子组件 -->
4<script setup>
5defineProps(['title'])
6</script>
- 类型校验:
defineProps({ title:{ type:String, required:true } })
- 单项数据流:子组件不能直接修改props(需要通过emit通知父组件)。
emits(子->父)
1<!-- 子组件 -->
2<button @click="$emit('update', value)" ></button>
3<!-- 父组件 -->
4<Child @update="handleUpdate" />
- Vue3特性:
defineEmits(['update'])
显式申明事件。
2.v-model双向绑定(语法糖进阶)
- 单值绑定
1<!-- 父组件 -->
2<Child v-model="message" />
3<!-- 子组件 -->
4<input :value="modelValue" @input="$emit('update:modelValue',$event.target.value)" >
5<script setup>
6defineProps(["modalValue"])
7defineEmits(["update:modelValue"])
8</script>
9
- 多值绑定
1<Child v-model:title="title" v-model:content="content" />
3.ref/expose(父访问子组件)
- 模版引用
1<!-- 父组件 -->
2<Child ref="childRef" />
3<script setup>
4const childRef = ref(null);
5
6childRef.value.childmethod();
7</script>
8
9<!-- 子组件 -->
10const childMethod = () => {};
11defineExpose({ childMethod })
12</script>
4.provide/inject(跨层级通信)
- 依赖注入
1import { provide } from 'vue';
2provide('theme', 'dark');
3
4
5import { inject } from 'vue';
6
7const theme = inject('theme', 'light')
- 响应式数据
1const count = ref(0);
2provide('count', count);
5.事件总线(Event Bus)
- Vue3官方废弃
$on
,推荐第三方库(如mitt)
1import mitt from 'mitt';
2export const emitter = mitt();
3
4
5emitter.emit('refresh', data);
6
7
8emitter.on('refresh', (data) => { })
9
10
11onUnmounted(() => emitter.off('refresh'))
6.状态管理(pinia)
- 替代Vuex的官方状态库
1export const useCounterStore = defineStore('counter',{
2 state:()=>({ count: 0 }),
3 actions:{
4 increment() { this.count++; }
5 }
6})
7
8
9const store = useCounterStore();
10store.increment();
7.属性透传($attr)
- 透传非props属性
1<!-- 父组件 -->
2<Child class="child-style" data-id="123" />
3
4<!-- 子组件 -->
5<div v-bind="$attrs"></div>
6<script setup>
7
8defineOptions({ inheritAttrs: false });
9</script>
8.模版引用(Template Refs)
- 直接操作DOM
1<template>
2 <input ref="inputRef" />
3</template>
4
5<script setup>
6const inputRef = ref(null);
7onMounted(() => inputRef.value.focus())
8</script>
高频面试题
1.父子组件通信方式有哪些?
- 父->子:props、$attr、ref。
- 子->父:emits、v-model。
- 双向:v-model、状态管理。
2.provide/inject能否替代Vuex?
- 适用场景:跨层级但关系明确的组件。
- 局限性:不适合全局状态管理,无法跟踪状态变化历史。
3.Vue3为什么不移除事件总线?
- 设计理念:避免全局事件导致代码维护困难。
- 代替方案:使用pinia管理状态或mitt库实现事件总线。
4.如何实现兄弟组件通信?
- 通过共同父组件中转(props/emits)
- 事件总线(mitt)
- 状态管理(Pinia)
5.动态组件如何保持状态?
<KeepAlive>
组件
1<KeepAlive>
2 <component :is="currentComponent" />
3</KeepAlive>
实战场景选择
场景 | 推荐方案 |
---|---|
父子简单数据传递 | Props/Emits |
表单双向绑定 | v-model语法糖 |
跨层级组件共享配置 | provide/inject |
复杂全局状态管理 | Pinia |
非父子组件松散通信 | 事件总线(mitt) |
组件模板直接操作 | ref/expose |
三、修饰符
核心修饰符分类及作用
1.事件修饰符
- stop:阻止事件冒泡。
1<button @click.stop="handleClick">点击</button>
- prevent:阻止默认行为。
1<form @submit.prevent="onSubmit"></form>
- capture:使用捕获模式。
1<div @click.capture="handleCapture">捕获触发</div>
- self:仅当事件从元素自身触发时执行。
1<div @click.self="handleSelf">仅自身点击有效</div
- once:事件只触发一次。
1<button @click.once="handleOnce">只触发一次</button>
- passive:提升滚动性能,不阻止默认行为。
1<div @scroll.passive="onScroll">滚动优化</div>
2.v-model修饰符
- lazy:输入框失焦后更新数据(替代input为change事件)。
1<input v-model.lazy="message" />
- number:将输入值转为数值类型。
1<input v-model.number="age" type="number" />
- trim:自动去除首位空格。
1<input v-model.trim="username" />
3.键盘修饰符
- 键名修饰符:直接使用按键名(如:.enter,.tab,.esc)。
1<input @keyup.enter="submit" />
- 系统修饰键:.ctrl,.alt,.shift,.meta(MAC的Command健)
1<button @click.ctrl="handleCtrlClick">需按住 Ctrl 点击</button>
- .exact:精确匹配系统修饰键组合。
1<button @click.ctrl.exact="onlyCtrl">仅 Ctrl 按下时触发</button>
4.鼠标修饰符
- .left,.right,.middle:限制鼠标按键。
1<div @mousedown.right="handleRightClick">右键点击</div>
Vue3修饰符新特性
1…sync修饰符的替代
- Vue2:通过.sync实现父子组件双向绑定。
- Vue3:改用v-model:propName+emit(‘update:propName’)。
1<!-- 父组件 -->
2<Child v-model:title="pageTitle" />
3
4<!-- 子组件 -->
5<script setup>
6defineProps(['title'])
7defineEmits(['update:title'])
8</script>
2…native修饰符移除
- Vue2:组件上绑定原生事件需用.native。
- Vue3:默认不绑定到根元素,需通过emits声明或手动绑定。
1<!-- 父组件 -->
2<Child @click="handleClick" /> <!-- 需子组件 emit('click') -->
3.自定义组件支持v-model修饰符
- 通过modelModifiers访问修饰符。
1<!-- 父组件 -->
2<CustomInput v-model.capitalize="text" />
3
4<!-- 子组件 -->
5<script setup>
6const props = defineProps(['modelValue', 'modelModifiers'])
7const emit = defineEmits(['update:modelValue'])
8
9const handleInput = (e) => {
10 let value = e.target.value
11 if (props.modelModifiers.capitalize) {
12 value = value.charAt(0).toUpperCase() + value.slice(1)
13 }
14 emit('update:modelValue', value)
15}
16</script>
高频面试题
1.v-model的.lazy和.sync有何区别?
- .lazy:延迟数据同步(input->change事件)。
- .sync:Vue2中用于父子组件双向绑定,Vue3改用v-model:prop。
2.如何阻止事件冒泡和默认行为?
- 链式调用:
@click.stop.prevent
(顺序不影响效果)。
3.Vue3如何监听组件原生事件?
- 子组件手动绑定并emit事件
- 使用v-on=“$attrs”将事件绑定到内部元素。
4.如何实现自定义v-model修饰符?
- 通过modelModifiers判断修饰符存在,并调整数据逻辑。
5…exact修饰符的作用是什么?
- 精确控制系统修饰键组合,避免其他修饰键按下触发。
四、指令
核心内置指令
1.数据绑定
- v-bind:动态绑定属性(缩写 :)。
1<img :src="imageUrl" :alt="altText">
2<!-- 动态属性名 -->
3<div :[dynamicAttr]="value"></div>
- v-model:表单双向绑定(组合式API增强)。
1<input v-model="text"> <!-- 默认对应 `modelValue` -->
2<!-- 自定义组件多v-model -->
3<CustomInput v-model:title="title" v-model:content="content" />
2.条件渲染
- v-if/v-else-if/v-else:动态添加/移除DOM元素。
1<div v-if="score > 90">A</div>
2<div v-else-if="score > 60">B</div>
3<div v-else>C</div>
- v-show:通过css切换显示(display:none)。
- v-if与v-show的区别:v-show初始渲染成本高,频繁切换时性能更好。
3.列表渲染
- v-for:遍历数组或对象
1<li v-for="(item, index) in items" :key="item.id">{{ index }}: {{ item.name }}</li>
- 必须指定key:优化虚拟DOM复用,避免渲染错误。
- 避免与v-if共用:优先级问题(Vue2中v-for优先,Vue3中v-if优先),应改用计算属性过滤数据。
4.事件绑定
- v-on:监听事件(缩写 @)
1<button @click="handleClick">点击</button>
2<!-- 事件修饰符 -->
3<form @submit.prevent="onSubmit"></form>
5.其他指令
- v-html:渲染原始HTML(警惕
XSS
攻击) - v-text:替代
{{ }}
插值 - v-pre:跳过编译,保留原始内容
- v-cloak:隐藏未编译的模版(配合css使用)
1[v-cloak] { display: none; }
- v-once:仅渲染一次,后续数据变化不更新。
1<div v-once>{{ staticContent }}</div>
Vue3指令新特性
1.v-model增强
- 支持多个v-model
1<UserForm v-model:name="name" v-model:age="age" />
- 自定义修饰符:通过
modelModifiers
访问
1defineProps(['modelValue', 'modelModifiers']);
2if (props.modelModifiers.capitalize) {
3
4}
2.v-bind合并行为
- 同名属性合并策略:Vue3中后绑定的属性会覆盖前面的,而Vue2会合并。
3.v-for中的ref处理
- ref数组不再自动创建,需手动处理:
1<div v-for="item in list" :ref="setItemRef"></div>
2<script setup>
3const itemRefs = [];
4const setItemRef = el => { if (el) itemRefs.push(el); };
5</script>
自定义指令
1.注册方式
- 全局注册
1app.directive('focus', {
2 mounted(el) { el.focus(); }
3});
- 局部注册(组合式API)
1<script setup>
2const vFocus = { mounted: (el) => el.focus() };
3</script>
2.指令生命周期钩子
- created:元素属性初始化前
- beforeMount:元素插入DOM前(Vue2的bind)
- mounted:元素插入DMO后(Vue2的inserted)
- beforeUpdate:组件更新前
- update:组件更新后
- beforeUnmount:组件卸载前(Vue2的unbind)
- unMounted:组件卸载后
3.指令参数
- el:绑定的DOM元素
- binding:绑定以下属性:
- value:指令的绑定值(如
v-dir="value"
) - oldValue:旧值(仅在
beforeUpdate和update可用
) - arg:指令参数(如
v-dir:arg
) - modifiers:修饰符对象(如
v-dir.modif->{ modif: true }
) - instance:组件实例(替代Vue2的
vnode.context
)
- value:指令的绑定值(如
实战实例
- 自动聚焦指令
1const vFocus = {
2 mounted(el) { el.focus(); }
3};
- 权限控制指令
1const vPermission = {
2 mounted(el, binding) {
3 const roles = store.getters.roles;
4 if (!roles.includes(binding.value)) {
5 el.parentNode?.removeChild(el);
6 }
7 }
8};
高频面试题
1.v-if和v-show的区别
- v-if:条件为假时销毁DOM,适合不频繁切换场景
- v-show:始终保留DOM,通过css切换显示,适合频繁切换
2.为什么v-for需要key?
- 虚拟DOM优化:key帮助Vue识别节点身份,避免错误使用复用元素,提升更新效率。
3.如何实现自定义指令?
- 定义指令对象并注册,通过生命周期钩子操作DOM(如自动聚焦、权限控制)。
4.v-model在自定义组件中的实现原理?
- 父组件:
v-model:propName="value"
- 子组件:接收propName,通过
emit('update:propName',value)
更新
5.Vue3中v-bind的合并策略变化
- Vue3中后绑定的属性会覆盖前面的同名属性,而Vue2会合并(如class和style)
五、ref和reactive的区别
核心区别与使用场景
特性 | ref | reactive |
---|---|---|
适用数据类型 | 基本类型(string /number /boolean )或对象 | 对象或数组 |
访问方式 | 通过 .value 访问 | 直接访问属性 |
响应式原理 | 内部对对象类型调用 reactive | 基于 Proxy 的深层代理 |
模板自动解包 | 在模板中无需 .value | 直接使用属性 |
解构响应性 | 需用 toRefs 保持响应性 | 直接解构会丢失响应性,需用 toRefs |
核心面试题
1.为什么ref需要.value?
-
设计目的:统一处理基本类型和对象类型。
- 基本类型无法通过Proxy代理,ref通过封装对象(
{ value:... }
)实现响应式
- 基本类型无法通过Proxy代理,ref通过封装对象(
-
底层实现:
1function ref(value) {
2 return {
3 __v_isRef: true,
4 get value() { track(this, 'value'); return value; },
5 set value(newVal) { value = newVal; trigger(this, 'value'); }
6 };
7}
2.如何选择ref和reactive?
- 优先ref
- 管理基本类型数据
- 需要明确的数据引用(如传递到函数中仍保持响应性)
- 优先reactive
- 管理复杂对象/数组,避免频繁使用.value
- 需要深层嵌套的响应式数据
3.如何解构reactive对象且保持响应性?
- 使用toRefs
1const state = reactive({ count: 0, name: 'Vue' });
2const { count, name } = toRefs(state);
3count.value++;
4.reactive的局限性是什么?
- 无法直接替换整个对象
1let obj = reactive({ a: 1 });
2obj = { a: 2 };
- 解决方案:使用Object.assign或ref包裹对象
1Object.assign(obj, { a: 2 });
2const objRef = ref({ a: 1 });
5.ref如何处理对象类型
- 自动调用reactive
1const objRef = ref({ count: 0 });
2objRef.value.count++;
6.如何使用ref实现防抖功能?
- 自定义customRef
1function debouncedRef(value, delay = 200) {
2 let timeout;
3 return customRef((track, trigger) => ({
4 get(){
5 track();
6 return value;
7 }
8 set(newVal){
9 clearTimeout(timeout);
10 timeout = setTimeout(() => {
11 value = newVal;
12 tigger();
13 }, delay);
14 }
15 }))
16}
17
18const text = debouncedRef('', 500);
原理深入
ref的响应式实现
- 基本类型:通过
RefImpl
类实现,而RefImpl
类内部使用了 JavaScript 原生的 getter/setter 语法(不是直接调用Object.defineProperty
)。 - 对象类型:内部转化为reactive代理
reactive的响应式实现
- 基于Proxy代理整个对象,递归处理嵌套熟悉
- 依赖收集:在get时调用track收集依赖
- 触发更新:在set时调用trigger通知更新
高频面试题
1.ref和reactive底层实现差异
- ref封装value属性,reactive使用Proxy代理整个对象
2.为什么解构reactive对象会失去响应性?
- 解构得到的是普通值,非响应式引用;使用toRefs转化为ref
3.如何在模版中正确使用ref?
- 直接使用变量名(自动解包.value),但嵌套在对象中需手动解包
1<template>
2 {{ count }} <!-- 自动解包 -->
3 {{ objRef.count }} <!-- 需确保 objRef 是 reactive 或解包后的 ref -->
4</template>
4.如何监听ref或reactive的变化?
- 使用watch或watchEffect:
1watch(countRef, (newVal) => { });
2watch(() => state.count, (newVal) => { });
5.ref和reactive的性能差异
- 基本类型:
ref
更高效(无需Proxy
代理)。 - 对象类型:性能差异可忽略。
六、slot插槽
核心概念
1. 默认插槽
- 父组件:传递内容到子组件默认位置
1<Child>默认内容</Child>
- 子组件:通过
<slot>
接收内容
1<template>
2 <div>
3 <slot>Fallback Content</slot>
4 </div>
5</template>
2.具名插槽
- 父组件:用
v-slot:name
或#name
指定插槽名
1 <Child>
2 <template #header>头部内容</template>
3 <template #default>默认内容</template>
4 <template #footer>底部内容</template>
5</Child>
- 子组件:用
<slot name="header">
定义具名插槽
1<template>
2 <slot name="header"></slot>
3 <slot></slot> <!-- 默认插槽 -->
4 <slot name="footer"></slot>
5</template>
3.作用域插槽
- 子组件:通过
<slot>
传递数据
1<template>
2 <slot :user="user" :data="data"></slot>
3</template>
- 父组件:用
v-slot="props"
接收数据
1<Child>
2 <template #default="slotProps">
3 {{ slotProps.user.name }}
4 </template>
5</Child>
- 解构语法
1<template #default="{ user, data }">
2 {{ user.age }}
3</template>
Vue3插槽新特性
1.统一语法
- 废弃
slot
属性:改用v-slot
指令 - 废弃
slot-scope
:统一使用v-slot
或#
语法
2.动态插槽名
1<template #[dynamicSlotName]>
2 动态插槽内容
3</template>
3.$slots API
变化
- Vue2:
this.$slots
和this.$scopedSlots
分离 - Vue3:统一为
this.$slots
,作用域插槽通过函数调用
1this.$slots.header?.()
高频面试题
1.作用域插槽的作用是什么?
- 数据反向传递:允许子组件向父组件传递数据,实现内容渲染逻辑的定制
2.如何传递多个插槽?
- 多个
<template>
分别指定插槽名 - 通过
v-bind
合并插槽内容(高阶组件)
1<Child v-slot="{ data }">
2 <template #header>Header {{ data }}</template>
3 <template #footer>Footer {{ data }}</template>
4</Child>
3. 动态插槽的应用场景
- 根据状态动态切换插槽内容,如表格组件根据数据类型渲染不同列
4. slot的name属性是否支持动态绑定?
- 支持:
<slot :name="dynamicName">
,但父组件需要用动态插槽名接收
高级用法和原理
1.渲染作用域
- 插槽内容在父组件作用域编译,只能访问父组件数据(除非通过作用域插槽传递子数据)
2.作用域插槽实现原理
- 子组件:将数据作为函数参数传递给插槽
1render() {
2 return h('div', this.$slots.default({ user: this.user }))
3}
3.插槽性能优化
- 避免在插槽内容中使用复杂计算,必要时用v-once缓存静态内容
实战实例
1<!-- 子组件 ScopedList.vue -->
2<template>
3 <ul>
4 <li v-for="item in items" :key="item.id">
5 <slot :item="item" :index="index">
6 默认内容:{{ item.name }}
7 </slot>
8 </li>
9 </ul>
10</template>
11
12<!-- 父组件 -->
13<ScopedList :items="list">
14 <template #default="{ item, index }">
15 {{ index + 1 }}. {{ item.name }} (ID: {{ item.id }})
16 </template>
17
18 <template #footer>
19 <div>Total: {{ list.length }}</div>
20 </template>
21</ScopedList>
七、watch、watchEffect、computed的区别
核心区别
特性 | computed | watch | watchEffect |
---|---|---|---|
用途 | 派生响应式数据 | 监听数据变化,执行副作用操作 | 自动收集依赖,执行副作用操作 |
返回值 | 只读的 Ref 对象 | 返回停止监听的函数 | 返回停止监听的函数 |
依赖收集 | 自动收集依赖,惰性计算(缓存结果) | 需显式指定监听源 | 自动收集依赖,立即执行 |
新旧值获取 | 无 | 可获取旧值和新值 | 无旧值,只跟踪最新值 |
执行时机 | 依赖变化时重新计算 | 默认在组件更新前执行(flush: 'pre' ) | 默认在组件更新前执行(类似 watch ) |
异步处理 | 不支持 | 支持异步操作 | 支持异步操作 |
核心使用场景
1.computed
- 场景:基于响应式数据生成新的值(如过滤列表、计算总和)
1const fullName = computed(() => `${firstName.value} ${lastName.value}`);
2.watch
- 场景:监听特定数据变化,执行异步或复杂逻辑(如API请求、验证)
1watch(userId, async (newId, oldId) => {
2 const data = await fetchUser(newId);
3 userData.value = data;
4}, { immediate: true });
3.watchEffect
- 场景:自动跟踪依赖变化,执行副作用操作(如日志、DOM操作)
1watchEffect(() => {
2 console.log(`窗口大小:${window.innerWidth}x${window.innerHeight}`);
3 document.title = `Count: ${count.value}`;
4});
高频面试题
1.三者核心区别是什么?
- computed:派生数据,有缓存,惰性计算
- watch:显式监听数据源,支持新旧值对比和异步
- watchEffect:自动收集依赖,立即执行,无旧值
2.watch和watchEffect的依赖收集方式有何不同?
- watch:需显式指定监听目标(如
() => state.a
) - watchEffect:自动收集回调函数内使用的所有响应式依赖
3.如何停止watch或watchEffect的监听?
- 调用它们返回的停止函数
1const stop = watch(data, callback);
2stop();
4.computed和普通函数的区别
- computed会缓存结果,依赖不变时不重新计算;普通函数每次调用时都会执行
5.watch的immediate和deep选项的作用
immediate:true
:立即执行回调(初始值触发)deep:true
:深度监听对象/数组内部变化
6.什么情况下使用watchEffect替代watch?
- 当依赖项不明确或需要自动跟踪多个依赖时(如同时监听多个状态)
底层原理与性能优化
1.computed缓存机制
- 内部通过
dirty
标志位标记是否需要重新计算,依赖未变化时直接返回缓存值
2.watch的异步调度
- 默认在组件更新前执行(
flush:'pre'
),可配置'post'
(组件更新后)或'sync'
(同步执行)
3.watchEffect的依赖收集
- 在首次执行回调时收集所有响应式依赖(类似Vue2的watcher依赖收集)
4.性能注意事项
- 避免过度使用watchEffect:自动依赖收集可能导致不必要的重复执行
- 合理使用computed缓存:减少重复计算开销
- 及时清理副作用:在
onUnmounted
中停止监听,避免内存泄漏。
代码实例对比
1.computed VS watch
1const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0));
2
3
4watch(total, (newVal) => {
5 console.log("总价变化:", newVal);
6});
2.watch VS watchEffect
1watch([a, b], ([newA, newB], [oldA, oldB]) => {
2 console.log(`a从${oldA}变为${newA}, b从${oldB}变为${newB}`);
3});
4
5
6watchEffect(() => {
7 console.log(`a=${a.value}, b=${b.value}`);
8});
Vue3新增特性
1.watch支持监听多个数据源
1watch([ref1, () => reactiveObj.prop], ([val1, val2]) => { });
2.watchPostEffect和watchSyncEffect
- watchPostEffect:等同于
watchEffect(..., { flush: 'post' })
,在DOM更新后执行。 - watchSyncEffect:等同于
watchEffect(..., { flush: 'sync' })
,同步执行
总结
- computed:用于派生数据,优先使用
- watch:用于监听特定变化数据,需精确控制依赖
- watchEffect:用于自动依赖跟踪,简化副作用管理
个人笔记记录 2021 ~ 2025