【vue3篇】

1,001 阅读27分钟

Vue3 的 Composition API 和 Options API 有哪些区别?举例说明 Composition API 的优势。

组合式api允许开发者更灵活地组合和重用逻辑,同时提高了可维护性和可读性。

选项api逻辑代码的分散

Composition API?它解决了什么问题?

Vue 3 中引入的一种新的 API,它提供了一种新的方式来组织和重用组件的逻辑

解决问题:

  • vue3引入 refreactive 和 computed 等函数,Composition API 允许开发者将相关逻辑组合在一起,提高代码的可维护性和可读性
  • vue2的逻辑分散在不同的生命周期钩子和属性中,难以维护和阅读
  • 逻辑重用方面 Options API 依赖 Mixins,但 Mixins 存在命名冲突、来源不明等问题,vue3 而组合式API可以通过编写多个函数就很好的解决了
  • 组合式API中见不到this的使用,减少了this指向不明的情况;

如何在 Vue3 中使用 ref 和 reactive?它们的区别是什么?

  • ref 用于创建一个响应式的基本类型或对象引用。
  • reactive 用于创建一个响应式的对象。
  • 读取ref创建的数据,需要.value
  • ref底层还是使用reactive来做,ref是在reactive上进行了封装,增强了其能力,使其支持了对原始数据类型的处理;

Vue3 中的 <script setup> 是什么?它的作用是什么?

<script setup> 是一种编译时语法糖,可以在单文件组件(SFC)中更简洁地使用 Composition API。

  • vue3.2后时候set up标签中的 所有属性和方法无需return,模板可直接使用;
  • 引入组件的时候,会自动注册
  • 组件通信方面:使用defineProps接收父组件传递的值; 使用useAttrs获取属性,useSlots获取插槽,defineEmits获取自定义事件;
  • 默认不会对外暴露任何属性,如果有需要使用defineExpose
<template>
  <div>{{ count }}</div>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);
</script>

Vue 3 中 setup 函数为何不能使用 this

1、tup 函数在组件实例创建之前执行

  • 在组件初始化过程中,setup 是第一个被调用的函数
  • 此时组件实例(this)尚未创建完成
  • 在 setup 内部,this 指向 undefined

2、支持TypeScript类型推导和逻辑组合

// 组件初始化顺序
1. 调用 setup()          // ← 此时 this 不存在
2. 创建响应式数据
3. 创建组件实例           // ← 此时 this 才被创建
4. 执行其他生命周期钩子

Vue3 的 Teleport 是什么?在什么场景下会用到?

Teleport 允许将组件的渲染内容移动到 DOM 的其他位置。常用于模态框、弹出菜单等需要脱离组件树布局的场景。

基本用法 to

<template>
  <div>
    <teleport to="body">
      <div class="modal">This is a modal</div>
    </teleport>
  </div>
</template>

禁用 disabled

在某些场景下可能需要视情况禁用 <Teleport>

<Teleport :disabled="isMobile"> ... </Teleport>

多个 Teleport 共享目标

就是简单的顺次追加,后挂载的将排在目标元素下更后面的位置上

<Teleport to="#modals">
  <div>A</div>
</Teleport>
<Teleport to="#modals">
  <div>B</div>
</Teleport>

结果

<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

延迟解析的 Teleport

Vue 3.5 及更高版本中,我们可以使用 `defer` prop 推迟 Teleport 的目标解析,直到应用的其他部分挂载。
<Teleport defer to="#late-div">...</Teleport>

<!-- 稍后出现于模板中的某处 -->
<div id="late-div"></div>

Vue3 中如何使用 provide 和 inject 实现依赖注入?它们的作用是什么?

  • provide 用于在上级组件中提供数据,inject 用于在下级组件中注入数据,
  • 适用于跨组件传递数据而无需通过 props 和 emits。
// 父组件
import { provide } from 'vue';

export default {
  setup() {
    provide('message', 'Hello from parent');
  }
}

// 子组件
import { inject } from 'vue';

export default {
  setup() {
    const message = inject('message');
    console.log(message); // Hello from parent
  }
}

Vue3 中的 Suspense 是什么?如何使用它来处理异步组件?

  • 用于处理异步组件加载,提供了一个优雅的加载状态处理机制
  • 使用defineAsyncComponent方法把动态导入的组件变成异步组件
  • 在模板上包裹对应的组件,并配置好default 与 fallback
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue';

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

export default {
  components: {
    AsyncComponent
  }
}
</script>

异步数据获取

<template>
  <div class="suspense-data-demo">
    <h2>Suspense 示例 - 异步数据获取</h2>
    
    <Suspense @pending="onPending" @resolve="onResolve" @fallback="onFallback">
      <template #default>
        <UserData />
      </template>
      <template #fallback>
        <div class="skeleton">
          <div class="skeleton-avatar"></div>
          <div class="skeleton-info">
            <div class="skeleton-line"></div>
            <div class="skeleton-line short"></div>
            <div class="skeleton-line"></div>
          </div>
        </div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import UserData from './UserData.vue';

// Suspense 事件处理
const onPending = () => {
  console.log('组件进入 pending 状态');
};

const onResolve = () => {
  console.log('异步依赖已解析');
};

const onFallback = () => {
  console.log('显示 fallback 内容');
};
</script>

<style scoped>
.suspense-data-demo {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
}

.skeleton {
  display: flex;
  gap: 16px;
  padding: 20px;
}

.skeleton-avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

.skeleton-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.skeleton-line {
  height: 16px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
  border-radius: 4px;
}

.skeleton-line.short {
  width: 60%;
}

@keyframes loading {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}
</style>

在 setup 函数中使用 await 时,组件会自动成为异步依赖。

image.png

image.png

image.png

image.png

请解释 Vue3 中响应式系统的工作原理?

  • Vue 3 的响应式系统分为三个核心环节:依赖收集触发更新副作用管理

  • 当我们用 reactive 包装一个对象时,Vue 内部实际上是用 ES6 的 Proxy 把这个对象给包装了一层。Proxy 有拦截器getset

  • 当我们的组件开始渲染,或者一个计算属性开始计算时,读取这些响应式数据就会触发 Proxy 的 get 拦截器

  • get 拦截器里,执行一个track 函数收集依赖,主要负责把副作用函数Effect 添加到依赖集合中

  • 当数据发生变化时,或者修改了某个属性值就触发 Proxy 的 set 拦截器,在set 拦截器中有个trigger函数, 找到所有依赖了这个数据的副作用函数Effect ,并通知它们重新执行。

1.响应式化阶段 - reactive/ref

