Vue3-重新学习-20260407

6 阅读6分钟

Vue3

一、Vue3 基础特性

1. Vue3 新特性与性能优化

问题:Vue3 新增特性有哪些?Vue3 与 Vue2 的区别是什么?Vue3 的性能提升体现在哪些方面?Vue3 的 TypeScript 支持如何?

答案

Vue3 主要新特性

  1. 组合式 API(Composition API):更好的逻辑复用和代码组织
  2. 响应式系统重构:使用 Proxy 替代 Object.defineProperty
  3. 更好的 TypeScript 支持:源码使用 TypeScript 重写
  4. Tree-shaking 支持:按需引入,减小打包体积
  5. 新组件:Fragment、Teleport、Suspense
  6. 自定义渲染器 API:支持自定义渲染目标
  7. 编译器优化:静态提升、补丁标记、事件监听缓存

Vue3 vs Vue2 核心区别

特性Vue2Vue3
响应式系统Object.definePropertyProxy
API 风格Options APIOptions API + Composition API
TypeScript需要额外类型定义原生支持
打包体积较大更小(Tree-shaking)
性能良好更好(编译时优化)
代码组织按选项组织按功能组织(Composition API)
组件单根节点Fragment(多根节点)

性能提升体现

  1. 打包体积优化

    • 核心库体积减少约 41%
    • Tree-shaking 支持,未使用 API 不打包
    • 最小 gzip 大小约 10KB
  2. 渲染性能优化

    • 静态提升:静态节点提升到渲染函数外部
    • 补丁标记:动态节点标记,仅更新必要部分
    • 事件监听缓存:缓存事件处理函数
    • SSR 优化:服务端渲染性能提升 2-3 倍
  3. 内存优化

    • Proxy 响应式减少内存占用
    • 更好的垃圾回收

TypeScript 支持

  1. 源码使用 TypeScript 重写
  2. 完整的类型定义
  3. 组合式 API 类型推断良好
  4. 编译器支持 TypeScript
  5. 与 IDE 集成更好(VSCode Volar 插件)

Composition API 示例

import { ref, computed, onMounted } from 'vue';

export default {
  setup() {
    // 响应式数据
    const count = ref(0);
    const doubleCount = computed(() => count.value * 2);
    
    // 方法
    const increment = () => {
      count.value++;
    };
    
    // 生命周期
    onMounted(() => {
      console.log('组件挂载');
    });
    
    // 返回模板可用数据
    return {
      count,
      doubleCount,
      increment
    };
  }
}

新组件特性

  1. Fragment:支持多根节点模板
  2. Teleport:将组件渲染到 DOM 其他位置
  3. Suspense:异步组件加载状态处理

补充说明

  • Vue3 完全兼容 Vue2 语法(Options API)
  • 渐进式迁移策略,可逐步采用新特性
  • 配套工具链更新:Vue CLI → Vite,Vuex → Pinia

二、Vue3 响应式系统

2. Proxy 响应式与响应式 API

问题:Vue3 的响应式原理是什么?Vue3 如何使用 Proxy 实现响应式?reactive、ref、toRef、toRefs 函数如何使用?reactive 与 ref 的区别是什么?computed、watch、watchEffect 函数如何使用?watch 与 watchEffect 的区别是什么?

答案

Proxy 响应式原理

// Vue3 响应式核心实现
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);  // 依赖收集
      return typeof res === 'object' ? reactive(res) : res;  // 递归代理
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, key);  // 触发更新
      }
      return result;
    },
    deleteProperty(target, key) {
      const hasKey = key in target;
      const result = Reflect.deleteProperty(target, key);
      if (hasKey) {
        trigger(target, key);
      }
      return result;
    }
  });
}

Proxy vs Object.defineProperty 优势

  1. 支持数组索引和 length 修改
  2. 支持对象新增/删除属性
  3. 性能更好:无需递归遍历对象
  4. 支持更多操作拦截:has、ownKeys、deleteProperty 等

reactive 函数

  • 创建响应式对象(深层次响应式)
  • 仅支持对象类型(Object、Array、Map、Set)
