Vue3 Composition API与响应式系统深度解析从Proxy到组件逻辑复用的工程实践
Vue3 Composition API与响应式系统深度解析从Proxy到组件逻辑复用的工程实践
Vue3的Composition API是Vue框架自2.x以来最重要的架构变革。与Vue2的Options API将组件逻辑分散在data、methods、computed等选项中不同,Composition API允许开发者按功能组织代码,将相关逻辑集中在一起。这一变革的基础是Vue3全新的响应式系统——基于ES6 Proxy实现,替代了Vue2基于Object.defineProperty的方案,解决了数组监听、属性新增/删除等问题。本文将从响应式系统的底层原理出发,深入探讨Proxy与Reflect的使用、reactive与ref的设计、computed与watch的实现、Composition API的组织模式、以及自定义组合式函数的设计,通过大量可运行的代码示例帮助读者建立对Vue3响应式系统的完整认知。
一、响应式系统的底层原理
Vue3的响应式系统基于Proxy实现,理解Proxy是理解Vue3响应式的关键。
1.1 Proxy与Reflect基础
// ES6 Proxy基础
// 原始对象
const data = { count: 0, name: 'Vue' };
// 创建Proxy
const proxy = new Proxy(data, {
// 拦截读取
get(target, key, receiver) {
console.log(`读取 ${key}`);
// 使用Reflect保证正确的this指向
const result = Reflect.get(target, key, receiver);
return result;
},
// 拦截设置
set(target, key, value, receiver) {
console.log(`设置 ${key} = ${value}`);
const result = Reflect.set(target, key, value, receiver);
// 通知更新
trigger(target, key);
return result;
},
// 拦截属性删除
deleteProperty(target, key) {
console.log(`删除 ${key}`);
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
},
// 拦截key in proxy
has(target, key) {
console.log(`检查 ${key} 是否存在`);
return Reflect.has(target, key);
},
// 拦截Object.keys
ownKeys(target) {
console.log('获取所有keys');
return Reflect.ownKeys(target);
}
});
// 测试
proxy.count; // 读取 count
proxy.count = 1; // 设置 count = 1
'name' in proxy; // 检查 name 是否存在
Object.keys(proxy); // 获取所有keys
delete proxy.name; // 删除 name
// Vue3的核心:track和trigger
const targetMap = new WeakMap(); // 存储依赖关系
// 收集依赖
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
let activeEffect = null;
// effect函数
function effect(fn) {
activeEffect = fn;
fn(); // 执行时会触发track
activeEffect = null;
}
// 使用
const state = reactive({ count: 0 });
effect(() => {
console.log('Count is:', state.count); // 触发track
});
state.count = 1; // 触发trigger,重新执行effect
// reactive的简化实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
// 深层响应式
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
}
});
}
Proxy是ES6引入的元编程特性,允许拦截对象的基本操作(读取、设置、删除等)。Vue3使用Proxy替代Vue2的Object.defineProperty,解决了几个核心问题:可以监听数组操作(push、pop、splice等)、可以监听属性新增和删除、可以监听Map/Set等集合类型。Reflect方法与Proxy handler一一对应,使用Reflect保证正确的this指向和默认行为。track函数在读取属性时收集依赖(哪个effect使用了这个属性),trigger函数在属性变化时触发相关effect重新执行。这种"依赖收集-触发更新"模式是Vue响应式的核心。
1.2 reactive与ref的设计
// reactive:代理对象
import { reactive, ref, toRefs } from 'vue';
// reactive用于对象
const state = reactive({
count: 0,
user: { name: 'Vue', age: 3 },
list: [1, 2, 3]
});
// 修改属性触发更新
state.count = 1;
state.user.name = 'Vue3';
state.list.push(4); // 数组操作也能触发更新
state.newProp = 'new'; // 新增属性也能触发更新
// 嵌套对象自动深层响应
state.user.name = 'Changed'; // 触发更新
// ref:包装基本类型
const count = ref(0);
const name = ref('Vue');
// 修改ref需要.value
count.value++;
name.value = 'Vue3';
// ref也可以包装对象
const objRef = ref({ count: 0 });
objRef.value.count = 1; // 自动深层响应
// reactive vs ref的选择
// 对象用reactive,基本类型用ref
// 但在组合式函数中,ref更通用(可以统一处理)
// ref的解包
// 在template中,ref自动解包
// <div>{{ count }}</div> 而非 <div>{{ count.value }}</div>
// reactive的局限
const state = reactive({ count: 0 });
// 解构会失去响应性
let { count } = state; // count不再是响应式
count++; // 不会触发更新
// toRefs:将reactive对象的所有属性转为ref
const state2 = reactive({ a: 1, b: 2 });
const { a, b } = toRefs(state2); // a和b是ref,保持响应性
a.value = 10; // 触发更新
// toRef:单个属性
const c = toRef(state2, 'a');
// ref的简化实现
function ref(value) {
return {
get value() {
track(this, 'value');
return value;
},
set value(newVal) {
value = newVal;
trigger(this, 'value');
}
};
}
// isRef和unref
import { isRef, unref } from 'vue';
const r = ref(0);
console.log(isRef(r)); // true
console.log(unref(r)); // 0,如果是ref返回.value,否则返回原值
// shallowRef:浅层响应
const shallow = shallowRef({ count: 0 });
shallow.value = { count: 1 }; // 触发更新
shallow.value.count = 2; // 不触发更新(浅层)
// customRef:自定义ref
function debouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
};
});
}
const debounced = debouncedRef('hello');
reactive和ref是Vue3响应式的两个核心API。reactive用于代理对象,深层响应——嵌套对象也会被代理。ref用于包装任意值(包括基本类型),通过.value访问。reactive的局限是解构会失去响应性,解决方案是toRefs。shallowRef提供浅层响应(只监听.value的变化)。customRef允许自定义依赖收集和触发逻辑。在Composition API中,ref更常用,因为它可以统一处理所有类型,且在组合式函数间传递更方便。
1.3 数组与集合的响应式
import { reactive, effect } from 'vue';
// 数组的响应式
const arr = reactive([1, 2, 3]);
// Vue3正确处理了数组的各种操作
arr.push(4); // 触发更新
arr.pop(); // 触发更新
arr.shift(); // 触发更新
arr.unshift(0); // 触发更新
arr.splice(0, 1); // 触发更新
arr[0] = 10; // 触发更新
arr.length = 5; // 触发更新
// Vue2的数组问题(Vue3已解决)
// Vue2通过重写数组方法实现响应式,但有局限:
// - arr[0] = 10 不触发更新(Vue2需要$set)
// - arr.length = 5 不触发更新
// Map的响应式
const map = reactive(new Map([['a', 1], ['b', 2]]));
map.set('c', 3); // 触发更新
map.delete('a'); // 触发更新
map.clear(); // 触发更新
effect(() => {
console.log('Map size:', map.size); // 监听size变化
});
// Set的响应式
const set = reactive(new Set([1, 2, 3]));
set.add(4); // 触发更新
set.delete(1); // 触发更新
effect(() => {
console.log('Set size:', set.size);
});
// 遍历响应式集合
effect(() => {
for (const [key, value] of map) {
console.log(key, value);
}
});
// Vue3如何处理集合类型
// Proxy默认不拦截Map/Set的方法调用
// Vue3通过自定义handler拦截get,返回包装后的方法
// 模拟Vue3的集合处理
const collectionHandlers = {
get(target, key, receiver) {
// 对Map/Set的方法进行特殊处理
if (key === 'size') {
return Reflect.get(target, 'size', target);
}
if (key === 'get') {
return (k) => {
track(target, k);
return target.get(k);
};
}
if (key === 'add') {
return (value) => {
const result = target.add(value);
trigger(target, value);
return result;
};
}
// ...其他方法
return Reflect.get(target, key, receiver);
}
};
Vue3完全解决了Vue2的数组响应式问题——所有数组操作(包括索引修改和length修改)都能触发更新。Map和Set等集合类型也是完全响应式的——add、delete、clear、size变化都能被追踪。这是通过自定义Proxy handler实现的——对于集合类型,handler会拦截方法调用,在操作前后分别进行track和trigger。这使得Vue3可以无缝处理任何JavaScript数据结构,不再需要Vue2的特殊API(如delete)。
二、computed与watch
2.1 computed计算属性
import { ref, reactive, computed } from 'vue';
// 基本用法
const count = ref(0);
const double = computed(() => count.value * 2);
console.log(double.value); // 0
count.value = 5;
console.log(double.value); // 10
// 可写computed
const firstName = ref('Vue');
const lastName = ref('3');
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ');
}
});
console.log(fullName.value); // "Vue 3"
fullName.value = 'Vue 4';
console.log(firstName.value, lastName.value); // "Vue" "4"
// computed的缓存
const data = ref([1, 2, 3, 4, 5]);
let computeCount = 0;
const sum = computed(() => {
computeCount++;
console.log('Computing sum...');
return data.value.reduce((a, b) => a + b, 0);
});
console.log(sum.value); // Computing... 15
console.log(sum.value); // 15(缓存,不重新计算)
console.log(sum.value); // 15(缓存)
data.value.push(6);
console.log(sum.value); // Computing... 21(依赖变化,重新计算)
// computed依赖多个源
const a = ref(1);
const b = ref(2);
const c = ref(3);
const result = computed(() => a.value + b.value + c.value);
// computed的简化实现
function computed(getter) {
let value;
let dirty = true;
// 使用effect追踪依赖
const runner = effect(() => {
value = getter();
dirty = false;
});
return {
get value() {
if (dirty) {
// 重新计算
runner();
}
return value;
}
};
}
// 嵌套computed
const items = ref([
{ name: 'Apple', price: 5, qty: 2 },
{ name: 'Banana', price: 3, qty: 5 },
]);
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
);
const tax = computed(() => totalPrice.value * 0.1);
const grandTotal = computed(() => totalPrice.value + tax.value);
console.log(totalPrice.value); // 25
console.log(tax.value); // 2.5
console.log(grandTotal.value); // 27.5
// 链式computed(依赖另一个computed)
items.value[0].qty = 10;
console.log(grandTotal.value); // 60 + 6 = 66
computed是Vue3的缓存计算属性。它有两个核心特性:惰性求值(只有访问.value时才计算)和缓存(依赖不变时返回缓存值)。可写computed通过提供get和set函数实现。computed可以嵌套——一个computed可以依赖另一个computed,形成依赖链。computed的缓存特性使得即使多次访问也不会重复计算,只有依赖变化时才重新计算。这在处理昂贵计算时非常有用。
2.2 watch与watchEffect
import { ref, reactive, watch, watchEffect } from 'vue';
// watch:监听特定数据
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count: ${oldValue} -> ${newValue}`);
});
count.value = 1; // 触发: Count: 0 -> 1
count.value = 2; // 触发: Count: 1 -> 2
// watch的选项
watch(count, (newValue, oldValue) => {
console.log('Changed');
}, {
immediate: true, // 立即执行一次
deep: true, // 深度监听
flush: 'post' // 在DOM更新后执行
});
// 监听多个源
const a = ref(1);
const b = ref(2);
watch([a, b], ([newA, newB], [oldA, oldB]) => {
console.log(`a: ${oldA}->${newA}, b: ${oldB}->${newB}`);
});
a.value = 10; // 触发
b.value = 20; // 触发
// 监听reactive对象
const state = reactive({
user: { name: 'Vue', age: 3 },
list: [1, 2, 3]
});
// 监听整个对象(需要deep)
watch(state, (newValue, oldValue) => {
console.log('State changed');
}, { deep: true });
// 监听特定属性
watch(() => state.user.name, (newName, oldName) => {
console.log(`Name: ${oldName} -> ${newName}`);
});
// 监听数组
watch(() => state.list, (newList, oldList) => {
console.log('List changed');
}, { deep: true });
// watchEffect:自动追踪依赖
watchEffect(() => {
console.log(`Count is ${count.value}`); // 自动追踪count
});
count.value = 5; // 触发watchEffect
// watchEffect vs watch
// watchEffect: 自动追踪依赖,不需要指定,立即执行
// watch: 需要指定监听源,惰性执行(除非immediate)
// watchEffect的清理
watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log('Timeout');
}, 1000);
onCleanup(() => {
clearTimeout(timer);
});
});
// watch的清理
watch(count, (newValue, oldValue, onCleanup) => {
const controller = new AbortController();
fetch(`/api/data?count=${newValue}`, { signal: controller.signal })
.then(res => res.json())
.then(data => {
console.log(data);
});
onCleanup(() => {
controller.abort(); // 取消前一个请求
});
});
// 停止监听
const stop = watch(count, (newValue) => {
console.log(newValue);
});
count.value = 10; // 触发
stop(); // 停止监听
count.value = 20; // 不触发
// watchEffect的flush选项
watchEffect(() => {
console.log('DOM:', document.querySelector('#app').innerHTML);
}, {
flush: 'pre' // 默认:在组件更新前执行
// flush: 'post' // 在DOM更新后执行
// flush: 'sync' // 同步执行(不推荐)
});
watch和watchEffect用于监听响应式数据的变化。watch需要明确指定监听源和回调,可以获取新旧值。watchEffect自动追踪依赖,立即执行一次。两者都支持清理函数——用于取消异步操作、清除定时器等。watch的deep选项用于深度监听对象变化。flush选项控制执行时机——pre(默认,组件更新前)、post(DOM更新后)、sync(同步执行)。停止监听通过调用返回的stop函数实现。理解这些选项对于正确处理副作用非常重要。
三、Composition API组织模式
3.1 setup函数与script setup
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<p>Double: {{ double }}</p>
<p>User: {{ user.name }} ({{ user.age }})</p>
</div>
</template>
<script>
import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
export default {
props: {
initialCount: {
type: Number,
default: 0
}
},
emits: ['change'],
setup(props, { emit, attrs, slots, expose }) {
// 响应式状态
const count = ref(props.initialCount);
const user = reactive({ name: 'Vue', age: 3 });
// 计算属性
const double = computed(() => count.value * 2);
// 方法
const increment = () => {
count.value++;
emit('change', count.value);
};
const decrement = () => {
count.value--;
emit('change', count.value);
};
// 侦听器
watch(count, (newValue, oldValue) => {
console.log(`Count changed: ${oldValue} -> ${newValue}`);
});
// 生命周期
onMounted(() => {
console.log('Component mounted');
console.log('Current count:', count.value);
});
onUnmounted(() => {
console.log('Component will unmount');
});
// 暴露给父组件
expose({
count,
reset: () => { count.value = props.initialCount; }
});
// 返回模板需要的内容
return {
count,
double,
user,
increment,
decrement
};
}
};
</script>
<!-- script setup语法糖 -->
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<p>Double: {{ double }}</p>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
// 直接定义,无需return
const count = ref(0);
const double = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
// 生命周期直接调用
onMounted(() => {
console.log('Mounted');
});
// props和emits
const props = defineProps({
initialCount: {
type: Number,
default: 0
}
});
const emit = defineEmits(['change']);
// 暴露给父组件
defineExpose({
count,
reset: () => { count.value = props.initialCount; }
});
</script>
setup函数是Composition API的入口。在setup中定义响应式状态、计算属性、方法和侦听器,最后返回模板需要的内容。script setup是Vue3.2引入的语法糖,简化了setup的写法——不需要return,顶层变量自动暴露给模板。defineProps和defineEmits是编译器宏,不需要导入。defineExpose用于暴露内容给父组件。script setup使得Composition API的使用更加简洁,是推荐的写法。
3.2 组合式函数
import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
// useCounter:计数器逻辑
function useCounter(initialValue = 0, options = {}) {
const count = ref(initialValue);
const { min, max } = options;
const increment = () => {
if (max !== undefined && count.value >= max) return;
count.value++;
};
const decrement = () => {
if (min !== undefined && count.value <= min) return;
count.value--;
};
const reset = () => {
count.value = initialValue;
};
return {
count,
increment,
decrement,
reset
};
}
// 使用
// <script setup>
// const { count, increment, decrement } = useCounter(0, { min: 0, max: 10 });
// </script>
// useMouse:鼠标位置
function useMouse() {
const x = ref(0);
const y = ref(0);
const update = (e) => {
x.value = e.pageX;
y.value = e.pageY;
};
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
return { x, y };
}
// 使用
// const { x, y } = useMouse();
// useFetch:数据获取
function useFetch(url, options = {}) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
const execute = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = await response.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
};
onMounted(execute);
return { data, error, loading, refetch: execute };
}
// 使用
// const { data, loading, error } = useFetch('/api/users');
// useLocalStorage:本地存储
function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key);
const value = ref(stored ? JSON.parse(stored) : defaultValue);
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
return value;
}
// 使用
// const theme = useLocalStorage('theme', 'light');
// useDebounce:防抖
function useDebounce(value, delay = 300) {
const debounced = ref(value.value);
let timer;
watch(value, (newValue) => {
clearTimeout(timer);
timer = setTimeout(() => {
debounced.value = newValue;
}, delay);
});
return debounced;
}
// 使用
// const searchTerm = ref('');
// const debouncedTerm = useDebounce(searchTerm, 500);
// useEventListener:事件监听
function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback);
});
onUnmounted(() => {
target.removeEventListener(event, callback);
});
}
// 使用
// useEventListener(window, 'resize', () => console.log('Resized'));
// usePagination:分页
function usePagination(totalItems, pageSize = 10) {
const currentPage = ref(1);
const totalPages = computed(() => Math.ceil(totalItems.value / pageSize));
const goTo = (page) => {
currentPage.value = Math.max(1, Math.min(page, totalPages.value));
};
const nextPage = () => goTo(currentPage.value + 1);
const prevPage = () => goTo(currentPage.value - 1);
return {
currentPage,
totalPages,
nextPage,
prevPage,
goTo
};
}
// 组合多个组合式函数
function useUserPosts(userId) {
const { data: user, loading: userLoading } = useFetch(`/api/users/${userId}`);
const { data: posts, loading: postsLoading } = useFetch(
computed(() => user.value ? `/api/users/${user.value.id}/posts` : null)
);
return {
user,
posts,
loading: computed(() => userLoading.value || postsLoading.value)
};
}
// 使用
// const { user, posts, loading } = useUserPosts(1);
组合式函数(Composables)是Vue3逻辑复用的核心模式。命名以use开头,返回响应式状态和方法。useCounter封装计数逻辑,useMouse追踪鼠标位置,useFetch处理数据获取,useLocalStorage持久化数据,useDebounce实现防抖,useEventListener封装事件监听,usePagination处理分页。这些函数可以在任意组件中复用,且不增加组件层级(与mixin不同)。组合式函数可以嵌套——useUserPosts组合了两个useFetch。这种模式使得Vue3的逻辑复用远比Vue2的mixin清晰和安全——没有命名冲突、来源明确。
3.3 provide与inject
import { provide, inject, ref, reactive, readonly } from 'vue';
// 父组件提供数据
// Parent.vue
import { provide, ref, readonly } from 'vue';
export default {
setup() {
const theme = ref('light');
const user = reactive({ name: 'Vue', age: 3 });
// 提供可变数据(不推荐)
// provide('theme', theme);
// 提供只读数据(推荐)
provide('theme', readonly(theme));
provide('user', readonly(user));
// 提供修改方法
const setTheme = (newTheme) => {
theme.value = newTheme;
};
provide('setTheme', setTheme);
return { theme, setTheme };
}
};
// 子组件注入数据
// Child.vue
import { inject } from 'vue';
export default {
setup() {
const theme = inject('theme', 'light'); // 第二个参数是默认值
const user = inject('user', null);
const setTheme = inject('setTheme', () => {});
// theme是只读的,不能直接修改
// theme.value = 'dark'; // 报错
// 通过方法修改
const toggleTheme = () => {
setTheme(theme.value === 'light' ? 'dark' : 'light');
};
return { theme, user, toggleTheme };
}
};
// 使用Symbol作为key(推荐)
import { provide, inject, InjectionKey } from 'vue';
const ThemeKey = Symbol('theme') as InjectionKey<Ref<string>>;
const UserKey = Symbol('user') as InjectionKey<{ name: string; age: number }>;
// 提供时使用key
provide(ThemeKey, theme);
// 注入时有类型提示
const theme = inject(ThemeKey); // 类型为 Ref<string> | undefined
// 封装provide/inject为组合式函数
function useThemeProvider() {
const theme = ref('light');
provide(ThemeKey, readonly(theme));
const setTheme = (newTheme) => {
theme.value = newTheme;
};
provide('setTheme', setTheme);
return { theme, setTheme };
}
function useTheme() {
const theme = inject(ThemeKey, ref('light'));
const setTheme = inject('setTheme', () => {});
return { theme, setTheme };
}
// 使用
// 父组件: const { theme, setTheme } = useThemeProvider();
// 子组件: const { theme, setTheme } = useTheme();
provide/inject是Vue3跨层级传递数据的方式。最佳实践包括:使用readonly包装提供的数据防止子组件直接修改、提供修改方法而非可变数据、使用Symbol作为key避免命名冲突、使用InjectionKey提供类型安全。封装为组合式函数(useThemeProvider/useTheme)可以隐藏实现细节,使API更加清晰。provide/inject替代了Vue2的event bus,适用于主题、用户信息、国际化等全局状态的传递。
四、响应式系统的高级特性
4.1 shallowReactive与shallowRef
import { shallowReactive, shallowRef, triggerRef, customRef } from 'vue';
// shallowReactive:只有第一层是响应式
const state = shallowReactive({
count: 0,
nested: { value: 1 }
});
// 修改第一层触发更新
state.count = 1; // 触发更新
// 修改嵌套不触发更新
state.nested.value = 2; // 不触发更新
state.nested = { value: 3 }; // 触发更新(替换整个对象)
// 使用场景:大对象只需浅层响应
const bigData = shallowReactive({
list: [], // list本身变化触发,list内容变化不触发
meta: {} // 同上
});
// 手动触发更新
const state2 = shallowRef({ count: 0 });
state2.value.count = 1; // 不触发更新
triggerRef(state2); // 手动触发
// shallowRef:只监听.value的变化
const shallow = shallowRef({ count: 0 });
shallow.value = { count: 1 }; // 触发更新
shallow.value.count = 2; // 不触发更新
// 使用场景
// 1. 大型数据结构(避免深层代理的性能开销)
// 2. 第三方库对象(如DOM元素、Canvas等)
// 3. 只需要顶层响应的场景
4.2 readonly与toRaw
import { readonly, toRaw, markRaw, isReactive, isReadonly } from 'vue';
// readonly:创建只读代理
const original = reactive({ count: 0 });
const readonlyCopy = readonly(original);
console.log(readonlyCopy.count); // 0
// readonlyCopy.count = 1; // 警告:Set operation on key failed
// 原始对象变化,只读副本也变化
original.count = 10;
console.log(readonlyCopy.count); // 10
// toRaw:获取原始对象
const raw = toRaw(original);
console.log(raw === original); // false(original是Proxy)
console.log(raw.count); // 10
// markRaw:标记对象不被转为响应式
const config = markRaw({
canvas: document.createElement('canvas'),
// 不希望被Proxy包装
});
const state = reactive({ config });
// state.config是原始对象,不是Proxy
// 检查函数
console.log(isReactive(original)); // true
console.log(isReactive(readonlyCopy)); // false(是readonly不是reactive)
console.log(isReadonly(readonlyCopy)); // true
console.log(isReadonly(original)); // false
// 使用场景
// readonly: 传递给子组件时防止修改
// toRaw: 需要原始对象时(如传递给非Vue库)
// markRaw: 第三方库对象、DOM元素等不应该被代理的对象
4.3 effectScope
import { effectScope, watchEffect, ref } from 'vue';
// effectScope:管理一组effect的生命周期
function useFeature() {
const scope = effectScope();
const count = ref(0);
const doubled = ref(0);
scope.run(() => {
// 这里的effect会自动被scope管理
watchEffect(() => {
doubled.value = count.value * 2;
});
watchEffect(() => {
console.log('Count:', count.value);
});
// 更多effect...
});
// 停止所有effect
const stop = () => scope.stop();
return { count, doubled, stop };
}
// 使用
const { count, doubled, stop } = useFeature();
count.value = 5; // 触发effect
stop(); // 停止所有effect
count.value = 10; // 不触发effect
// 组件中的自动scope
// Vue组件的setup函数本身就在一个effectScope中
// 组件卸载时,所有effect自动停止
// 组合式函数中使用scope
function useMousePosition() {
const scope = effectScope(true); // 隔离的scope
const x = ref(0);
const y = ref(0);
scope.run(() => {
const update = (e) => {
x.value = e.clientX;
y.value = e.clientY;
};
window.addEventListener('mousemove', update);
// 清理函数也会被scope管理
scope.run(() => {
onScopeDispose(() => {
window.removeEventListener('mousemove', update);
});
});
});
return { x, y, dispose: () => scope.stop() };
}
effectScope用于管理一组effect的集体生命周期。在组合式函数中,使用scope可以确保所有创建的watch/watchEffect能被统一停止。Vue组件的setup函数本身就运行在一个scope中,组件卸载时自动停止所有effect。对于需要在组件外使用的组合式函数,手动管理scope很重要——确保不再使用时清理所有effect,避免内存泄漏。
五、性能优化
5.1 减少不必要的响应式
import { ref, reactive, shallowRef, shallowReactive, markRaw, computed } from 'vue';
// 不好的做法:所有数据都响应式
function bad() {
// 大列表完全响应式,性能差
const hugeList = reactive(
Array.from({ length: 10000 }, (_, i) => ({ id: i, data: new Array(1000) }))
);
}
// 好的做法:按需响应式
function good() {
// 列表本身响应式,元素内容不响应式
const hugeList = shallowRef(
Array.from({ length: 10000 }, (_, i) => ({ id: i, data: new Array(1000) }))
);
// 只让需要的部分响应式
const selected = ref(null);
// 使用计算属性派生数据
const filtered = computed(() => {
if (!selected.value) return [];
return hugeList.value.filter(item => item.category === selected.value);
});
}
// markRaw:标记不响应的对象
function useThirdParty() {
const chart = markRaw(new ChartJS()); // 第三方库实例
const state = reactive({ chart, data: [] });
// chart不会被代理,data会
}
// 大数据使用shallowReactive
function useTable() {
const tableData = shallowReactive({
rows: [], // 只监听数组引用变化
columns: [], // 只监听数组引用变化
pagination: reactive({ // 分页信息完全响应式
page: 1,
size: 10,
total: 0
})
});
return { tableData };
}
// v-once和v-memo
// v-once:只渲染一次
// <div v-once>{{ expensiveComputation() }}</div>
// v-memo:条件缓存
// <div v-memo="[item.id, item.selected]">
// {{ item.name }}
// </div>
// 只有item.id或item.selected变化时才重新渲染
5.2 计算属性优化
import { computed, ref } from 'vue';
// 避免在computed中创建新对象
function badComputed() {
const items = ref([{ id: 1, name: 'A' }, { id: 2, name: 'B' }]);
// 每次都创建新数组
const filtered = computed(() => {
return items.value.map(item => ({ ...item, processed: true }));
});
}
function goodComputed() {
const items = ref([{ id: 1, name: 'A' }, { id: 2, name: 'B' }]);
// 直接返回过滤结果,不创建新对象
const filtered = computed(() => {
return items.value.filter(item => item.id > 0);
});
}
// 分割大型computed
function splitComputed() {
const data = ref([...]); // 大量数据
// 不好:一个大computed
const processed = computed(() => {
// 大量计算
});
// 好:分割成多个小computed
const filtered = computed(() => data.value.filter(/* ... */));
const sorted = computed(() => [...filtered.value].sort(/* ... */));
const paginated = computed(() => sorted.value.slice(/* ... */));
}
// 使用缓存
function useCache() {
const cache = new Map();
const data = ref(null);
const getCached = computed(() => {
const key = JSON.stringify(data.value);
if (cache.has(key)) {
return cache.get(key);
}
const result = expensiveCompute(data.value);
cache.set(key, result);
return result;
});
}
5.3 列表渲染优化
<template>
<!-- 使用key优化列表 -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- v-memo:选择性更新 -->
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id, item.updated]"
>
{{ item.name }}
<span>{{ item.detail }}</span>
</div>
<!-- 虚拟列表(使用第三方库如vue-virtual-scroller) -->
<RecycleScroller
:items="hugeList"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>
</template>
<script setup>
import { ref, computed } from 'vue';
// 分页加载
const allItems = ref([]);
const currentPage = ref(1);
const pageSize = 50;
const visibleItems = computed(() => {
const start = (currentPage.value - 1) * pageSize;
return allItems.value.slice(start, start + pageSize);
});
// 懒加载
const loadMore = async () => {
const newItems = await fetchMore();
allItems.value.push(...newItems);
};
</script>
Vue3的性能优化主要围绕减少不必要的响应式追踪和渲染。shallowReactive/shallowRef用于大对象,只追踪浅层变化。markRaw标记第三方库对象不响应。computed的缓存特性避免重复计算。v-once和v-memo控制渲染粒度。虚拟列表处理超长列表。合理拆分computed避免大型计算。这些优化在处理大数据量和频繁更新时至关重要。
六、TypeScript集成
import { ref, reactive, computed, PropType, defineComponent } from 'vue';
// ref的类型
const count = ref<number>(0);
const name = ref<string>('Vue');
// reactive的类型
interface User {
id: number;
name: string;
age: number;
}
const user = reactive<User>({ id: 1, name: 'Vue', age: 3 });
// computed的类型(自动推断)
const double = computed(() => count.value * 2); // ComputedRef<number>
// 组合式函数的类型
function useCounter(initial: number = 0) {
const count = ref<number>(initial);
const increment = () => count.value++;
const decrement = () => count.value--;
return { count, increment, decrement };
}
// 使用组合式函数
const { count: myCount, increment: inc } = useCounter(10);
// myCount: Ref<number>
// defineComponent的类型
export default defineComponent({
props: {
title: String,
count: {
type: Number as PropType<number>,
required: true
},
items: {
type: Array as PropType<User[]>,
default: () => []
}
},
setup(props) {
// props有类型提示
console.log(props.title); // string | undefined
console.log(props.count); // number
const doubled = computed(() => props.count * 2);
return { doubled };
}
});
// script setup的TypeScript
// <script setup lang="ts">
import { ref, computed } from 'vue';
interface Props {
msg: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0
});
const emit = defineEmits<{
(e: 'change', value: number): void;
(e: 'update', payload: { id: number; name: string }): void;
}>();
const doubled = computed(() => props.count * 2);
const handleClick = () => {
emit('change', props.count + 1);
};
// </script>
// provide/inject的类型安全
import { inject, provide, InjectionKey, Ref } from 'vue';
interface ThemeContext {
theme: Ref<string>;
setTheme: (theme: string) => void;
}
const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme');
// 提供方
const theme = ref('light');
provide(ThemeKey, {
theme,
setTheme: (t: string) => { theme.value = t; }
});
// 注入方(有类型提示)
const ctx = inject(ThemeKey);
if (ctx) {
ctx.theme.value; // string
ctx.setTheme('dark'); // 类型安全
}
Vue3对TypeScript有一流支持。ref/reactive/computed都支持泛型类型标注。组合式函数返回类型自动推断。defineProps和defineEmits支持类型声明。provide/inject通过InjectionKey实现类型安全。script setup + TypeScript是Vue3的推荐开发方式——获得完整的类型提示和编译时检查。
七、总结与最佳实践
Vue3的Composition API和响应式系统是Vue框架的重大革新。基于Proxy的响应式系统解决了Vue2的所有限制,Composition API提供了更好的逻辑组织和复用能力。理解响应式原理——track/trigger、依赖收集、触发更新——是正确使用Vue3的基础。
最佳实践总结如下。第一,理解Proxy响应式——Vue3通过Proxy拦截对象操作实现响应式,解决了Vue2的数组和新属性问题。第二,reactive用于对象,ref用于基本类型——但ref也可以包装对象,且在组合式函数中更通用。第三,computed有缓存——依赖不变时返回缓存值,避免重复计算。第四,watch指定监听源,watchEffect自动追踪——选择适合场景的API。第五,组合式函数是逻辑复用的核心——命名以use开头,返回响应式状态和方法。第六,provide/inject用于跨层级传递——使用readonly包装、提供修改方法、使用Symbol key。第七,性能优化——shallowReactive/shallowRef减少深层追踪,markRaw标记不响应对象,v-memo控制渲染粒度。第八,TypeScript集成——使用类型标注获得完整的类型安全。第九,script setup是推荐写法——简洁且功能完整。第十,effectScope管理effect生命周期——在组件外使用组合式函数时尤其重要。Vue3的Composition API虽然学习曲线比Options API陡峭,但一旦掌握,它能显著提升代码的可维护性和复用性。随着Vue3生态的持续发展——Vite、Pinia、Vue Router等工具的完善——Composition API已经成为Vue开发的标准模式。深入理解响应式系统和Composition API的设计原理,是现代Vue开发的必备技能。
- 点赞
- 收藏
- 关注作者
评论(0)