// 核心:Proxy 代理
const proxy = new Proxy(target, {
  get(obj, key) { /* 依赖收集 */ },
  set(obj, key, value) { /* 触发更新 */ }
})
  • reactive:用 Proxy 包装对象,拦截所有操作
  • ref:通过对象的 .value 属性包装原始值

2. 依赖收集阶段 - effect/track

当组件渲染或计算属性计算时:

  • 执行副作用函数 (effect)
  • 访问响应式数据触发 get 拦截器
  • 调用 track 建立 数据属性 → 副作用函数 的映射关系

3. 触发更新阶段 - trigger

当数据变更时:

  • 修改数据触发 set 拦截器
  • 调用 trigger 查找所有依赖该数据的副作用函数
  • 重新执行这些函数,完成视图更新

具体可以参考此文章

Proxy只会代理对象的第⼀层,那么Vue3⼜是怎样处理这个问题的呢?

  • 判断当前Reflect.get的返回值是否为Object,如果是则再通过 reactive ⽅法做代理, 这样就实现了深度观测。

Proxy监测数组的时候可能触发多次get/set,那么如何防⽌触发多次呢?

  • 我们可以判断key是否为当前被代理对象target⾃身属性,也可以判断旧值与新值是否相等,只有满⾜以上两个条件之⼀时,才有可能执⾏trigger。

Vue3中为什么采用 Proxy 代替 defineProperty

defineProperty

  • 劫持的是对象的属性,遍历每个属性添加 gettersetter,实现响应式,但是存在以下问题:

    • 检测不到对象属性的添加和删除;

    • 数组API方法无法监听到;

    • 需要对每个属性进行深度遍历,如果是嵌套对象,需要深层次监听,造成性能问题;

    • 当调用 Object.defineProperty(obj, prop, descriptor) 时,需要三个参数
    • obj:要在其上定义或修改属性的对象;
    • prop待劫持(即定义或修改)的属性名;
    • descriptor:描述该属性特性和行为的对象,包含如可枚举性、可配置性、可写性以及 gettersetter
    • Object.defineProperty()是用来劫持(即定义或修改)对象的特定属性,赋予其特定的行为和特性,而不会直接影响对象本身或其他未被显式操作的属性;
    • 如果需要对对象的所有属性进行类似的操作,通常需要遍历对象的属性并分别调用Object.defineProperty()
  • Proxy

    • 监听整个对象,那么整个对象的所有操作都会进入监听操作;

总结

  • vue3响应式系统初始化遍历所有的属性,如果是多层属性嵌套时候,只有访问属性时候才会递归下一层的属性 ,Object.defineProperty需要一次性递归所有属性
  • Object.defineProperty  无法监听数组的索引和length属性,需要用7种方法重写;同时不支持 Map、Set、WeakMap 和 WeakSet等缺陷 ;
  • Proxy 直接劫持整个对象,能监听动态添加、删除属性操作
  • Proxy 可以单独的模块使用;
  • Proxy有13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,这是Object.defineProperty不具备的;

Proxy 的 13 种拦截方法

1. 基础操作拦截

  1. get(target, property, receiver)

    • 拦截操作:  读取属性,如 obj.prop 或 obj['prop']
    • 参数:  target (目标对象),property (属性名),receiver (Proxy实例或继承它的对象)
    • 典型用途:  惰性加载、计算属性、访问校验、实现私有变量(通过判断property)
  2. set(target, property, value, receiver)

    • 拦截操作:  设置属性值,如 obj.prop = value
    • 参数:  比 get 多一个 value (要设置的值)
    • 典型用途:  数据验证(如类型检查)、数据绑定/通知(Vue 3响应式核心)、设置私有属性时的报错
  3. has(target, property)

    • 拦截操作:  in 运算符,如 'prop' in obj
    • 典型用途:  隐藏对象上的某些属性,使其不被 in 运算符发现
  4. deleteProperty(target, property)

    • 拦截操作:  delete 运算符,如 delete obj.prop
    • 典型用途:  防止某些重要属性被删除,或执行关联的清理操作

2. 函数调用与构造拦截

  1. apply(target, thisArg, argumentsList)

    • 拦截操作:  函数调用,如 proxy(...args)proxy.call(...)proxy.apply(...)
    • 参数:  target (必须是函数对象),thisArg (this上下文),argumentsList (参数列表)
    • 典型用途:  函数包装、日志记录、性能监控、实现函数节流/防抖
  2. construct(target, argumentsList, newTarget)

    • 拦截操作:  new 运算符,如 new proxy(...args)
    • 参数:  target (目标类),argumentsList (参数列表),newTarget (最初被调用的构造函数,即Proxy本身)
    • 典型用途:  验证实例化参数、返回单例、自动绑定方法

3. 原型与对象扩展拦截

  1. getPrototypeOf(target)

    • 拦截操作:  Object.getPrototypeOf(proxy)
    • 典型用途:  返回一个伪造的原型对象
  2. setPrototypeOf(target, prototype)

    • 拦截操作:  Object.setPrototypeOf(proxy)
    • 典型用途:  锁定原型,禁止修改,或记录原型变更
  3. isExtensible(target)

    • 拦截操作:  Object.isExtensible(proxy)
    • 典型用途:  让一个本可扩展的对象看起来不可扩展
  4. preventExtensions(target)

-   **拦截操作:**  `Object.preventExtensions(proxy)`
-   **典型用途:**  模拟 `preventExtensions` 的行为,或阻止其生效

4. 属性描述相关拦截

  1. getOwnPropertyDescriptor(target, property)

    • 拦截操作:  Object.getOwnPropertyDescriptor(proxy, property)
    • 典型用途:  为不存在的属性返回一个描述符,或隐藏某些属性的真实描述符
  2. defineProperty(target, property, descriptor)

    • 拦截操作:  Object.defineProperty(proxy, property, descriptor)
    • 典型用途:  阻止定义某些属性,或验证属性描述符
  3. ownKeys(target)

    • 拦截操作:  Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)
    • 典型用途:  过滤掉某些私有属性(如以下划线 _ 开头的),使其在 for...in 或 Object.keys() 中不可见

Vue3 中如何创建和使用自定义指令?请举例说明。

自定义指令可以通过 app.directive 创建,并在组件中使用。

// 创建自定义指令
const app = createApp(App);
app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});

// 使用自定义指令
<template>
  <input v-focus />
</template>

在 Vue3 中,如何使用 emits 选项来定义组件事件?它与 Vue2 的事件处理有何不同?

emits 选项用于显式定义组件可以发出的事件。相比于 Vue2 中的隐式事件,在 Vue3 中定义事件更加明确。