import { reactive } from 'vue';

const state = reactive({
  count: 0,
  user: {
    name: 'Alice',
    age: 20
  },
  items: ['item1', 'item2']
});

// 修改响应式数据
state.count++;
state.user.age = 21;
state.items.push('item3');

ref 函数

  • 创建响应式引用(任何类型)
  • 通过 .value 访问/修改值
  • 模板中自动解包(无需 .value)
import { ref } from 'vue';

const count = ref(0);       // 基本类型
const user = ref({          // 对象类型
  name: 'Alice',
  age: 20
});

// 访问/修改
console.log(count.value);   // 0
count.value = 1;            // 修改

// 模板中自动解包
// <div>{{ count }}</div>  无需 .value

reactive vs ref 对比

特性reactiveref
创建对象reactive({...})ref(value)
访问方式直接访问属性.value 访问
模板使用直接使用自动解包
类型支持仅对象/数组任意类型
响应式深层次深层次响应式浅层(但对象内部自动转为响应式)
适用场景复杂状态对象简单值、模板引用

toRef 与 toRefs

import { reactive, toRef, toRefs } from 'vue';

const state = reactive({
  count: 0,
  name: 'Alice'
});

// toRef: 将 reactive 对象的某个属性转为 ref
const countRef = toRef(state, 'count');
countRef.value++;  // state.count 也会更新

// toRefs: 将 reactive 对象所有属性转为 ref
const { count, name } = toRefs(state);
// 解构后仍保持响应式

computed 计算属性

import { ref, computed } from 'vue';

const count = ref(0);

// 只读计算属性
const doubleCount = computed(() => count.value * 2);

// 可写计算属性
const writableComputed = computed({
  get: () => count.value * 2,
  set: (newValue) => {
    count.value = newValue / 2;
  }
});

watch 侦听器

import { ref, watch, reactive } from 'vue';

const count = ref(0);
const state = reactive({ user: { name: 'Alice' } });

// 基本用法
watch(count, (newVal, oldVal) => {
  console.log(`count 变化: ${oldVal}${newVal}`);
});

// 监听 reactive 对象的属性
watch(() => state.user.name, (newVal, oldVal) => {
  console.log(`用户名变化: ${oldVal}${newVal}`);
});

// 深度监听
watch(
  () => state.user,
  (newVal, oldVal) => {
    // 深度监听对象
  },
  { deep: true }
);

// 立即执行
watch(count, callback, { immediate: true });

// 监听多个源
watch([count, () => state.user.name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('多个值变化');
});

watchEffect 响应式副作用

  • 自动追踪依赖,依赖变化时重新执行
  • 立即执行一次
import { ref, watchEffect } from 'vue';

const count = ref(0);

// 自动追踪 count 依赖
watchEffect(() => {
  console.log(`count 值: ${count.value}`);
});

// 清理副作用
const stop = watchEffect((onInvalidate) => {
  // 执行副作用
  
  // 清理函数(依赖变化或停止监听时调用)
  onInvalidate(() => {
    console.log('清理副作用');
  });
});

// 停止监听
stop();

watch vs watchEffect 对比

特性watchwatchEffect
追踪依赖显式指定监听源自动追踪
初始执行需要设置 immediate: true立即执行
新旧值提供新旧值不提供新旧值
适用场景监听特定数据变化响应式副作用
清理函数不支持支持 onInvalidate

其他响应式 API

import {
  readonly,
  shallowReactive,
  shallowReadonly,
  isProxy,
  isReactive,
  isReadonly,
  markRaw,
  toRaw
} from 'vue';

// readonly: 创建只读响应式对象
const readOnlyState = readonly(state);

// shallowReactive: 浅层响应式(只响应第一层)
const shallowState = shallowReactive({
  nested: { count: 0 }  // nested 不是响应式的
});

// markRaw: 标记对象不被转为响应式
const nonReactiveObj = markRaw({ id: 1 });

// toRaw: 获取原始对象(非响应式)
const rawState = toRaw(state);

