Vue3 Composition API与响应式系统深度解析从Proxy到组件逻辑复用的工程实践

举报
柠檬🍋 发表于 2026/08/29 23:16:05 2026/08/29
【摘要】 Vue3 Composition API与响应式系统深度解析从Proxy到组件逻辑复用的工程实践Vue3的Composition API是Vue框架自2.x以来最重要的架构变革。与Vue2的Options API将组件逻辑分散在data、methods、computed等选项中不同,Composition API允许开发者按功能组织代码,将相关逻辑集中在一起。这一变革的基础是Vue3全新的...

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(如setset、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开发的必备技能。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。