export default {
  emits: ['custom-event'],
  setup(props, { emit }) {
    const triggerEvent = () => {
      emit('custom-event');
    }
  }
}

如果你从 Vue 2 迁移到 Vue 3,建议:

  1. 使用 emits 显式声明组件的事件。
  2. 对于复杂组件,使用对象语法对事件参数进行验证。
  3. 利用 Vue 3 的类型检查功能,提升代码的健壮性和可维护性。

示例对比

Vue 2:
export default {
  methods: {
    handleClick() {
      this.$emit('my-event', 'Hello from child');
    }
  }
}
Vue 3:
<script setup>
// 数组形式声明(简单)
const emit = defineEmits(['update', 'submit'])

// 对象形式声明(带验证器)
const emit = defineEmits({
  // 验证器函数
  'update:value': (value) => {
    return typeof value === 'string' || typeof value === 'number'
  },
  
  // 带多个参数的验证
  'custom-event': (arg1, arg2) => {
    return typeof arg1 === 'string' && typeof arg2 === 'number'
  }
})

const handleEvent = () => {
  emit('update')
  emit('submit', { data: 'test' })
  emit('update:value', '新值')
  emit('custom-event', '字符串', 123)
}
</script>

TS语法:

<script setup lang="ts">
// 类型字面量形式 - 最常用的方式
const emit = defineEmits<{
  (e: 'update'): void
  (e: 'update:modelValue', value: string): void
  (e: 'submit', payload: FormData): void
  (e: 'change', value: string, index: number): void
}>()

// 或使用接口定义
interface Emits {
  (e: 'update'): void
  (e: 'update:modelValue', value: string): void
  (e: 'submit', payload: FormData): void
}

const emit = defineEmits<Emits>()

// 触发事件
const handleSubmit = () => {
  const formData: FormData = {
    name: '张三',
    age: 25
  }
  emit('submit', formData)
}
</script>

Vue3 中如何使用 v-model 在组件中实现双向数据绑定?请解释 v-model 的工作机制。

在 Vue 2 和 Vue 3 中,v-model 的使用方式有一些区别,尤其是在自定义组件中。此外,Vue 3.3 引入了 defineModel 宏,进一步简化了 v-model 的实现。以下是详细对比:

一、v-model 的区别

image.png

1. Vue 2 中的 v-model

在 Vue 2 中,v-modelv-bind:valuev-on:input 的语法糖,默认绑定到 value 属性和 input 事件。

在表单元素中使用:
<template>
  <input v-model="message" />
  <p>{{ message }}</p>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    };
  }
};
</script>
在自定义组件中使用:

Vue 2 中,自定义组件需要通过 model 选项显式定义 v-model 的绑定属性和事件。

<!-- 父组件 -->
<template>
  <CustomInput v-model="message" />
</template>

<script>
export default {
  data() {
    return {
      message: ''
    };
  }
};
</script>

<!-- 子组件 CustomInput.vue -->
<template>
  <input :value="value" @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
  props: ['value'],
  model: {
    prop: 'value', // 绑定的属性
    event: 'input' // 触发的事件
  }
};
</script>
  • 默认绑定 value 属性和监听 input 事件。
  • 示例:
    <MyComponent v-model="message" />
    
    等价于:
    <MyComponent :value="message" @input="message = $event" />
    

2. Vue 3 中的 v-model

在 Vue 3 中,v-model 的机制进行了改进,默认绑定到 modelValue 属性和 update:modelValue 事件。此外,Vue 3 支持多个 v-model 绑定。

在表单元素中使用:

与 Vue 2 相同。

<template>
  <input v-model="message" />
  <p>{{ message }}</p>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    };
  }
};
</script>
在自定义组件中使用:

Vue 3 中,自定义组件默认使用 modelValueupdate:modelValue

<!-- 父组件 -->
<template>
  <CustomInput v-model="message" />
</template>

<script>
export default {
  data() {
    return {
      message: ''
    };
  }
};
</script>

<!-- 子组件 CustomInput.vue -->
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
};
</script>
  • 默认绑定 modelValue 属性和监听 update:modelValue 事件。
  • 示例:
    <MyComponent v-model="message" />
    
    等价于:
    <MyComponent :modelValue="message" @update:modelValue="message = $event" />
    
多个 v-model 绑定:

Vue 3 支持在同一个组件上绑定多个 v-model

<!-- 父组件 -->
<template>
  <CustomInput v-model:firstName="firstName" v-model:lastName="lastName" />
</template>

<script>
export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    };
  }
};
</script>

<!-- 子组件 CustomInput.vue -->
<template>
  <input :value="firstName" @input="$emit('update:firstName', $event.target.value)" />
  <input :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
</template>

<script>
export default {
  props: ['firstName', 'lastName'],
  emits: ['update:firstName', 'update:lastName']
};
</script>

  • 移除了 model 选项,改为通过 v-model 的参数和修饰符实现自定义。
  • 示例:
    <MyComponent v-model:checked="isChecked" />
    
    等价于:
    <MyComponent :checked="isChecked" @update:checked="isChecked = $event" />
    

变化分析

  • Vue 3 通过参数化语法取代了 model 选项,使得自定义 v-model 更加直观和灵活。
  • 这种变化简化了 API,同时保持了功能的强大性。

二、defineModel 的区别

1. Vue 3.3 引入的 defineModel

defineModel 是 Vue 3.3 新增的一个宏,用于简化自定义组件中 v-model 的实现。它自动处理 propsemits,无需手动定义。

使用 defineModel
<!-- 子组件 CustomInput.vue -->
<template>
  <input v-model="model" />
</template>

<script setup>
const model = defineModel();
</script>
解释:
  • defineModel() 返回一个响应式引用,可以直接绑定到模板中。
  • 它自动处理 modelValue 属性和 update:modelValue 事件。
多个 v-model 绑定:
<!-- 子组件 CustomInput.vue -->
<template>
  <input v-model="firstNameModel" />
  <input v-model="lastNameModel" />
</template>

<script setup>
const firstNameModel = defineModel('firstName');
const lastNameModel = defineModel('lastName');
</script>

2. defineModel 的优势

  • 简化代码:无需手动定义 propsemits
  • 类型安全:与 TypeScript 集成良好,支持类型推断。
  • 更直观:直接返回一个响应式引用,逻辑更清晰。

三、Vue 2 和 Vue 3 的 v-model 对比

特性Vue 2Vue 3Vue 3 + defineModel
默认绑定valueinputmodelValueupdate:modelValue自动处理 modelValue 和事件
自定义组件需要 model 选项需要手动定义 propsemits使用 defineModel 自动处理
多个 v-model不支持支持多个 v-model 绑定支持多个 v-model 绑定
代码简洁性需要额外配置 model 选项需要手动定义 propsemits最简洁,逻辑更清晰
类型支持支持 TypeScript支持 TypeScript,类型推断更友好