补充说明

  1. 响应式最佳实践

    • 优先使用 ref(更灵活)
    • 复杂对象使用 reactive + toRefs 解构
    • 避免在 reactive 中使用 ref(除非必要)
  2. 性能考虑

    • shallowReactive/shallowReadonly 减少性能开销
    • markRaw 避免不必要的响应式转换
    • 合理使用 watch 和 watchEffect
  3. TypeScript 类型推断

    • ref 自动推断类型:ref<number>(0)
    • reactive 类型推断正确
    • computed 返回 Ref 类型

三、组合式 API

3. setup 函数与组合逻辑复用

问题:什么是组合式 API?setup 函数如何使用?setup 函数的参数有哪些?setup 函数的返回值是什么?setup 的执行时机是什么?如何选择 ref 与 reactive?组合式 API 中的生命周期钩子如何使用?provide/inject 在组合式 API 中如何使用?

答案

组合式 API 概念

  • 基于函数组合的 API 设计
  • 更好的逻辑复用和代码组织
  • 替代 Options API 的 mixins 和混入

setup 函数基本用法

import { ref, onMounted } from 'vue';

export default {
  props: ['title'],
  emits: ['update-title'],
  
  setup(props, context) {
    // 响应式数据
    const count = ref(0);
    const message = ref('Hello');
    
    // 计算属性
    const doubleCount = computed(() => count.value * 2);
    
    // 方法
    const increment = () => {
      count.value++;
    };
    
    const updateTitle = () => {
      context.emit('update-title', '新标题');
    };
    
    // 生命周期
    onMounted(() => {
      console.log('组件挂载');
    });
    
    // 暴露给模板
    return {
      count,
      message,
      doubleCount,
      increment,
      updateTitle
    };
  }
};

setup 函数参数

  1. props:父组件传递的 props(响应式)
  2. context:上下文对象
    • attrs:非 props 属性(类似 $attrs)
    • slots:插槽(类似 $slots)
    • emit:触发事件(类似 $emit)
    • expose:暴露组件实例方法

setup 返回值

  • 返回对象:属性合并到组件实例,模板中可用
  • 返回渲染函数:直接渲染,忽略模板
// 返回渲染函数
import { h, ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    
    return () => h('div', [
      h('span', `Count: ${count.value}`),
      h('button', { onClick: () => count.value++ }, '增加')
    ]);
  }
};

setup 执行时机

  • beforeCreate 之前执行
  • this 不可用(组件实例未创建)
  • 无法访问 Options API 中的 data、methods 等

ref vs reactive 选择

  1. 使用 ref

    • 基本类型值
    • 需要模板自动解包
    • 需要响应式引用(任意类型)
    • 需要传递到函数中(保持响应式)
  2. 使用 reactive

    • 复杂状态对象
    • 多个相关属性组合
    • 需要深层次响应式
// 推荐模式:reactive + toRefs
import { reactive, toRefs } from 'vue';

function useUser() {
  const state = reactive({
    name: '',
    age: 0,
    email: ''
  });
  
  const updateName = (newName) => {
    state.name = newName;
  };
  
  return {
    ...toRefs(state),  // 解构后保持响应式
    updateName
  };
}

组合式 API 生命周期钩子

Options APIComposition API
beforeCreate❌ 不需要(setup 代替)
created❌ 不需要(setup 代替)
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue';

export default {
  setup() {
    onBeforeMount(() => {
      console.log('挂载前');
    });
    
    onMounted(() => {
      console.log('挂载后');
    });
    
    onBeforeUnmount(() => {
      console.log('卸载前');
    });
    
    onUnmounted(() => {
      console.log('卸载后');
    });
  }
};

provide/inject 组合式 API

// 祖先组件
import { provide, ref } from 'vue';

export default {
  setup() {
    const theme = ref('dark');
    
    // 提供数据
    provide('theme', theme);
    provide('updateTheme', (newTheme) => {
      theme.value = newTheme;
    });
    
    return { theme };
  }
};

// 后代组件
import { inject } from 'vue';

export default {
  setup() {
    // 注入数据(带默认值)
    const theme = inject('theme', 'light');
    const updateTheme = inject('updateTheme', () => {});
    
    return { theme, updateTheme };
  }
};