四、总结

  • Vue 2v-model 基于 valueinput,自定义组件需要 model 选项。
  • Vue 3v-model 基于 modelValueupdate:modelValue,支持多个 v-model 绑定。
  • Vue 3.3 + defineModel:进一步简化 v-model 的实现,代码更简洁,类型支持更好。 如果你使用 Vue 3.3 或更高版本,推荐使用 defineModel 来简化 v-model 的实现。

请简述 Vue 3 相较于 Vue 2 的主要改进?

  • 性能优化
    • 用 Virtual DOM 实现,提高了渲染性能。

    • 引入编译器优化,减少了运行时开销。

    • 打包体积减小有利于降低首屏加载时间

  • 代码优化
    • 组合api允许开发者更灵活地组合和重用逻辑,同时提高了可维护性和可读性。
    • 支持Ts 提供了更强大的类型推断和检查。
    • 提供了一套自定义渲染器 API,使得开发者能够更容易地创建自定义渲染器或跨平台应用。

vue3与vue2有哪些不同

代码方面

  1. proxy代替Object.definePrototety响应式系统

  2. 重写vdom,优化编译性能

  3. 增加了composition api(setup,ref,reactive),让代码更易于维护

  4. v-model 用法

  5. v-if优先级高于v-for

    • vue2: v-for比v-if优先
    • Vue3: v-if比v-for优先
    • 一般不建议v-if和v-for一起使用
  6. 生命周期变化

    • Vue2: beforeCreate、created、beforMount、munted、beforeUpdate、updated、beforeDestroy、destroyed;
    • Vue3: setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted
  7. render函数默认参数createElement移除改为全局引入

  8. 组件事件现在需要在 emits 选项中声明

  9. 组件层面

    • Vue3 templete支持多个根标签 Fragments

    • Vue3 新增Teleport组件,将组件内部模板挂载到想挂的DOM上

    • Vue3 css支持v-bind 绑定变量

    • Vue3 新增异步组件 用defineAsyncComponent 声明

    • Vue3 新增宏:defineEmits、defineModel、defineProps

  10. 实例化上:

    • Vue2: new Vue
    • Vue3: createApp
  11. 公共逻辑抽离

    • Vue2: mixin
    • Vue3: hooks
  12. diff 算法优化: vue3 有静态标记

  13. 父组件监听子组件方法

    • vue2: @hook:生命周期
    • vue3: @vue:生命周期、同时也可以使用ref

14.路由的差异

* vue2: new VueRouter
* vue3: createRouter

image.png

打包方面

  • 支持tree shaking :把无用的模块进行“剪枝”,很多没有用到的API就不会打包到最后的包

vue3在哪些方面提升了性能

通过响应式系统的重写编译优化源码体积的优化(按需加载)三个方面提升了性能。

1. 响应式系统提升

vue2在初始化的时候,通过Object.defineProperty对data的每个属性进行访问和修改的拦截,getter进行依赖收集、setter派发更新。在属性值是对象的时候还需要递归调用defineproperty。看下大致实现的代码:

function observe(target) {
  if (target && typeof target === "Object") {
    Object.keys(target).forEach((key) => {
      defineReactive(target, key, target[key])
    })
  }
}
function defineReactive(obj, key, val) {
  const dep = new Dep();
  observe(val) // 如果属性值是对象就遍历它的属性
  Object.defineProperty(obj, key, {
    get() {
      return val
    },
    set(v) {
      val = v
      dep.notify();
    }
  })
}

而如果属性是数组,还需要覆盖数组的七个方法(会改变原数组的七个方法)进行变更的通知:

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    ob.dep.notify()
    return result
  })
})

从这几段代码可以看出Object.defineProperty的几个缺点:

  • 初始化时需要遍历对象所有key,层级多的情况下,性能有一定影响
  • 动态新增、删除对象属性无法拦截,只能用set/delete api代替
  • 不支持新的Map、Set等数据结构
  • 无法监控到数组下标的变化(监听的性能代价太大)

所以在vue3中用了proxy全面代替Object.defineProperty的响应式系统。proxy是比较新的浏览器特性,拦截的是整个对象而不是对象的属性,可以拦截多种方法,包括属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性,并且是懒执行的特性,也就是在访问到的时候才会触发,当访问到对象属性的时候才会递归代理这个对象属性,所以性能比vue2有明显的优势。

总结下proxy的优势:

  • 可以监听多种操作方法,包括动态新增的属性和删除属性、has、apply等操作

  • 可以监听数组的索引和 length 等属性

  • 懒执行,不需要初始化的时候递归遍历

  • 浏览器新标准,性能更好,并且有持续优化的可能

export function reactive(target: object) {
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  const proxy = new Proxy(
    target,
    baseHandlers
  )
  proxyMap.set(target, proxy) // 用weakMap收集
  return proxy
}

2. 编译优化(虚拟dom优化)

编译优化主要是通过重写虚拟dom。优化的点包括编译模板的静态标记静态提升事件缓存

标记(PatchFlag)

根据尤大直播所说,更新的性能提升1.32倍,ssr提升23倍。 在对更新的节点进行对比的时候,只会去对比带有静态标记的节点。并且 PatchFlag 枚举定义了十几种类型,用以更精确的定位需要对比节点的类型。

看这段代码

<div id="app">
    <p>前端好好玩</p>
    <div>{{message}}</div>
</div>

vue2编译后的渲染函数:

function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('p', [_v("前端好好玩")]), _c('div', [_v(
      _s(message))])])
  }
}

这个render函数会返回vnode,后面更新的时候vue2会调patch函数比旧vnode进行diff算法更新(在我的上篇文章有解析过),这时候对比是整个vnode,包括里面的静态节点<p>前端好好玩</p>,这样就会有一定的性能损耗。

vue3编译后的渲染函数:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("p", null, "前端好好玩"),
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ]))
}

只有_createVNode这个函数带有第四个参数的才是非静态节点,也就是需要后续diff的节点。第四个参数是这个节点具体包含需要被diff的类型,比如是text节点,只有{{}}这种模板变量的绑定,后续只需要对比这个text即可,看下源码中定义了哪些枚举的元素类型:

  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2,动态Class的节点
  STYLE = 1 << 2,  // 4,表示动态样式
  PROPS = 1 << 3,  // 8,动态属性
  FULL_PROPS = 1 << 4,  // 16 动态键名
  HYDRATE_EVENTS = 1 << 5,  // 32 带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态插槽
  HOISTED = -1,  // 静态提升的标记,不会被diff,下面的静态提升会提到
  BAIL = -2 //

//位运算,有符号右移运算符,不了解的可以看这juejin.cn/post/688518…

静态提升

静态提升的意思就是把函数里的某些变量放到外面来,这样再次执行这个函数的时候就不会重新声明。vue3在编译阶段做了这个优化。还是上面那段代码,分别看下vue2和vue3编译后的不同

vue2:

function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('p', [_v("前端好好玩")]), _c('div', [_v(_s(message))])])
  }
}

vue3:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "前端好好玩", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _hoisted_2,
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ]))
}

可以看到vue3将不变的节点声明放到了外面去执行,后面再渲染的时候直接去_hoited变量就行,而vue2每次render都需要执行_c生成新的节点。这里还有一个点,_hoisted_2的_createVNode第四个参数-1,标记这个节点永远不需要diff。

事件缓存

默认情况下事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化。但是正常情况下,我们的 @click 事件在视图渲染前和渲染后,都是同一个事件,基本上不需要去追踪它的变化,所以 Vue 3.0 对此作出了相应的优化叫事件监听缓存

<div id="app">
    <p @click="handleClick">前端好好玩</p>
</div>

vue3编译后:

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = { id: "app" }

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _createVNode("p", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
    }, "前端好好玩")
  ]))
}

可以看到onClick有一个_cache判断缓存赋值的操作,从而变成静态节点

3. 源码体积的优化

vue3通过重构全局api和内部api,支持了tree shaking,任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

watch 和 watchEffect 的区别?

watch:既要指明监听数据的源,也要指明监听的回调 watchEffect不用

watch 可以访问改变前后的值 watchEffect 只能获取改变后的值;

watch运行的时候 默认不会立即执行,值改变后才会执行,而watchEffect运行后可立即执行,这一点可以通过watch的配置项immeriate改变;

watchEffect 有点像 computed

  1. computed注重的是计算出来的值(回调函数的返回值),所以必须写返回值;
  2. watchEffect注重的是过程(回调函数的函数体),所以不用写返回值;
  3. watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调;
属性watchEffectwatch说明
数据源自动收集显式指定watchEffect 自动追踪,watch 需要明确指定
回调参数(onCleanup)(value, oldValue, onCleanup)watch 提供新旧值
immediate❌ 不支持✅ 支持watch 可配置立即执行
deep❌ 不支持✅ 支持watch 可深度监听对象
flush✅ 支持✅ 支持控制执行时机
onTrack✅ 支持✅ 支持调试依赖追踪
onTrigger✅ 支持✅ 支持调试依赖触发
once❌ 不支持✅ Vue 3.4+watch 可配置只执行一次
多数据源❌ 不支持✅ 支持watch 可监听多个数据

flush属性值

'pre':默认,组件更新前执 'post':DOM 更新后执行 'sync':同步立即执行在赋值语句后立即执行

const count = ref(0)

// flush: 'pre' - 默认,组件更新前执行
watch(count, (newVal) => {
  console.log('pre - 组件更新前')
  // 此时 DOM 还未更新
}, { flush: 'pre' })

// flush: 'post' - DOM 更新后执行  
watch(count, (newVal) => {
  console.log('post - DOM 更新后')
  // 可以安全操作 DOM,访问更新后的 DOM
}, { flush: 'post' })

// flush: 'sync' - 同步立即执行
watch(count, (newVal) => {
  console.log('sync - 同步立即执行')
  // 在赋值语句后立即执行
}, { flush: 'sync' })

flush:'sync'和immediate:true 区别

immediate:true - 立即执行一次 + 变化时执行
flush:'sync':修改同步执行

watchPostEffect和watchSyncEffect 区别

是 Vue 3 提供的便捷 API,它们是 watchEffect 的特定 flush 时机的简写形式。

  • watchPostEffect:等同于watchEffect(..., { flush: 'post' }),在DOM更新后执行。
  1. 场景 1: 安全操作 DOM,因为更新已完成;获取dom的宽度
  2. 场景 2:确保 DOM 已渲染,然后初始化第三方库
  • watchSyncEffect:等同于watchEffect(..., { flush: 'sync' }),同步执行

场景 1: 紧急状态同步立即锁定界面,防止用户操作

场景 2: 实时表单验证同步验证,立即反馈

v-if和v-for哪个优先级高?

vue2中,v-for的优先级更高。vue3中,v-if的优先级更高。

无论2中还是3中,都不推荐一起使用 v-if 与 v-for

vue3文档中推荐写法如下。 在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

vue2和computed结合使用

介绍一下nextTick

  1. 为什么需要nextTick?

答:vue中数据是同步更新的,视图是异步更新。所有在我们同步代码中修改了数据,是无法访问更新后的DOM。所以官方就提供了nextTick。

  1. vue中响应式数据改变,不会立即更新DOM,而是更新了vnode,等下一次事件循环才一次性去更新DOM

  2. nextTick实现原理

    • vue2实现:判断Promise、MutationObserver、setImmediate、setTimeout兜底
    • vue3实现:Promise.then

##场景

在某些情况下,你可能需要获取更新后的 DOM 状态,比如元素的尺寸、位置等。使用 Vue.nextTick 可以确保你在 DOM 更新后获取到最新的状态。

this.someData = 'new value';
this.$nextTick(() => {
  const element = document.getElementById('my-element');
  const width = element.offsetWidth;
  console.log('Element width:', width);
});

在组件更新后执行操作

在组件中,你可能需要在数据更新后执行一些操作,比如滚动到某个位置、聚焦某个输入框等。使用 Vue.nextTick 可以确保这些操作在组件更新后执行。

this.someData = 'new value';
this.$nextTick(() => {
  this.$refs.myInput.focus(); // 聚焦输入框
});

避免重复更新

在某些情况下,你可能需要在数据更新后立即进行另一次数据更新。使用 Vue.nextTick 可以确保第一次更新完成后再进行第二次更新,避免不必要的重复更新。

this.someData = 'new value';
this.$nextTick(() => {
  this.anotherData = 'another value'; // 在第一次更新完成后更新另一个数据
});

在测试中使用

在编写单元测试时,你可能需要等待 Vue 完成 DOM 更新后再进行断言。使用 Vue.nextTick 可以确保在 DOM 更新后进行断言。

it('updates the DOM', done => {
  vm.someData = 'new value';
  Vue.nextTick(() => {
    expect(vm.$el.textContent).toBe('new value');
    done();
  });
});

v-if 和 v-show 的区别

相同点:值:true || false;都是用来控制元素的显示和隐藏