useAttrs 和 useSlots

import { useAttrs, useSlots } from 'vue';

export default {
  setup(props, context) {
    // useAttrs: 获取 attrs(非 props 属性)
    const attrs = useAttrs();
    console.log(attrs.class, attrs.style);
    
    // useSlots: 获取插槽
    const slots = useSlots();
    console.log(slots.default, slots.header);
    
    return {};
  }
};

组合函数(Composable)

// useCounter.js
import { ref, computed } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  
  const double = computed(() => count.value * 2);
  
  const increment = () => {
    count.value++;
  };
  
  const decrement = () => {
    count.value--;
  };
  
  const reset = () => {
    count.value = initialValue;
  };
  
  return {
    count,
    double,
    increment,
    decrement,
    reset
  };
}

// 组件中使用
import { useCounter } from './useCounter';

export default {
  setup() {
    const { count, double, increment } = useCounter(0);
    
    return {
      count,
      double,
      increment
    };
  }
};

组合函数最佳实践

  1. 命名约定:以 use 开头(如 useFetchuseLocalStorage
  2. 参数处理:支持配置选项
  3. 返回值:返回 ref 或 reactive 对象
  4. 副作用管理:在组合函数中处理清理
  5. 类型定义:为 TypeScript 提供完整类型

补充说明

  1. 组合式 API 优势

    • 更好的逻辑复用(替代 mixins)
    • 更灵活的代码组织(按功能而非选项)
    • 更好的 TypeScript 支持
    • 更小的打包体积(Tree-shaking)
  2. 迁移策略

    • 新项目直接使用 Composition API
    • 现有项目逐步迁移(新组件使用 Composition API)
    • Options API 仍可用,两者可共存
  3. 性能考虑

    • setup 函数只执行一次
    • 响应式数据创建在 setup 外部
    • 避免在渲染函数中创建新对象

四、Vue3 组件系统

4. 组件定义与使用

问题:Vue3 组件的使用方法是什么?defineComponent 函数的作用是什么?组件的 props 与 emits 如何使用?组件的 slots 与 attrs 如何使用?组件的 expose 如何使用?递归组件与动态组件如何使用?异步组件如何使用?组件中多个 v-model 如何使用?

答案

defineComponent 函数

  • 提供 TypeScript 类型推断
  • 对 Options API 和 Composition API 都适用
import { defineComponent } from 'vue';

// Options API
export default defineComponent({
  props: {
    title: String
  },
  data() {
    return { count: 0 };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
});

// Composition API
export default defineComponent({
  props: {
    title: String
  },
  setup(props) {
    const count = ref(0);
    
    return { count };
  }
});

props 与 emits

<!-- 子组件 -->
<script setup lang="ts">
// 使用 defineProps 和 defineEmits
const props = defineProps<{
  title: string;
  count?: number;
}>();

// 带默认值的 props
const propsWithDefaults = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
});

// 定义 emits
const emit = defineEmits<{
  (e: 'update:title', value: string): void;
  (e: 'increment', value: number): void;
}>();

// 触发事件
const updateTitle = () => {
  emit('update:title', '新标题');
  emit('increment', 1);
};
</script>

slots 与 attrs

<!-- 子组件 -->
<template>
  <div class="wrapper">
    <!-- 默认插槽 -->
    <slot></slot>
    
    <!-- 具名插槽 -->
    <slot name="header"></slot>
    
    <!-- 作用域插槽 -->
    <slot name="item" :item="itemData"></slot>
    
    <!-- 透传 attrs -->
    <child v-bind="$attrs"></child>
  </div>
</template>

<script setup>
import { useAttrs, useSlots } from 'vue';

// 在脚本中访问
const attrs = useAttrs();
const slots = useSlots();

console.log(attrs.class);  // 非 props 属性
console.log(slots.default); // 插槽函数
</script>

expose 暴露组件方法

<!-- 子组件 -->
<script setup>
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
  count.value++;
};

// 暴露给父组件的方法
defineExpose({
  increment,
  getCount: () => count.value
});
</script>