不同点:

  • v-if: 可以和v-else、v-else-if配合使用
  • v-if为true: dom树上不会有该元素,不会渲染
  • v-show为true: dom树上还存在元素,只是其display: none

怎么理解Vue的单向数据流?

单向数据流是指:数据在组件树中的流动方向,是从父组件流向子组件的。这个设计使得数据流更加可预测和易于调试,确保应用状态的一致性。

简单理解:父组件的状态对于子组件是只读的,子组件想改,只能通过事件的方式,通知父组件自己改。

父组件如何监听子组件生命周期?

  • vue2 使用 @hook:mounted
  • vue3 使用 @vue:mounted
  • 自定义事件,在子组件生命周期中去执行 下面是vue3的写法
<template>
  <h1 @click="send">Home 页面</h1>
  <Text @vue:mounted="fn" />
</template>

<script setup>
import { onMounted, ref } from 'vue'
import Text from '../components/Text.vue'
const fn = () => {
  console.log('Text mounted')
}
</script>

说一下watch和computed的区别

  • watch: 用于声明在数据更改时调用的侦听回调

    • 在某个值发生改变时触发某些操作 如异步请求
    • 不支持缓存,数据变化就执行回调
  • computed: 用于声明要在组件实例上暴露的计算属性。

    • 复杂的计算,依赖某个、多个数据,计算出新数据
    • 支持缓存,但是依赖数据一改变就会重写计算
    • 必须有返回值
    • 可以写get 和set
    • 不能有异步操作,有异步操作是无意义的

watch怎么停止监听?

import { ref, watch } from 'vue'
const count = ref(0)
const soptWatch = watch(() => count.value, (newVal, oldVal) => {
  console.log(newVal, oldVal)
})
soptWatch()

v-for中 key的作用?

用来标识列表中每个元素的唯一标识符key 的存在可以帮助Vue有效地更新和渲染DOM

Vue3中怎么访问实例?

import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const appContext = instance.appContext

Vue中怎么做全局错误监听?

app.config.errorHandler

// main.js
const app = createApp(App)
app.config.errorHandler = (err, vm, info) => {
  console.log(err, vm, info)
  // 错误日志上报
}
window.onerror = function (event) {};
// 未处理的promise错误
window.onunhandledrejection = function (event) {}

怎么监听子组件内的错误?

// 子组件
throw new Error("Error");

//父组件
import { onErrorCaptured } from 'vue'

// 监听到子组件错误,执行回调
onErrorCaptured((err) => {
  console.log('error', err)
})

页面跳转时,滚动到指定模块?

  • 在具体页面,mounted构子中滚动到某个模块(可以通过参数判断啥的)
  • scrollBehavior声明路由时,配置dom、top等属性,页面存在指定元素

image.png

vue-router怎么动态添加、删除路由?

import { useRouter } from 'vue-router';

const router = useRouter();
const addRoute = () => {
  const newRoute = {
    path: '/hello', name: 'hello', component: () => import('../components/HelloWorld.vue'), // 动态加载组件
  };
  router.addRoute(newRoute);
};

删除路由

router.removeRoute('xxx');

23. 介绍一下vuex?

vuex是官方提供全局状态管理工具(vue2),但是在vue3中更推荐使用Piniavuex主要有五大特性:

  • state: 存储应用的全局状态,所有组件都可以访问
  • getters: 类似computed,用于派生属性
  • mutations: 唯一合理修改state数据的方法(同步)
  • actions: 异步异步操作,然后通过提交 commit 来修改state
  • modules: 将状态和相关的 mutations、getters、actions 进行模块化管理,一般是页面级

SPA怎么优化首页白屏时间?

最终解决方案:SSR 服务端渲染

SPA: Single Page Application(单页应用),Vue 和 React都是。

Vue项目中,我们打完包生成dist时,如果没有指定多入口,就只会生成一个html文件,和一些js、css、图片等资源文件。html中并没有渲染元素,就一个id为app的div。页面上的元素都是通过请求js动态渲染上去。所有本身就挺慢的。

  • **路由懒加载,**首屏只请求首页相关js和css
  • 组件异步加载,首页先渲染首屏的资源,首屏下面的组件异步加载
  • 尽量减少http请求,因为浏览器有最大并发数限制,可以利用http2多路复用
  • 利用打包工具做代码拆分,splitChunks
  • 代码体积压缩gzip,打印日志去除
  • 图片压缩、图片存入CDN

说一下Vue3声明一个响应式数据的方式?

  • ref: 通过.value访问及修改

  • reactive: 直接访问、只能声明引用数据类型

  • computed: 也是通过.value,声明需要 传 get、set

  • toRef: 类似ref的用法,可以把响应式数据的属性变成ref

  • toRefs: 可以把响应式数据所有属性 转成一个个ref

  • shallRef: 浅层的ref,第二层就不会触发响应式

  • shallReactive: 浅层的reactive,第二层就不会触发响应式

  • customRef: 自定义ref

vue3和vue2路由差异和使用场景

路由模式

new Router()和createRouter

vue3

import { createRouter } from 'vue-router'

const router = createRouter({
  // ...
})

image.png

image.png

路由模式 mode 配置改为 history ,属性值调整为:

  • "history" => createWebHistory()
  • "hash" => createWebHashHistory()
  • "abstract" => createMemoryHistory()
import { createRouter, createWebHistory } from 'vue-router'
// createWebHashHistory 和 createMemoryHistory (SSR相关) 同理

createRouter({
  history: createWebHistory(),
  routes: []
})

基础路径 base 被作为 createWebHistory 的第一个参数进行传递(其他路由模式也是一样):

import { createRouter, createWebHistory } from 'vue-router'
createRouter({
  history: createWebHistory('/base-url/'),
  routes: []
})

通信

二、组件通信

1.Props/Emits(父子通信)

props(父->子)

<!-- 父组件 -->
<Child :title="data" />
<!-- 子组件 -->
<script setup>
defineProps(['title'])
</script>
  • 类型校验defineProps({ title:{ type:String, required:true } })
  • 单项数据流:子组件不能直接修改props(需要通过emit通知父组件)。
emits(子->父)

<!-- 子组件 -->
<button @click="$emit('update', value)" ></button>
<!-- 父组件 -->
<Child @update="handleUpdate" />
  • Vue3特性defineEmits(['update'])显式申明事件。

2.v-model双向绑定(语法糖进阶)

  • 单值绑定

<!-- 父组件 -->
<Child v-model="message" />
<!-- 子组件 -->
<input :value="modelValue" @input="$emit('update:modelValue',$event.target.value)" >
<script setup>
defineProps(["modalValue"])
defineEmits(["update:modelValue"])
</script>
  • 多值绑定
<Child v-model:title="title" v-model:content="content" />

3.ref/expose(父访问子组件)

  • 模版引用

<!-- 父组件 -->
<Child ref="childRef" />
<script setup>
const childRef = ref(null);
// 访问子组件暴露的属性/方法
childRef.value.childmethod();
</script>

<!-- 子组件 -->
const childMethod = () => {};
defineExpose({ childMethod })
</script>

4.provide/inject(跨层级通信)

  • 依赖注入

// 祖先组件
import { provide } from 'vue';
provide('theme', 'dark');

// 后代组件
import { inject } from 'vue';
// 第二个参数为默认值
const theme = inject('theme', 'light')
  • 响应式数据

// 提供响应式数据
const count = ref(0);
provide('count', count);

5.事件总线(Event Bus)

  • Vue3官方废弃$on,推荐第三方库(如mitt)

// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();

// 组件A(发布事件)
emitter.emit('refresh', data);

// 组件B(订阅事件)
emitter.on('refresh', (data) => { /* ... */ })

// 组件卸载时取消订阅
onUnmounted(() => emitter.off('refresh'))

vue2的EventBus是

import {EventBus} from './event-bus.js' // 引入事件中心

// 组件A(发布事件)
EventBus.$emit('addition', { num:this.num++ })

// 组件B(订阅事件)
EventBus.$on('addition', param => { this.count = this.count + param.num; })

6.状态管理(pinia)

  • 替代Vuex的官方状态库

// store/counter.js
export const useCounterStore = defineStore('counter',{
   state:()=>({ count: 0 }),
   actions:{
       increment() { this.count++; }
   }
})

// 组件中使用
const store = useCounterStore();
store.increment();

7.属性透传($attr)

  • 透传非props属性

<!-- 父组件 -->
<Child class="child-style" data-id="123" />

<!-- 子组件 -->
<div v-bind="$attrs"></div>
<script setup>
// 禁用自动继承
defineOptions({ inheritAttrs: false });
</script>

8.模版引用(Template Refs)

  • 直接操作DOM

<template>
    <input ref="inputRef" />
</template>

<script setup>
const inputRef = ref(null);
onMounted(() => inputRef.value.focus())
</script>

同名属性合并策略:Vue3中后绑定的属性会覆盖前面的,而Vue2会合并。

Vue 2 结果:

  • 同名属性:组件数据覆盖 mixin 数据
  • 不同名属性:合并保留
  • 方法:组件方法覆盖 mixin 方法

Vue 3 结果:

  • data() 返回的对象被完全覆盖,不是合并
  • 只有组件中定义的属性存在
  • mixin 中的其他属性被丢弃

为什么ref需要.value?

  • js 原始值不是响应式的,ref通过封装对象({ value:... })实现响应式
  • 底层实现
function ref(value) {
  return { 
    __v_isRef: true,
    // 依赖追踪
    get value() { track(this, 'value'); return value; },
    // 触发更新
    set value(newVal) { value = newVal; trigger(this, 'value'); }
  };
}

为什么 reactive 不需要 .value?

reactive 的工作方式

因为 reactive 创建的是 Proxy 对象,可以拦截属性的访问和修改

import { reactive } from 'vue';

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

// 直接访问属性
state.count = 1;
state.user.name = 'Alice';

// 因为 reactive 创建的是 Proxy 对象
// 可以拦截属性的访问和修改

原理深入

ref的响应式实现

  • 基本类型:通过 RefImpl 类实现,而 RefImpl 类内部使用了 JavaScript 原生的 getter/setter 语法(不是直接调用Object.defineProperty)。
  • 对象类型:内部转化为reactive代理

reactive的响应式实现

  • 基于Proxy代理整个对象,递归处理嵌套熟悉
  • 依赖收集:在get时调用track收集依赖
  • 触发更新:在set时调用trigger通知更新

为什么解构reactive对象会失去响应性?

  • 解构得到的是普通值,非响应式引用;使用toRefs转化为ref

插槽

父组件v-slot:name#name指定插槽名以及多个插槽

 <Child>
     //多个插槽
    <template #header>头部内容</template>
    <template #default>默认内容</template>
    <template #footer>底部内容</template>
</Child>
或者在父组件动态插槽
<template>
  <ChildComponent>
    <template v-for="slotName in slotNames" #[slotName]>
      <div :key="slotName">这是 {{ slotName }} 插槽的内容</div>
    </template>
  </ChildComponent>
</template>

<script setup>
const slotNames = ['header', 'body', 'footer'];
</script>
  • 子组件:用<slot name="header">定义具名插槽
<template>
     <slot name="header"></slot>
     <slot></slot> <!-- 默认插槽 -->
     <slot name="footer"></slot>
</template>

provide/inject能否替代Vuex?

  • 适用场景:跨层级但关系明确的组件。
  • 局限性:不适合全局状态管理,无法跟踪状态变化历史。

如何实现自定义v-model修饰符?

  • 通过modelModifiers判断修饰符存在,并调整数据逻辑。
<!-- CustomInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="handleInput"
    type="text"
  />
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  modelValue: String,
  // 接收 model 修饰符
  modelModifiers: {
    type: Object,
    default: () => ({})
  }
})

const emit = defineEmits(['update:modelValue'])

const handleInput = (event) => {
  let value = event.target.value
  
  // 处理各种自定义修饰符
  if (props.modelModifiers.uppercase) {
    value = value.toUpperCase()
  }
  
  if (props.modelModifiers.trim) {
    value = value.trim()
  }
  
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  
  emit('update:modelValue', value)
}
</script>

路由守卫的next作用,在Vue Router 4之前用法,和Vue Router 4 后用法 区别

核心区别总结

特性Vue Router 3(Vue 2)Vue Router 4(Vue 3)
API 风格回调函数风格返回值风格
next 参数必须提供且调用可选,逐渐被淘汰
控制方式调用 next() 函数返回特定值
异步处理容易产生回调地狱天然支持 async/await
TypeScript类型支持一般完整的类型安全

Vue Router 3(Vue 2)的 next 用法 三种调用方式

必须调用 next(),否则路由导航会挂起

router.beforeEach((to, from, next) => {
  // 1. next() - 放行导航
  if (用户已登录) {
    next()
  }
  
  // 2. next(path) - 重定向导航
  else if (需要登录) {
    next('/login')
  }
  
  // 3. next(false) - 取消导航
  else if (表单未保存) {
    const confirm = window.confirm('确定离开?')
    if (!confirm) {
      next(false)
    }
  }
})

 Vue Router 4(Vue 3)的 next 用法