<!-- 父组件 -->
<script setup>
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';

const childRef = ref();

onMounted(() => {
  // 调用子组件暴露的方法
  childRef.value.increment();
  console.log(childRef.value.getCount());
});
</script>

<template>
  <ChildComponent ref="childRef" />
</template>

递归组件

<!-- TreeItem.vue -->
<script setup>
defineProps({
  item: Object
});
</script>

<template>
  <li>
    {{ item.name }}
    <ul v-if="item.children">
      <!-- 递归调用自身 -->
      <TreeItem
        v-for="child in item.children"
        :key="child.id"
        :item="child"
      />
    </ul>
  </li>
</template>

动态组件

<script setup>
import { shallowRef } from 'vue';
import Home from './Home.vue';
import About from './About.vue';
import Contact from './Contact.vue';

const currentComponent = shallowRef(Home);
const tabs = [
  { name: 'Home', component: Home },
  { name: 'About', component: About },
  { name: 'Contact', component: Contact }
];

const switchTab = (tab) => {
  currentComponent.value = tab.component;
};
</script>

<template>
  <div>
    <button
      v-for="tab in tabs"
      :key="tab.name"
      @click="switchTab(tab)"
    >
      {{ tab.name }}
    </button>
    
    <KeepAlive>
      <component :is="currentComponent" />
    </KeepAlive>
  </div>
</template>

异步组件

// 1. 基本异步组件
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
  import('./MyComponent.vue')
);

// 2. 带配置的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./MyComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,  // 延迟显示 loading
  timeout: 3000,  // 超时时间
  suspensible: false  // 是否使用 Suspense
});

// 3. 组合式 API 中使用
export default {
  components: {
    AsyncComponent
  },
  setup() {
    return {};
  }
};

v-model 多绑定

<!-- 子组件 -->
<script setup>
defineProps({
  firstName: String,
  lastName: String
});

defineEmits(['update:firstName', 'update:lastName']);
</script>

<template>
  <input
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

<!-- 父组件 -->
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

const firstName = ref('张');
const lastName = ref('三');
</script>

<template>
  <!-- 多个 v-model -->
  <CustomInput
    v-model:first-name="firstName"
    v-model:last-name="lastName"
  />
  
  <p>全名: {{ firstName }} {{ lastName }}</p>
</template>

Teleport 组件

<!-- Modal.vue -->
<template>
  <!-- 将内容渲染到 body 元素 -->
  <Teleport to="body">
    <div class="modal">
      <slot></slot>
    </div>
  </Teleport>
</template>

<!-- 使用 -->
<template>
  <button @click="showModal = true">显示模态框</button>
  
  <Modal v-if="showModal">
    <h2>标题</h2>
    <p>内容</p>
    <button @click="showModal = false">关闭</button>
  </Modal>
</template>

Suspense 组件

<!-- 异步组件 -->
<script setup>
const data = await fetchData();  // 异步操作
</script>

<template>
  <div>{{ data }}</div>
</template>

<!-- 父组件 -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

Fragment 多根节点

<!-- Vue2: 需要包裹元素 -->
<template>
  <div>
    <header></header>
    <main></main>
  </div>
</template>

<!-- Vue3: 支持多根节点 -->
<template>
  <header></header>
  <main></main>
  <footer></footer>
</template>

补充说明

  1. 组件设计原则

    • 单一职责:一个组件只做一件事
    • 可复用性:通过 props 和插槽定制
    • 可维护性:清晰的接口和文档
    • 性能考虑:合理使用异步组件和 KeepAlive
  2. TypeScript 集成

    • 使用 defineComponent 获得完整类型推断
    • 为 props 和 emits 提供类型定义
    • 使用泛型定义组件属性
  3. 性能优化

    • 合理使用 shallowRef 和 shallowReactive
    • 避免不必要的组件重新渲染
    • 使用 KeepAlive 缓存组件状态
    • 异步组件减少初始加载体积

由于Vue3答案内容较多,这里只展示了前4个部分的核心内容,完整答案包含响应式API、组合式函数、新组件特性、路由、状态管理、性能优化等全部60个问题的详细解答。