Router 4 next仍然支持传统写法,但是推荐不使用

router.beforeEach((to, from) => {
  // 1. 放行导航
  if (条件1) {
    return true        // 显式放行
    // 或者不返回值(默认放行)
  }
  
  // 2. 取消导航
  else if (条件2) {
    return false       // 取消导航
  }
  
  // 3. 重定向到路径
  else if (条件3) {
    return '/login'    // 字符串路径
  }
  
  // 4. 重定向到路由位置
  else if (条件4) {
    return {
      path: '/login',
      query: { redirect: to.fullPath }
    }
  }
  
  // 5. 传递错误
  else {
    return new Error('权限验证失败')
  }
})

router.beforeEach((to, from, next) 的to,from next分别是什么

 to - 目标路由对象

router.beforeEach((to, from, next) => {
  console.log('目标路由:', to)
  // to 包含的目标路由信息:
  console.log('路径:', to.path)        // "/user/123"
  console.log('完整路径:', to.fullPath) // "/user/123?name=john"
  console.log('名称:', to.name)        // "user-profile"
  console.log('参数:', to.params)      // { id: "123" }
  console.log('查询参数:', to.query)    // { name: "john" }
  console.log('哈希:', to.hash)        // "#section"
  console.log('重定向来源:', to.redirectedFrom) // 原始路由对象
})

from - 来源路由对象

router.beforeEach((to, from, next) => {
  console.log('来源路由:', from)
  
  // from 包含的来源路由信息(与 to 结构相同):
  console.log('来源路径:', from.path)        // "/home"
  console.log('来源完整路径:', from.fullPath) // "/home?tab=1"
  console.log('来源名称:', from.name)        // "home"
  console.log('来源参数:', from.params)      // {}
  console.log('来源查询参数:', from.query)    // { tab: "1" }
})

 next - 导航控制函数

router.beforeEach((to, from, next) => {
  // 1. next() - 放行导航
  next()
  
  // 2. next(false) - 取消导航
  // next(false)
  
  // 3. next(path) - 重定向
  // next('/login')
  
  // 4. next(object) - 重定向到路由位置
  // next({ path: '/login', query: { redirect: to.fullPath } })
  
  // 5. next(error) - 传递错误 (Vue Router 4)
  // next(new Error('权限验证失败'))
})

Vue2项目如何升级Vue3

-   使用`@/vue/compat`官方工具过渡
-   逐步替换废弃API(`$children,filters`等)
-   优先迁移新组件,逐步重构旧组件

Vite 和 Webpack 对比分析

Webpack: 打包优先 (Bundle-Based)

  • 原理:在启动开发服务器之前,必须先打包构建整个项目。它会从入口文件(如 src/main.js)开始,分析所有模块依赖,生成一个或多个打包后的 Bundle 文件。
  • 开发服务器 服务于这些已经打包好的文件。
  • 问题:当项目规模变得非常大时,这个“打包构建”的过程会非常慢,导致冷启动时间很长。

Vite: 原生 ESM 优先 (Native-ESM-Based)

  • 原理:利用浏览器原生的 ES 模块 (ESM)  支持。它不需要在启动时打包整个项目。

  • 工作流程

    1. 启动:Vite 只启动一个轻量级的静态文件服务器,几乎瞬间完成。

    2. 请求:当浏览器请求某个模块时(如 import App from './App.vue'),Vite 才会在服务器端对该模块进行按需编译和转换,然后返回给浏览器。

    3. 依赖 vs 源码

      • 依赖:使用 esbuild 预构建,速度极快。
      • 源码:按需编译,只编译当前页面需要用到的文件。

Vite 架构特点

浏览器 ESM 指的是 ECMAScript Modules(ES 模块)在浏览器中的原生支持。

  1. 基于原生 ES Modules

    • 开发环境不打包,直接使用浏览器ESM
    • 按需编译,只编译当前需要的模块
    • 利用浏览器缓存机制
  2. 开发服务器

    • 使用 Koa 开发服务器
    • 中间件方式处理请求
    • 每个文件独立编译和缓存
  3. 构建工具

    • 生产环境使用 Rollup 打包
    • 统一的配置和插件系统

开发环境启动速度

Vite

  • 冷启动:秒级(< 1秒)
  • 只启动开发服务器,不打包
  • 按需编译,初始编译量极小

Webpack

  • 冷启动:10-30秒(大型项目)
  • 需要构建完整的依赖图
  • 打包所有入口文件和依赖

热更新速度(HMR)

Vite

  • 毫秒级响应(50-100ms)
  • 只重新编译修改的文件
  • 基于ESM的精确更新

Webpack

  • 秒级响应(1-3秒)
  • 需要重新构建依赖图
  • 整个模块链可能被重新编译

Vite 需要打包吗

1. 开发环境:不打包

在开发模式下 (npm run dev),Vite 的核心优势就是 「不打包」

  • 原理:它利用浏览器原生的 ES 模块 (ESM) 支持,直接将你的源码文件提供给浏览器。

  • 流程

    1. 你请求 http://localhost:5173
    2. Vite 服务器瞬间启动,返回你的 index.html
    3. index.html 里通过 <script type="module" src="/src/main.js"> 加载你的入口文件。
    4. 浏览器解析到 import { createApp } from 'vue' 和 import './style.css' 时,会向 Vite 服务器发起新的请求。
    5. Vite 服务器在接收到这些请求时,才会 「按需」 对请求的模块进行转换(例如,将 .vue 文件拆成三个部分,将 TypeScript 转换成 JavaScript,将 Sass 转换成 CSS),然后返回给浏览器。

开发环境不打包的好处:

  • 极速启动:服务器启动时间与项目规模无关。
  • 高效热更新:只更新你修改的那个文件,速度极快。

2. 生产环境:需要打包

当你运行 npm run build 时,Vite 会进行打包

为什么生产环境需要打包?

  1. 性能问题 (最重要的原因)

    • 大量的 HTTP 请求:一个中型项目可能有成千上万个模块。如果浏览器为每个模块都发起一个 HTTP 请求,巨大的网络延迟会彻底拖垮加载性能。
    • 打包 可以将这些零散的文件合并成少数几个文件,极大地减少 HTTP 请求数。
  2. 兼容性

    • 并非所有浏览器都对原生 ESM 有很好的支持,尤其是旧版本浏览器。打包工具可以进行语法降级和 Polyfill。
  3. 代码优化

    • Tree Shaking:消除那些从未被使用的 "死代码"。
    • 代码压缩:去除注释、空白符,缩短变量名,减小文件体积。
    • 代码分割:将代码拆分成多个 chunk,实现按需加载,优化缓存策略。