vue面试宝典之一

发布于:2025-03-07 ⋅ 阅读:(100) ⋅ 点赞:(0)

关于生命周期

1. vue3和vue2的生命周期 每个阶段分别可以做些什么,以及各自的使用时机和场景区别?父子组件之间生命周期的执行顺序进去总结

Vue 3 与 Vue 2 的生命周期有很多相似之处,但也有明显的变化。Vue 3 对生命周期钩子做了重命名和优化,使得生命周期更加灵活,特别是在组合式 API 中。以下是 Vue 3 和 Vue 2 的生命周期对比、使用时机、以及常见使用场景。


Vue 2 生命周期

在 Vue 2 中,生命周期钩子如下:

  1. beforeCreate:实例刚创建时调用,数据和事件还未初始化。
  2. created:实例创建完成,数据和事件已完成初始化,但尚未挂载到 DOM 上。
  3. beforeMount:模板已经编译,挂载前调用,DOM 还未渲染。
  4. mounted:实例挂载完成,模板编译生成的 DOM 已插入页面。
  5. beforeUpdate:响应式数据更新时触发,DOM 尚未更新。
  6. updated:组件 DOM 更新完成后调用。
  7. activated: 组件激活时调用。
  8. deactivated: 组件停用时调用。
  9. beforeDestroy:组件销毁前调用。
  10. destroyed:组件销毁完成。

Vue 3 生命周期

Vue 3 的生命周期与 Vue 2 类似,但重命名了一些钩子以适应组合式 API。以下是 Vue 3 的生命周期钩子:

  1. setup:组合式 API 的初始化阶段,用于创建响应式数据、定义方法等。
  2. onBeforeMount(相当于 Vue 2 的 beforeMount):DOM 未挂载。
  3. onMounted(相当于 Vue 2 的 mounted):DOM 已挂载。
  4. onBeforeUpdate(相当于 Vue 2 的 beforeUpdate):数据更新,DOM 未更新。
  5. onUpdated(相当于 Vue 2 的 updated):数据更新后 DOM 已更新。
  6. onBeforeUnmount(相当于 Vue 2 的 beforeDestroy):组件销毁前。
  7. onUnmounted(相当于 Vue 2 的 destroyed):组件销毁后。
  8. onActivated: 组件激活。
  9. onDeactivated: 组件停用。

此外,Vue 3 引入了一些新的生命周期钩子函数,提供更灵活的控制:

  • onRenderTracked:用于追踪组件的渲染依赖。
  • onRenderTriggered:当组件重新渲染时触发,调试渲染性能非常有用。

使用时机与场景

1. 数据初始化:created(Vue 2) / setup(Vue 3)
  • Vue 2created 阶段用于初始化数据、调用 API 等操作。
  • Vue 3:在组合式 API 中使用 setup,可以直接定义 refreactive 变量,同时可以进行异步操作,如调用 API 获取数据。

示例:

// Vue 2
created() {
  this.fetchData();
}

// Vue 3
<script setup>
import { ref, onMounted } from 'vue';

const data = ref(null);
async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  data.value = await response.json();
}

fetchData();
</script>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2. DOM 操作:mounted

mounted 钩子在 Vue 2 和 Vue 3 中都有,但 Vue 3 中的组合式 API 使用 onMounted

  • Vue 2:可以在 mounted 中直接获取 DOM 节点。
  • Vue 3:通过 onMounted 钩子进行 DOM 操作,适合对渲染后的 DOM 进行操作。

示例:

// Vue 2
mounted() {
  this.$refs.myElement.focus();
}

// Vue 3
<script setup>
import { onMounted, ref } from 'vue';

const myElement = ref(null);

onMounted(() => {
  myElement.value.focus();
});
</script>

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3. 组件更新:updatedbeforeUpdate
  • Vue 2beforeUpdateupdated 钩子分别在更新前和更新后触发,适用于需要监听和处理数据变化的情况。
  • Vue 3:通过 onBeforeUpdateonUpdated 实现类似效果。

示例:

// Vue 2
beforeUpdate() {
  console.log('组件即将更新');
},
updated() {
  console.log('组件已更新');
}

// Vue 3
import { onBeforeUpdate, onUpdated } from 'vue';

onBeforeUpdate(() => {
  console.log('组件即将更新');
});

onUpdated(() => {
  console.log('组件已更新');
});

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4. 组件销毁:beforeDestroydestroyed(Vue 2) / onBeforeUnmountonUnmounted(Vue 3)
  • Vue 2:在 beforeDestroy 中可以做一些清理工作,比如移除事件监听器或销毁定时器。
  • Vue 3onBeforeUnmountonUnmounted 用于相同场景,且支持组合式 API 的写法。

示例:

// Vue 2
beforeDestroy() {
  window.removeEventListener('resize', this.handleResize);
},
destroyed() {
  console.log('组件已销毁');
}

// Vue 3
import { onBeforeUnmount, onUnmounted } from 'vue';

function handleResize() {
  console.log('窗口大小变化');
}

window.addEventListener('resize', handleResize);

onBeforeUnmount(() => {
  window.removeEventListener('resize', handleResize);
});

onUnmounted(() => {
  console.log('组件已销毁');
});

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5. 依赖追踪:onRenderTrackedonRenderTriggered(Vue 3 特有)

Vue 3 新增的 onRenderTrackedonRenderTriggered,适合在调试中使用,帮助开发者了解组件渲染的依赖关系,找出潜在性能问题。

示例:

import { onRenderTracked, onRenderTriggered } from 'vue';

onRenderTracked((e) => {
  console.log('依赖追踪:', e);
});

onRenderTriggered((e) => {
  console.log('触发渲染:', e);
});

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


Vue 3 和 Vue 2 生命周期钩子的总结对比

功能 Vue 2 生命周期 Vue 3 生命周期(组合式 API) 使用场景
数据初始化 created setup 初始化数据、调用 API
初次渲染 beforeMount / mounted onBeforeMount / onMounted DOM 操作、动画
数据更新 beforeUpdate / updated onBeforeUpdate / onUpdated 监听和响应数据变化
组件销毁 beforeDestroy / destroyed onBeforeUnmount / onUnmounted 清理定时器、事件监听
依赖追踪 不适用 onRenderTracked / onRenderTriggered 调试性能问题

在 Vue 3 中,组合式 API 带来了更高的灵活性,支持更加简洁的生命周期钩子函数,同时还允许在 setup 阶段完成更多初始化工作。

在 Vue 中,父子组件的生命周期钩子执行顺序在组件创建、更新、销毁的过程中有不同的执行方式。了解父子组件的生命周期钩子执行顺序有助于在复杂组件间的状态传递、事件监听等操作中更好地控制和优化代码。

父子组件生命周期钩子执行顺序

以下是 Vue 3 和 Vue 2 中父子组件的生命周期钩子执行顺序的对比说明。


1. 组件的创建过程

在组件创建时,Vue 3 和 Vue 2 的父子组件生命周期钩子的执行顺序是相同的,遵循“父 beforeCreate -> 子 beforeCreate -> 子 created -> 父 created”的方式。详细执行顺序如下:

  • 父组件的 beforeCreate:父组件实例刚创建,数据和事件尚未初始化。
  • 父组件的 created:父组件实例已创建完成,数据和事件初始化完成。
  • 子组件的 beforeCreate:子组件实例创建。
  • 子组件的 created:子组件初始化完成。
  • 子组件的 beforeMount:模板编译完成,但 DOM 未挂载。
  • 子组件的 mounted:子组件完成挂载,DOM 插入页面。
  • 父组件的 beforeMount:父组件模板编译完成,准备挂载。
  • 父组件的 mounted:父组件挂载完成,整个组件树的初次渲染完成。

总结执行顺序:

  • beforeCreate -> 父 created
  • beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted
  • beforeMount -> 父 mounted

示例代码:

<!-- ParentComponent.vue -->
<template>
  <div>
    <p>Parent Component</p>
    <ChildComponent />
  </div>
</template>

<script setup>
import ChildComponent from './ChildComponent.vue';

onBeforeMount(() => console.log('Parent beforeMount'));
onMounted(() => console.log('Parent mounted'));
</script>

<!-- ChildComponent.vue -->
<template>
  <p>Child Component</p>
</template>

<script setup>
onBeforeMount(() => console.log('Child beforeMount'));
onMounted(() => console.log('Child mounted'));
</script>

输出顺序:

  1. Parent beforeCreate
  2. Parent created
  3. Child beforeCreate
  4. Child created
  5. Child beforeMount
  6. Child mounted
  7. Parent beforeMount
  8. Parent mounted

2. 组件更新过程

在组件更新过程中,由于子组件的变化会影响父组件,所以更新顺序从“父 beforeUpdate”开始。

  • 父组件的 beforeUpdate:父组件的响应式数据更新后,准备重新渲染。
  • 子组件的 beforeUpdate:子组件的响应式数据更新,准备重新渲染。
  • 子组件的 updated:子组件更新完成,新的 DOM 已插入。
  • 父组件的 updated:父组件更新完成。

总结执行顺序:

  • beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

示例代码:

在以下示例中,父组件中的 counter 变化会触发父子组件的更新。

<!-- ParentComponent.vue -->
<template>
  <div>
    <p>Parent Component: {{ counter }}</p>
    <button @click="increment">Increment</button>
    <ChildComponent :counter="counter" />
  </div>
</template>

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

const counter = ref(0);
const increment = () => counter.value++;

onBeforeUpdate(() => console.log('Parent beforeUpdate'));
onUpdated(() => console.log('Parent updated'));
</script>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<!-- ChildComponent.vue -->
<template>
  <p>Child Component: {{ counter }}</p>
</template>

<script setup>
import { toRef } from 'vue';
const props = defineProps({ counter: Number });

onBeforeUpdate(() => console.log('Child beforeUpdate'));
onUpdated(() => console.log('Child updated'));
</script>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输出顺序(点击按钮后):

  1. Parent beforeUpdate
  2. Child beforeUpdate
  3. Child updated
  4. Parent updated

3. 组件销毁过程

当父组件或子组件被销毁时,它们的生命周期钩子执行顺序为“父 beforeUnmount -> 子 beforeUnmount -> 子 unmounted -> 父 unmounted”。

  • 父组件的 beforeUnmount:父组件销毁前触发。
  • 子组件的 beforeUnmount:子组件销毁前触发。
  • 子组件的 unmounted:子组件销毁完成。
  • 父组件的 unmounted:父组件销毁完成。

总结执行顺序:

  • beforeUnmount -> 子 beforeUnmount -> 子 unmounted -> 父 unmounted

示例代码:

在以下示例中,点击按钮销毁子组件。

<!-- ParentComponent.vue -->
<template>
  <div>
    <p>Parent Component</p>
    <button @click="toggleChild">Toggle Child</button>
    <ChildComponent v-if="showChild" />
  </div>
</template>

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

const showChild = ref(true);
const toggleChild = () => showChild.value = !showChild.value;

onBeforeUnmount(() => console.log('Parent beforeUnmount'));
onUnmounted(() => console.log('Parent unmounted'));
</script>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<!-- ChildComponent.vue -->
<template>
  <p>Child Component</p>
</template>

<script setup>
onBeforeUnmount(() => console.log('Child beforeUnmount'));
onUnmounted(() => console.log('Child unmounted'));
</script>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输出顺序(销毁子组件后):

  1. Parent beforeUnmount
  2. Child beforeUnmount
  3. Child unmounted
  4. Parent unmounted

总结

生命周期阶段 Vue 2 执行顺序 Vue 3 执行顺序(组合式 API)
创建过程 beforeCreate -> 父 created -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 beforeMount -> 父 mounted 相同
更新过程 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated 相同
销毁过程 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed beforeUnmount -> 子 beforeUnmount -> 子 unmounted -> 父 unmounted
  • Vue 3 在销毁阶段,重命名了销毁相关的生命周期钩子为 onBeforeUnmountonUnmounted,并支持组合式 API。
  • 父子组件的生命周期顺序在组件创建、更新和销毁阶段的钩子触发顺序在 Vue 3 和 Vue 2 中是基本一致的。

2.生命周期中发送请求在created还是mounted?为什么发送请求不在beforeCreate里?beforeCreate和created有什么区别?

React 面试题:Vue 生命周期中发送请求在 created 还是 mounted

1. 发送请求应该放在 created 还是 mounted

一般来说,API 请求通常放在 created 钩子中,而 mounted 也可以用于某些特殊场景

  • created(推荐):

    • 组件实例已创建,data 可用。
    • 适合获取初始化数据,如列表数据、配置信息
    • 适用于 SSR(服务器端渲染)created 触发时还未渲染 DOM,不依赖 DOM 操作。
  • mounted

    • 组件已挂载,DOM 可用。
    • 适用于 依赖 DOM 操作的请求,如 获取元素大小、执行动画

📌 结论
👉 如果请求数据用于渲染 UI,应该放在 created,因为数据获取后 UI 会自动更新,不需要等待 DOM 挂载。
👉 如果请求涉及 DOM 操作(如 document.querySelector),则放在 mounted


2. 为什么不在 beforeCreate 发送请求?

beforeCreate 阶段:

  • 组件实例尚未创建datacomputedmethods不可用

  • 不能操作 this,否则会报错:

    beforeCreate() {
      console.log(this.someData); // undefined
      axios.get("/api/data").then(response => {
        this.someData = response.data; // ❌ 报错:this 还未初始化
      });
    }
    
  • 因此,不能在 beforeCreate 发送请求,因为无法处理响应数据。


3. beforeCreatecreated 的区别

生命周期钩子 组件实例创建 data / props methods computed watch el API 请求推荐
beforeCreate 未创建 ❌ 不可用 ❌ 不可用 ❌ 不可用 ❌ 不可用 ❌ 不可用 不推荐
created 已创建 ✅ 可用 ✅ 可用 ✅ 可用 ✅ 可用 未挂载 推荐
mounted ✅ 已创建 ✅ 可用 ✅ 可用 ✅ 可用 ✅ 可用 已挂载 适用于 DOM 依赖请求

📌 总结

  • beforeCreate 不适合请求,因为 data 还不可用。
  • created 适合大多数数据请求,数据可用,避免 DOM 依赖问题。
  • mounted 适用于 DOM 相关的 API 请求(如 getBoundingClientRect)。

4. Vue3 Composition API 的请求位置

在 Vue3 的 setup() 里,我们通常直接调用 API 请求

import { ref, onMounted } from "vue";
import axios from "axios";

export default {
  setup() {
    const data = ref(null);

    axios.get("/api/data").then((res) => {
      data.value = res.data;
    });

    onMounted(() => {
      console.log("组件已挂载");
    });

    return { data };
  },
};
  • setup() 里发请求相当于 created
  • 也可以用 onMounted() 处理 DOM 相关逻辑。

5. 真实面试回答示例

💡 问题:Vue 组件中发送请求应该放在哪个生命周期钩子?为什么?
回答:
API 请求通常放在 created,因为此时组件实例已创建,可以访问 datamethods,且不会依赖 DOM。
如果请求需要操作 DOM(如 getBoundingClientRect),则应该放在 mounted
beforeCreate 阶段实例尚未初始化,this 不能使用,因此不能放在 beforeCreate

3.在created中如何获取dom

created 钩子中无法直接获取 DOM,因为此时组件 尚未挂载el 还不可用。


为什么在 created 里不能获取 DOM?

在 Vue 的生命周期中:

  • created 只是组件实例化完成,但DOM 还未渲染
  • 此时 this.$el 还是 undefined 或者是 template 占位符。
  • 只有在 mounted 之后,DOM 才会被渲染并插入到页面中。

📌 示例:在 created 里尝试获取 this.$el

export default {
  created() {
    console.log(this.$el); // ❌ undefined
  },
  mounted() {
    console.log(this.$el); // ✅ 可以访问 DOM
  }
}

所以,如果你需要获取 DOM,应该等到 mounted 之后


如何在 created 里间接操作 DOM?

虽然 created 不能直接访问 DOM,但可以:

  1. 提前存储需要的选择器信息
  2. mounted 里操作 DOM
  3. 如果数据请求在 created,可以在 mounted 里处理 DOM 依赖逻辑

方案 1:将数据请求放在 created,DOM 操作放在 mounted

export default {
  data() {
    return { list: [] };
  },
  created() {
    // 先获取数据
    this.fetchData();
  },
  mounted() {
    // DOM 依赖操作
    console.log(this.$refs.myDiv); // ✅ 可以获取 DOM
  },
  methods: {
    fetchData() {
      setTimeout(() => {
        this.list = ["A", "B", "C"];
      }, 1000);
    },
  },
};

方案 2:Vue3 中用 onMounted

在 Vue3 的 setup() 里,也不能在 setup() 阶段访问 DOM,但可以使用 onMounted

import { ref, onMounted } from "vue";

export default {
  setup() {
    const myDiv = ref(null);

    onMounted(() => {
      console.log(myDiv.value); // ✅ 可以获取 DOM
    });

    return { myDiv };
  },
};

总结

生命周期钩子 是否能访问 DOM 适用场景
beforeCreate ❌ 不能 组件实例未创建,无法访问 this
created ❌ 不能 组件实例已创建,但 DOM 还未挂载
mounted ✅ 可以 组件挂载完成,可访问 this.$el$refs
onMounted(Vue3) ✅ 可以 适用于 Vue3 的 setup()

✅ 结论:想操作 DOM,应该在 mounted(Vue2)或 onMounted(Vue3)中进行!

4. 第二次或者第N次进去组件会执行哪些生命周期?

Vue 组件的生命周期:第二次或第 N 次进入组件会执行哪些钩子?

这取决于组件是如何被进入的

  • 同一路由复用(keep-alive 缓存)
  • 重新创建(组件销毁 & 重新加载)
  • 切换显示/隐藏(v-if / v-show

情况 1:普通组件,每次进入都会重新创建

如果组件在 router-viewv-if 控制下,每次进入都会重新创建

  1. beforeCreate
  2. created
  3. beforeMount
  4. mounted
  5. (如果离开) beforeUnmount
  6. (如果离开) unmounted

📌 示例

<template>
  <button @click="show = !show">切换组件</button>
  <my-component v-if="show" />
</template>
  • 每次 show = true,组件都会完整重新执行生命周期。

情况 2:keep-alive 组件(缓存)

如果组件使用了 keep-alive,那么不会销毁,而是被缓存

  1. 第一次进入

    • beforeCreate
    • created
    • beforeMount
    • mounted
    • activatedkeep-alive 组件独有)
  2. 第二次及后续进入

    • 不会重新执行 createdmounted
    • 直接触发 activated
  3. 离开

    • deactivated(不会销毁)

📌 示例

<keep-alive>
  <router-view />
</keep-alive>
  • activated:组件从缓存中激活时执行
  • deactivated:组件被缓存,不显示时执行
  • unmounted:组件完全销毁时才执行

情况 3:v-if vs v-show

方式 生命周期 适用场景
v-if 销毁 & 重新创建 大量 DOM 操作,减少性能开销
v-show 仅控制显示/隐藏,不触发生命周期 频繁切换的 UI(如 Tab 切换)

总结

进入方式 第一次执行 后续执行 退出时
普通组件 beforeCreatecreatedbeforeMountmounted 重新执行所有生命周期 beforeUnmountunmounted
keep-alive 组件 同上 + activated 只执行 activated deactivated(不会销毁)
v-if 切换 组件销毁 & 重新创建 完整生命周期重新执行 unmounted
v-show 切换 只执行 mounted 一次 不会重新执行生命周期 无需处理

👉 如果组件是被缓存的 keep-alive,则 activateddeactivated 取代了 mountedunmounted
👉 如果组件是 v-if 控制的,则每次都会重新走完整生命周期。

5. 加入keep-alive会执行哪些生命周期?

## **`keep-alive` 组件的生命周期**

当组件被 keep-alive 缓存时,它不会被销毁,而是进入“缓存状态”,因此它的生命周期和普通组件不同。


1. 第一次进入(组件被创建并挂载)

第一次进入时,keep-alive 组件和普通组件的生命周期几乎相同:

  1. beforeCreate
  2. created
  3. beforeMount
  4. mounted
  5. activated(keep-alive 专属)

📌 说明

  • activated 表示组件被 keep-alive 缓存,并且被激活(显示出来)。
  • mounted 只会在组件首次挂载时触发,后续缓存激活不会再触发 mounted

2. 第二次及后续进入(组件未销毁,只是重新显示)

第二次及之后的进入时,不会重新创建组件,只会触发:

  1. activated

📌 说明

  • activated 会在组件从缓存状态恢复时触发,而不会触发 createdmounted

3. 离开组件(切换到其他路由/隐藏)

keep-alive 组件离开但未销毁时,触发:

  1. deactivated

📌 说明

  • deactivated 代表组件被缓存并切换到后台,但没有销毁,数据依然存在。

4. 组件被销毁

如果组件彻底被销毁(比如 keep-alive 被移除),则触发:

  1. beforeUnmount
  2. unmounted

📌 说明

  • 组件完全销毁时,keep-alive 失效,进入普通的 unmounted 过程。

完整生命周期对比

事件 普通组件 keep-alive 组件
beforeCreate (仅首次)
created (仅首次)
beforeMount (仅首次)
mounted (仅首次)
activated (每次进入时触发)
deactivated (切换/隐藏时触发)
beforeUnmount (被销毁时触发)
unmounted (被销毁时触发)

示例代码

<template>
  <keep-alive>
    <MyComponent v-if="showComponent" />
  </keep-alive>
  <button @click="toggleComponent">切换组件</button>
</template>

<script>
export default {
  data() {
    return { showComponent: true };
  },
  methods: {
    toggleComponent() {
      this.showComponent = !this.showComponent;
    }
  }
};
</script>

<script>
export default {
  name: "MyComponent",
  created() {
    console.log("created");
  },
  mounted() {
    console.log("mounted");
  },
  activated() {
    console.log("activated");
  },
  deactivated() {
    console.log("deactivated");
  },
  beforeUnmount() {
    console.log("beforeUnmount");
  },
  unmounted() {
    console.log("unmounted");
  }
};
</script>

总结

activatedkeep-alive 组件每次进入都会触发(代替 mounted)。
deactivatedkeep-alive 组件离开时触发(不会销毁)。
createdmounted 只在首次进入时触发,后续进入不会重复执行。
beforeUnmountunmounted 仅在 keep-alive 失效或组件被销毁时执行

📌 记住: keep-alive 组件不会销毁,只是缓存 & 激活,所以多次进入不会触发 createdmounted,而是触发 activated

6. keep-alive的实现原理

keep-alive 的实现原理

keep-alive 是 Vue 提供的一个内置组件,用于缓存组件,避免不必要的重复创建和销毁,提高性能。它的核心原理是通过动态管理组件的挂载与卸载,而不是彻底销毁组件


1. keep-alive 组件的工作流程

keep-alive 包裹某个组件时:

  1. 第一次渲染

    • 组件被正常创建 (beforeCreate → created → beforeMount → mounted)。
    • 组件实例会被缓存,而不是销毁。
  2. 再次进入(缓存命中)

    • 组件不会重新执行 createdmounted
    • 只会触发 activated(表示组件从缓存中恢复)。
  3. 离开(缓存未销毁)

    • 组件不会被 unmounted,而是触发 deactivated(表示组件被缓存,但未销毁)。
  4. 如果 keep-alive 被销毁

    • 组件才会执行 beforeUnmount → unmounted,彻底销毁实例。

2. keep-alive 的核心实现

Vue 内部通过 虚拟 DOM 维护 keep-alive 组件的缓存,核心是:

  • 利用 cache 对象存储已经创建过的组件实例
  • 动态控制 VNodepatch 过程,使组件不会被销毁
  • activateddeactivated 钩子中控制组件的显示和隐藏

核心源码简化版

export default {
  name: "KeepAlive",
  setup(props, { slots }) {
    const cache = new Map(); // 缓存存储
    return () => {
      const vnode = slots.default();
      const key = vnode.type; // 组件的唯一 key
      if (cache.has(key)) {
        vnode.component = cache.get(key); // 复用缓存
      } else {
        cache.set(key, vnode.component); // 存入缓存
      }
      return vnode;
    };
  }
};

📌 关键点

  • cache 负责存储已经创建过的组件实例
  • 当组件再次渲染时,如果 cache 里有对应实例,则直接复用
  • 避免重复执行 createdmounted,仅触发 activateddeactivated

3. keep-alive 的核心逻辑

Vue 在 keep-alive 组件内部主要做了以下几件事:

(1)缓存组件实例

  • patch 过程中,Vue 会检测组件是否被 keep-alive 包裹:

    • 如果是,则将组件存入 cache,避免销毁。
    • 如果不是,则正常销毁组件。

(2)控制 VNode 渲染

  • 当组件被缓存后,Vue 只会重新激活它,而不会重新挂载。

📌 Vue 内部 keep-alive 组件的 patch 逻辑

if (keepAlive) {
  if (cache.has(vnode.key)) {
    vnode.component = cache.get(vnode.key);
  } else {
    cache.set(vnode.key, vnode.component);
  }
}
  • 组件 如果在 cache 里存在,直接复用,不再创建。
  • 组件 如果不在 cache,才会创建新的实例并存入缓存。

(3)activated & deactivated 事件

  • 组件进入时activated
  • 组件离开时deactivated
export default {
  activated() {
    console.log("组件被激活(从缓存恢复)");
  },
  deactivated() {
    console.log("组件被缓存(但未销毁)");
  }
};

4. keep-aliveincludeexclude

keep-alive 提供了 includeexclude 选项,用于控制哪些组件需要缓存,哪些不需要缓存

(1)include:指定需要缓存的组件

<keep-alive include="A,B">
  <component :is="view" />
</keep-alive>

📌 说明

  • 只有 AB 组件会被缓存,其他组件不会。

(2)exclude:指定不需要缓存的组件

<keep-alive exclude="C">
  <component :is="view" />
</keep-alive>

📌 说明

  • 组件 C 不会 被缓存,其他组件都会被缓存。

5. keep-alive 适用场景

适合使用 keep-alive 的场景

  • 多标签切换

    • 比如后台管理系统的多标签页面,每次切换不希望重新加载数据:
    <keep-alive>
      <router-view />
    </keep-alive>
    
  • 表单数据暂存

    • 需要在多个页面之间切换时保留表单数据,避免重新填写。

不适合使用 keep-alive 的场景

  • 组件需要频繁重新创建

    • 比如登录、注销等场景,每次进入都需要初始化数据,不适合缓存。
  • 组件包含大量副作用

    • 比如 WebSocket、定时器、全局事件监听等,可能导致资源泄露(需要在 deactivated 里手动清理)。

6. keep-alive 的优化

如果 keep-alive 组件缓存过多,可能会导致内存占用增加,可以通过以下方式优化:

  1. 使用 include / exclude 限制缓存的组件数量

  2. 手动清理缓存

    this.$refs.keepAliveComp.cache.clear();
    

7. 总结

关键点 解释
原理 keep-alive 通过 cache 存储已创建的组件,避免销毁,提高性能
核心生命周期 组件第一次渲染时执行 created / mounted,后续进入时执行 activated,离开时执行 deactivated
缓存机制 组件被 keep-alive 包裹后,不会重复创建,只会触发 activated / deactivated
优化方式 使用 include / exclude 控制缓存,避免占用过多内存

💡 结论: keep-alive 适用于需要频繁切换但不希望重复创建的组件,如路由缓存、表单数据暂存等场景,但使用时需注意控制缓存数量,以防止性能问题。

关于组件

7. 组件(通信)的方式,父组件直接修改子组件的值,子组件直接修改父组件的值

在 Vue 中,组件之间的通信通常遵循**单向数据流**的原则,即:
  • 父组件可以通过 props 传递数据给子组件,但子组件不能直接修改 props(违反 Vue 规则)
  • 子组件可以通过 $emit 触发事件通知父组件,但不能直接修改父组件的数据

但如果要直接修改父组件或子组件的值,可以通过以下几种方式实现:


1. 父组件直接修改子组件的值

方法 1:使用 ref 访问子组件实例

父组件可以通过 ref 直接访问子组件,并修改其值。

<!-- 父组件 -->
<template>
  <ChildComponent ref="childRef" />
  <button @click="changeChildValue">修改子组件值</button>
</template>

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

export default {
  components: { ChildComponent },
  setup() {
    const childRef = ref(null);

    const changeChildValue = () => {
      if (childRef.value) {
        childRef.value.childData = '父组件修改后的值';
      }
    };

    return { childRef, changeChildValue };
  }
};
</script>
<!-- 子组件 -->
<template>
  <div>子组件值:{{ childData }}</div>
</template>

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

export default {
  setup() {
    const childData = ref('初始值');

    return { childData };
  }
};
</script>

📌 说明

  • ref="childRef" 获取子组件的实例,父组件可以直接修改 childData
  • 适用于 Vue 3,Vue 2 需要用 this.$refs.childRef 访问子组件实例。

方法 2:使用 v-model 双向绑定

Vue 3 支持在自定义组件上使用 v-model,这样父组件可以直接修改子组件的值。

<!-- 父组件 -->
<template>
  <ChildComponent v-model="childValue" />
</template>

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

export default {
  components: { ChildComponent },
  setup() {
    const childValue = ref('父组件数据');

    return { childValue };
  }
};
</script>
<!-- 子组件 -->
<template>
  <input v-model="modelValue" />
</template>

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

export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const updateValue = (event) => {
      emit('update:modelValue', event.target.value);
    };

    return { updateValue };
  }
};
</script>

📌 说明

  • v-model 本质上是 props + emit 组合,使父组件能直接修改子组件的 modelValue

2. 子组件直接修改父组件的值

方法 1:使用 $emit 事件通知父组件

通常的做法是子组件不直接修改父组件的值,而是通知父组件修改自己的数据

<!-- 父组件 -->
<template>
  <ChildComponent :value="parentValue" @updateValue="updateParentValue" />
</template>

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

export default {
  components: { ChildComponent },
  setup() {
    const parentValue = ref('父组件初始值');

    const updateParentValue = (newValue) => {
      parentValue.value = newValue;
    };

    return { parentValue, updateParentValue };
  }
};
</script>
<!-- 子组件 -->
<template>
  <input :value="value" @input="updateParent" />
</template>

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

export default {
  props: ['value'],
  emits: ['updateValue'],
  setup(props, { emit }) {
    const updateParent = (event) => {
      emit('updateValue', event.target.value);
    };

    return { updateParent };
  }
};
</script>

📌 说明

  • 子组件不能直接修改 props,所以使用 $emit 触发 updateValue 事件,让父组件修改数据。

方法 2:使用 inject / provide

如果是多级组件嵌套,子组件可以使用 inject 直接获取父组件的数据,并进行修改:

<!-- 父组件 -->
<template>
  <ProvideChild />
</template>

<script>
import { ref, provide } from 'vue';
import ProvideChild from './ProvideChild.vue';

export default {
  components: { ProvideChild },
  setup() {
    const parentValue = ref('父组件初始值');

    provide('parentValue', parentValue);

    return { parentValue };
  }
};
</script>
<!-- 子组件 -->
<template>
  <div>子组件获取到的值:{{ parentValue }}</div>
  <button @click="updateParent">修改父组件的值</button>
</template>

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

export default {
  setup() {
    const parentValue = inject('parentValue');

    const updateParent = () => {
      parentValue.value = '子组件修改的值';
    };

    return { parentValue, updateParent };
  }
};
</script>

📌 说明

  • provide 让父组件提供数据,inject 让子组件获取数据并修改。
  • 这种方式可以让多级组件共享数据,但如果 parentValue 不是 ref,子组件修改不会生效。

方法 3:使用 Vuex / Pinia(全局状态管理)

如果数据是全局的,子组件可以通过 Vuex 或 Pinia 直接修改父组件的数据:

// store.js (Pinia)
import { defineStore } from 'pinia';

export const useStore = defineStore('main', {
  state: () => ({
    parentValue: '全局数据'
  }),
  actions: {
    updateParentValue(newValue) {
      this.parentValue = newValue;
    }
  }
});
<!-- 父组件 -->
<template>
  <ChildComponent />
</template>
<!-- 子组件 -->
<template>
  <button @click="updateGlobalValue">修改全局数据</button>
</template>

<script>
import { useStore } from '../store';

export default {
  setup() {
    const store = useStore();

    const updateGlobalValue = () => {
      store.updateParentValue('子组件修改的全局数据');
    };

    return { updateGlobalValue };
  }
};
</script>

📌 说明

  • Vuex/Pinia 适用于大型项目,可以方便管理全局状态。

总结

方式 父改子 子改父 适用场景
ref 访问子组件 父组件需要直接控制子组件
v-model 适用于表单组件的双向绑定
$emit 事件 适用于父子组件间事件通信
provide/inject 适用于多级嵌套组件通信
Vuex / Pinia 适用于全局状态管理

最佳实践

  • 推荐 $emit 事件:子组件通知父组件,而不是直接修改父数据。
  • 使用 refv-model:父组件需要修改子组件时。
  • 多级组件推荐 provide/inject,全局数据推荐 Vuex/Pinia。

8. vue2和vue3父子祖孙组件通讯的方法以及区别对比

在 Vue 2 和 Vue 3 中,父子、祖孙组件的通信方式有一些不同,Vue 3 引入了新的 API,使组件通信更加灵活和高效。以下是它们的主要通信方式和对比:

1. 父组件与子组件通信

Vue 2

  1. 父传子:props

    • Vue 2 使用 props 传递数据:
    <!-- 父组件 -->
    <template>
      <ChildComponent :msg="message" />
    </template>
    
    <script>
    import ChildComponent from './ChildComponent.vue';
    
    export default {
      components: { ChildComponent },
      data() {
        return { message: 'Hello from Parent' };
      }
    };
    </script>
    
    <!-- 子组件 -->
    <template>
      <p>{{ msg }}</p>
    </template>
    
    <script>
    export default {
      props: ['msg']
    };
    </script>
    
  2. 子传父:$emit

    • 子组件触发事件,父组件监听:
    <!-- 父组件 -->
    <template>
      <ChildComponent @childEvent="handleChildEvent" />
    </template>
    
    <script>
    import ChildComponent from './ChildComponent.vue';
    
    export default {
      components: { ChildComponent },
      methods: {
        handleChildEvent(value) {
          console.log('收到子组件数据:', value);
        }
      }
    };
    </script>
    
    <!-- 子组件 -->
    <template>
      <button @click="$emit('childEvent', '子组件的数据')">发送数据</button>
    </template>
    

Vue 3

  1. 父传子:props(同 Vue 2)

    <script setup>
    defineProps(['msg']);
    </script>
    
  2. 子传父:defineEmits

    <script setup>
    const emit = defineEmits(['childEvent']);
    
    const sendData = () => {
      emit('childEvent', '子组件数据');
    };
    </script>
    

🔹 区别

  • Vue 3 直接使用 definePropsdefineEmits,更简洁,无需 this.$emit
  • setup 语法更加直观,避免 this 作用域的问题。

2. 兄弟组件通信

Vue 2

  1. 事件总线(Event Bus

    // 创建全局事件总线 eventBus.js
    import Vue from 'vue';
    export const EventBus = new Vue();
    
    <!-- 兄弟组件 A 发送消息 -->
    <script>
    import { EventBus } from '../eventBus.js';
    
    export default {
      methods: {
        sendMessage() {
          EventBus.$emit('messageEvent', '来自 A 组件的消息');
        }
      }
    };
    </script>
    
    <!-- 兄弟组件 B 接收消息 -->
    <script>
    import { EventBus } from '../eventBus.js';
    
    export default {
      created() {
        EventBus.$on('messageEvent', (msg) => {
          console.log('收到的消息:', msg);
        });
      }
    };
    </script>
    
  2. Vuex(推荐用于全局状态)

    • Vuex 适用于大项目管理数据。

Vue 3

  1. 事件总线(mitt

    • Vue 3 移除了 this.$on,可以用 mitt 替代:
    // eventBus.js
    import mitt from 'mitt';
    export const EventBus = mitt();
    
    <!-- 兄弟组件 A -->
    <script setup>
    import { EventBus } from '../eventBus.js';
    
    const sendMessage = () => {
      EventBus.emit('messageEvent', '来自 A 组件的消息');
    };
    </script>
    
    <!-- 兄弟组件 B -->
    <script setup>
    import { EventBus } from '../eventBus.js';
    
    EventBus.on('messageEvent', (msg) => {
      console.log('收到的消息:', msg);
    });
    </script>
    
  2. Pinia(Vuex 替代方案)

    • Vue 3 推荐使用 Pinia 作为 Vuex 的替代方案。

🔹 区别

  • Vue 3 不能使用 $on,需要 mitt
  • 推荐用 Pinia 代替 Vuex。

3. 祖孙组件(跨级组件通信)

Vue 2

  1. provide/inject

    <!-- 父组件 -->
    <script>
    export default {
      provide() {
        return { parentData: '父组件数据' };
      }
    };
    </script>
    
    <!-- 孙组件 -->
    <script>
    export default {
      inject: ['parentData'],
      created() {
        console.log(this.parentData);
      }
    };
    </script>
    
  2. Vuex(全局管理)

    • Vuex 适合大规模数据管理。

Vue 3

  1. provide/inject(支持响应式 ref

    <!-- 父组件 -->
    <script setup>
    import { ref, provide } from 'vue';
    
    const parentData = ref('父组件数据');
    provide('parentData', parentData);
    </script>
    
    <!-- 孙组件 -->
    <script setup>
    import { inject } from 'vue';
    
    const parentData = inject('parentData');
    console.log(parentData.value);
    </script>
    

🔹 区别

  • Vue 3 支持 ref,祖孙组件通信变得更方便。

总结对比

方式 Vue 2 Vue 3 适用场景
父传子 props defineProps 组件间传递数据
子传父 $emit defineEmits 子组件通知父组件
兄弟通信 $emit + EventBus / Vuex mitt / Pinia 组件间消息传递
祖孙通信 provide/inject(仅基本数据) provide/inject(支持 ref 跨层级数据传递
全局状态 Vuex Pinia 适用于大项目

选择建议

  1. 小型项目

    • props / emit / provide/inject 足够。
    • 兄弟组件可以用 mitt
  2. 中大型项目

    • 推荐 Pinia(比 Vuex 更轻量)
    • 跨层级组件通信可以用 provide/inject + ref
  3. 实时通信

    • WebSocket 适用于需要长连接的情况。

Vue 3 提供了更简洁的 API,推荐使用 setup + defineProps / defineEmits,减少了 this 作用域问题,使代码更清晰、可维护。

9.vue中如何找到父组件,如何找到根组件

在 Vue 中,可以通过不同方式获取 **父组件** 和 **根组件**,具体方法取决于使用 Vue 2 还是 Vue 3。

1. 获取父组件

Vue 2

方式 1:使用 $parent
  • 直接访问当前组件的父组件实例:

    <script>
    export default {
      mounted() {
        console.log(this.$parent); // 获取父组件实例
      }
    };
    </script>
    

    注意:

    • 适用于简单的父子组件层级,但不推荐深度嵌套组件使用 $parent,容易导致耦合。
方式 2:inject/provide(推荐用于深层组件)
  • 适用于跨层级组件通信:

    <!-- 父组件 -->
    <script>
    export default {
      provide() {
        return { parentData: '父组件数据' };
      }
    };
    </script>
    
    <!-- 子组件 -->
    <script>
    export default {
      inject: ['parentData'],
      created() {
        console.log(this.parentData);
      }
    };
    </script>
    

Vue 3

方式 1:使用 $parent(仍然适用,但不推荐)
<script setup>
import { onMounted, getCurrentInstance } from 'vue';

onMounted(() => {
  const instance = getCurrentInstance();
  console.log(instance?.proxy.$parent); // 获取父组件实例
});
</script>
方式 2:inject/provide(推荐)
  • Vue 3 的 provide/inject 支持 ref,数据是响应式的:

    <!-- 父组件 -->
    <script setup>
    import { ref, provide } from 'vue';
    
    const parentData = ref('父组件数据');
    provide('parentData', parentData);
    </script>
    
    <!-- 子组件 -->
    <script setup>
    import { inject } from 'vue';
    
    const parentData = inject('parentData');
    console.log(parentData.value); // 访问响应式数据
    </script>
    

2. 获取根组件

Vue 2

方式 1:使用 $root
  • $root 直接获取应用根组件:

    <script>
    export default {
      mounted() {
        console.log(this.$root); // 根组件实例
      }
    };
    </script>
    

Vue 3

方式 1:使用 $root(仍然适用)
<script setup>
import { getCurrentInstance, onMounted } from 'vue';

onMounted(() => {
  const instance = getCurrentInstance();
  console.log(instance?.proxy.$root); // 获取根组件实例
});
</script>
方式 2:app.config.globalProperties
  • 在 Vue 3 中,globalProperties 可以用来在整个应用中共享数据:

    import { createApp } from 'vue';
    import App from './App.vue';
    
    const app = createApp(App);
    app.config.globalProperties.$rootData = '根组件数据';
    app.mount('#app');
    
    <!-- 任何子组件 -->
    <script setup>
    import { getCurrentInstance } from 'vue';
    
    const instance = getCurrentInstance();
    console.log(instance?.appContext.config.globalProperties.$rootData);
    </script>
    

总结

方式 Vue 2 Vue 3 适用场景
获取父组件 $parent $parent(不推荐) / inject(推荐) 访问父组件实例或跨层级数据传递
获取根组件 $root $root / app.config.globalProperties 获取整个 Vue 实例
  • 推荐使用 provide/inject 进行深层数据传递,避免 $parent 耦合。
  • 获取根组件时,Vue 3 可使用 getCurrentInstance() 访问 appContext.config.globalProperties

10. vue2,vue3中solt插槽,插槽有些什么类型,什么情况下使用插槽

## **Vue 2 和 Vue 3 中的 Slot(插槽)**

插槽(slot)是一种 Vue 组件的内容分发机制,允许父组件向子组件传递内容,同时让子组件在特定位置渲染这些内容。Vue 2 和 Vue 3 的插槽使用方式基本一致,但 Vue 3 进行了优化。


1. 插槽的类型

① 默认插槽(Default Slot)

  • 用于父组件向子组件传递默认内容
Vue 2
<!-- 子组件 -->
<template>
  <div>
    <slot>默认内容</slot>
  </div>
</template>
<!-- 父组件 -->
<template>
  <ChildComponent>父组件的内容</ChildComponent>
</template>

🔹 如果父组件没有传递内容,插槽会显示默认值 默认内容

Vue 3
<!-- 子组件 -->
<template>
  <div>
    <slot>默认内容</slot>
  </div>
</template>

Vue 3 语法和 Vue 2 基本一致


② 具名插槽(Named Slot)

  • 用于在子组件中定义多个插槽,父组件可以指定内容填充到特定插槽
Vue 2
<!-- 子组件 -->
<template>
  <div>
    <header><slot name="header">默认头部</slot></header>
    <main><slot>默认内容</slot></main>
    <footer><slot name="footer">默认底部</slot></footer>
  </div>
</template>
<!-- 父组件 -->
<template>
  <ChildComponent>
    <template v-slot:header>我是头部</template>
    <template v-slot:default>我是主体内容</template>
    <template v-slot:footer>我是底部</template>
  </ChildComponent>
</template>

🔹 v-slot:插槽名 替代 Vue 2 早期的 slot="header" 语法

Vue 3
  • Vue 3 的语法更简洁
<!-- 父组件 -->
<template>
  <ChildComponent>
    <template #header>我是头部</template>
    <template #default>我是主体内容</template>
    <template #footer>我是底部</template>
  </ChildComponent>
</template>

🔹 v-slot: 可以简写为 #


③ 作用域插槽(Scoped Slot)

  • 用于将子组件数据传递给父组件的插槽
Vue 2
<!-- 子组件 -->
<template>
  <div>
    <slot :user="user"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return { user: { name: 'Tom', age: 25 } };
  }
};
</script>
<!-- 父组件 -->
<template>
  <ChildComponent v-slot:default="{ user }">
    <p>用户:{{ user.name }},年龄:{{ user.age }}</p>
  </ChildComponent>
</template>

🔹 子组件 slot 传递 user,父组件使用 v-slot:default 解构数据

Vue 3
  • 语法基本相同:
<template>
  <ChildComponent #default="{ user }">
    <p>用户:{{ user.name }},年龄:{{ user.age }}</p>
  </ChildComponent>
</template>

④ 动态插槽名

  • 允许使用动态 :name 绑定插槽名称
Vue 2
<!-- 子组件 -->
<template>
  <div>
    <slot :name="dynamicSlot"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return { dynamicSlot: 'header' };
  }
};
</script>
<!-- 父组件 -->
<template>
  <ChildComponent>
    <template v-slot:[dynamicSlot]>我是动态插槽内容</template>
  </ChildComponent>
</template>
Vue 3
  • Vue 3 语法类似
<template>
  <ChildComponent>
    <template #[dynamicSlot]>我是动态插槽内容</template>
  </ChildComponent>
</template>

🔹 动态插槽适用于多个插槽切换的场景


2. 插槽的使用场景

1️⃣ 组件内容分发

  • 允许复用组件时,灵活定义不同的内容。
<Modal>
  <template #header>自定义标题</template>
  <template #body>这里是内容</template>
  <template #footer>按钮</template>
</Modal>

2️⃣ 作用域插槽传递数据

  • 当子组件需要把数据传递给父组件使用时
<TableComponent #default="{ row }">
  <p>{{ row.name }} - {{ row.age }}</p>
</TableComponent>

3️⃣ 动态插槽

  • 适用于可变插槽名称,如动态布局组件

3. Vue 2 和 Vue 3 插槽的区别

插槽类型 Vue 2 Vue 3 变化
默认插槽 无变化
具名插槽 v-slot:header#header
作用域插槽 语法简化
动态插槽 语法一致

4. 总结

  1. 默认插槽:适用于简单的内容插入。
  2. 具名插槽:适用于多区域内容插入。
  3. 作用域插槽:适用于子组件向父组件传递数据。
  4. 动态插槽:适用于动态切换插槽内容。

在 Vue 3 中,v-slot: 语法被简化为 #,推荐使用 setup 语法更清晰。

11.provide/inject—>依赖注入

## **`provide/inject` 依赖注入机制(Vue 2 & Vue 3)**

1. provide/inject 作用

provide/inject 是 Vue 提供的 跨层级组件通信 方式,允许祖先组件(provide)向后代组件(inject)提供数据,而无需逐级通过 props 传递。

适用于:

  • 组件层级较深,不适合 props 逐层传递。
  • 祖先组件需要向多个子孙组件共享状态。
  • 适用于全局配置(如主题、国际化等)。

2. Vue 2 provide/inject 用法

父组件(提供数据)
<!-- Parent.vue -->
<template>
  <Child />
</template>

<script>
export default {
  provide() {
    return {
      themeColor: 'blue'
    };
  }
};
</script>
子组件(注入数据)
<!-- Child.vue -->
<template>
  <div :style="{ color: themeColor }">我是子组件</div>
</template>

<script>
export default {
  inject: ['themeColor']
};
</script>

🔹 优点:跨层级传递 themeColor,避免 props 逐层传递。
🔹 缺点:Vue 2 版本的 provide 不是响应式的(数据变更不会影响子组件)。


3. Vue 3 provide/inject 用法

Vue 3 进行了优化,使 provide 可以 支持响应式数据

父组件(提供数据,可响应)
<!-- Parent.vue -->
<template>
  <button @click="changeColor">切换颜色</button>
  <Child />
</template>

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

const themeColor = ref('blue');

provide('themeColor', themeColor);

const changeColor = () => {
  themeColor.value = themeColor.value === 'blue' ? 'red' : 'blue';
};
</script>
子组件(注入数据)
<!-- Child.vue -->
<template>
  <div :style="{ color: themeColor }">我是子组件</div>
</template>

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

const themeColor = inject('themeColor');
</script>

🔹 Vue 3 使 provide 支持 ref,变更 themeColor 会自动更新子组件


4. provide/inject vs props vs Vuex/Pinia

方式 适用场景 数据是否响应式 适用范围
props 父组件向子组件传递数据 单层组件通信
provide/inject 祖先组件向后代组件提供数据 Vue 2 ❌ / Vue 3 ✅ 深层组件通信
Vuex/Pinia 全局状态管理 适用于大型应用

5. 适用场景

  • 深层组件通信(避免props 层层传递)
  • 全局配置(如主题、国际化)
  • 插件开发(如第三方库需要全局共享数据)

在 Vue 3 中,provide/inject 支持响应式,比 Vue 2 更强大,是 Vue 3 组件通信的重要方式之一。

12.vue中你如何封装组件(vue2,3)

## **Vue 组件封装(Vue 2 & Vue 3)**

封装 Vue 组件的目的是提高代码复用性、减少冗余、提升可维护性。Vue 2 和 Vue 3 的组件封装思路类似,但 Vue 3 提供了更现代的 Composition APIsetup),让封装更加灵活。


1. Vue 2 组件封装

① 基本封装(props 传参)

Vue 2 组件通常基于 Options API,使用 props 进行封装。

封装一个通用按钮组件
<!-- components/BaseButton.vue -->
<template>
  <button :class="['btn', type]" @click="$emit('click')">
    <slot></slot>
  </button>
</template>

<script>
export default {
  props: {
    type: {
      type: String,
      default: 'primary' // 支持 primary / secondary / danger
    }
  }
};
</script>

<style scoped>
.btn {
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}
.primary {
  background: blue;
  color: white;
}
.secondary {
  background: gray;
  color: white;
}
.danger {
  background: red;
  color: white;
}
</style>
在父组件中使用
<BaseButton type="danger" @click="handleClick">删除</BaseButton>

🔹 特点:通过 props 传递 type,通过 slot 传递内容,并通过 $emit 触发事件。


② 具名插槽封装复杂 UI

封装一个通用弹窗组件

<!-- components/Modal.vue -->
<template>
  <div v-if="visible" class="modal">
    <div class="modal-content">
      <header class="modal-header">
        <slot name="header">默认标题</slot>
      </header>
      <main class="modal-body">
        <slot>默认内容</slot>
      </main>
      <footer class="modal-footer">
        <slot name="footer">
          <button @click="close">关闭</button>
        </slot>
      </footer>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    visible: Boolean
  },
  methods: {
    close() {
      this.$emit('update:visible', false);
    }
  }
};
</script>

<style>
/* 省略样式 */
</style>
在父组件中使用
<Modal v-model="showModal">
  <template v-slot:header>自定义标题</template>
  <template>这里是内容</template>
  <template v-slot:footer>
    <button @click="showModal = false">关闭</button>
  </template>
</Modal>

🔹 特点

  • v-model 语法糖,简化 visibleprops$emit
  • 通过 slot 让内容更灵活。

③ 通过 mixins 复用逻辑

mixins 适用于 Vue 2,但容易造成命名冲突。

创建一个 mixin.js
export default {
  data() {
    return {
      loading: false
    };
  },
  methods: {
    fetchData() {
      this.loading = true;
      setTimeout(() => {
        this.loading = false;
      }, 1000);
    }
  }
};
在组件中使用
<script>
import loadingMixin from '@/mixins/loadingMixin';

export default {
  mixins: [loadingMixin],
  mounted() {
    this.fetchData();
  }
};
</script>

🔹 Vue 2 的 mixins 缺点:变量和方法容易冲突,不够直观。


2. Vue 3 组件封装

Vue 3 主要使用 Composition API,更易于封装和复用逻辑。

① Vue 3 基础组件封装

封装 BaseButton.vue
<template>
  <button :class="['btn', type]" @click="emit('click')">
    <slot></slot>
  </button>
</template>

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

defineProps({
  type: { type: String, default: 'primary' }
});
const emit = defineEmits(['click']);
</script>

🔹 特点

  • 采用 setup 语法,defineProps 处理 propsdefineEmits 处理事件。
  • 语法更简洁。

② 组合式 API (composition API) 复用逻辑

Vue 3 推荐用 composables 封装逻辑,代替 Vue 2 的 mixins

封装 useFetch.js
import { ref } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const loading = ref(true);

  fetch(url)
    .then(res => res.json())
    .then(json => {
      data.value = json;
      loading.value = false;
    });

  return { data, loading };
}
在组件中使用
<script setup>
import { useFetch } from '@/composables/useFetch';

const { data, loading } = useFetch('https://api.example.com/data');
</script>

🔹 优势

  • 避免 mixins 命名冲突
  • 更清晰的逻辑组织
  • 可复用性更强

③ 封装 provide/inject 组件

Vue 3 provide/inject 支持响应式,可用于全局配置(如主题、国际化)。

提供数据
<script setup>
import { ref, provide } from 'vue';

const themeColor = ref('blue');
provide('themeColor', themeColor);
</script>
注入数据
<script setup>
import { inject } from 'vue';

const themeColor = inject('themeColor');
</script>

<template>
  <div :style="{ color: themeColor }">我是子组件</div>
</template>

🔹 适用于深层嵌套组件通信


④ 封装全局组件

Vue 3 可以用 app.component() 注册全局组件:

import { createApp } from 'vue';
import BaseButton from '@/components/BaseButton.vue';

const app = createApp(App);
app.component('BaseButton', BaseButton);
app.mount('#app');

🔹 这样 BaseButton 可以在整个项目中直接使用


3. Vue 2 vs Vue 3 组件封装对比

对比点 Vue 2 Vue 3
组件 API Options API (data/methods) Composition API (setup)
逻辑复用 mixins(易冲突) composables(更清晰)
依赖注入 provide/inject(非响应式) provide/inject(响应式)
全局组件注册 Vue.component() app.component()

4. 总结

  1. 基础封装:组件 props + slot 让 UI 组件更灵活。

  2. 逻辑复用

    • Vue 2:mixins(缺点:变量冲突)。
    • Vue 3:composables(推荐)。
  3. 依赖注入

    • Vue 3 支持响应式 provide/inject,更适合全局共享数据。
  4. Vue 3 更简洁

    • setup 语法 + defineProps + defineEmits 让代码更清晰。

如果是新项目,建议直接用 Vue 3 进行组件封装,代码更简洁、逻辑更易复用!🚀

关于Vuex

13.1 Vuex有哪些属性,Vuex如何使用state值,Vuex的getters值修改,Vuex的mutations和actions区别,Vuex持久化存储

Vuex 核心属性 & 使用

Vuex 是 Vue 2 的官方状态管理库,主要用于管理全局共享状态。它的核心属性包括:

  1. state(状态):存储应用数据。
  2. getters(计算属性):类似 computed,从 state 派生数据。
  3. mutations(同步修改 state):唯一能直接修改 state 的方法。
  4. actions(异步操作):提交 mutations,处理异步逻辑(如 API 请求)。
  5. modules(模块化):拆分 Vuex 结构,便于管理。

1. Vuex 如何使用 state

在组件中使用 state

<template>
  <p>用户名:{{ username }}</p>
</template>

<script>
import { mapState } from 'vuex';

export default {
  computed: {
    // 直接访问
    username() {
      return this.$store.state.user.name;
    },
    // 使用 mapState 辅助函数
    ...mapState({
      username: state => state.user.name
    })
  }
};
</script>

🔹 mapState 可以映射多个 state,减少代码量。


2. Vuex 的 getters 值修改

定义 getters

const store = new Vuex.Store({
  state: {
    count: 10
  },
  getters: {
    doubleCount: state => state.count * 2
  }
});

在组件中使用

<template>
  <p>双倍数量:{{ doubleCount }}</p>
</template>

<script>
import { mapGetters } from 'vuex';

export default {
  computed: {
    ...mapGetters(['doubleCount'])
  }
};
</script>

🔹 getters 不能直接修改,需要通过 mutations 修改 state,然后 getters 会自动更新。


3. mutationsactions 的区别

对比项 mutations actions
修改 state ✅ 直接修改 ❌ 不能修改
是否支持异步 ❌ 只能同步 ✅ 适用于异步操作
调用方式 commit 提交 dispatch 分发

示例:mutations 修改 state

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload;
    }
  }
});

调用:

this.$store.commit('increment', 2);

示例:actions 异步调用 mutations

const store = new Vuex.Store({
  actions: {
    asyncIncrement({ commit }, payload) {
      setTimeout(() => {
        commit('increment', payload);
      }, 1000);
    }
  }
});

调用:

this.$store.dispatch('asyncIncrement', 2);

🔹 mutations 必须同步,actions 可以异步。


4. Vuex 持久化存储

Vuex 默认存储在内存中,刷新页面会丢失数据。持久化存储的常见方案:

  1. 本地存储(localStorage/sessionStorage)
  2. Vuex 持久化插件(vuex-persistedstate

方法 1:手动存储到 localStorage

const store = new Vuex.Store({
  state: {
    user: JSON.parse(localStorage.getItem('user')) || {}
  },
  mutations: {
    setUser(state, user) {
      state.user = user;
      localStorage.setItem('user', JSON.stringify(user));
    }
  }
});

🔹 缺点:需要手动存储和读取。


方法 2:使用 vuex-persistedstate

安装:

npm install vuex-persistedstate

使用:

import createPersistedState from 'vuex-persistedstate';

const store = new Vuex.Store({
  state: {
    user: {}
  },
  plugins: [createPersistedState()] // 持久化所有 state
});

🔹 推荐,自动同步 localStorage,简单易用。


5. Vuex 适用场景

适用

  • 需要多个组件共享状态,如用户信息、购物车数据。
  • 需要集中管理复杂的状态逻辑(如权限、异步数据)。
  • 需要对状态进行时间旅行(如撤销/重做)。

不适用

  • 简单组件间通信(可用 propsemitprovide/inject)。
  • 只在某个组件中用到的状态(可用 refreactive)。

6. 总结

  • Vuex 主要属性state(数据)、getters(计算属性)、mutations(同步修改)、actions(异步操作)、modules(模块化)。
  • state 通过 mapState 使用getters 通过 mapGetters 计算派生数据。
  • mutations 只能同步修改 stateactions 可异步操作
  • Vuex 持久化存储可用 localStoragevuex-persistedstate 插件
  • Vuex 适用于复杂应用,Vue 3 推荐用 Pinia 代替 Vuex(更轻量,支持 TS)。

🚀 如果是 Vue 3,建议使用 Pinia 替代 Vuex,性能更优,语法更简洁!

关于路由

14.vue2和3关于路由的模式和区别

在 Vue 2 和 Vue 3 中,路由的核心库都是 `vue-router`,但 Vue 3 版本使用的是 `vue-router@4`,相较于 Vue 2 的 `vue-router@3` 有一些重要变化。以下是 Vue 2 和 Vue 3 在路由模式和实现上的区别:

1. Vue 2 和 Vue 3 的路由模式

Vue Router 提供两种模式:

  1. hash 模式(默认):基于 URL # 号的哈希值,使用 location.hash 进行更新,无需服务器配置。
  2. history 模式:使用 HTML5 的 history.pushStatehistory.replaceState,URL 无 # 号,需服务器支持。

Vue 2

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const router = new VueRouter({
  mode: 'history', // 或者 'hash'
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
});

export default router;

Vue 3

import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(), // 或者 createWebHashHistory()
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
});

export default router;

🔹 区别

  • Vue 3 使用 createRouter 创建路由实例,而 Vue 2 直接用 new VueRouter()
  • Vue 3 使用 createWebHistory()createWebHashHistory() 代替 mode 配置。

2. Vue 2 和 Vue 3 的路由 API 变化

(1)路由实例的创建

版本 Vue 2 Vue 3
创建路由 new VueRouter({}) createRouter({})
路由模式 mode: 'history' createWebHistory()

(2)获取路由实例

Vue 2:

this.$router.push('/home'); // 访问页面
console.log(this.$route.path); // 获取当前路径

Vue 3(推荐 useRouteuseRouter):

import { useRoute, useRouter } from 'vue-router';

const route = useRoute();
const router = useRouter();

router.push('/home');
console.log(route.path);

🔹 区别

  • Vue 3 推荐使用 useRoute()useRouter() 代替 this.$routerthis.$route,更符合组合式 API 设计。

3. beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave 的区别

Vue 2 中,beforeRouteEnter 只能用于 选项式 API

export default {
  beforeRouteEnter(to, from, next) {
    next(vm => {
      console.log(vm); // 访问组件实例
    });
  }
};

Vue 3 中,推荐使用 onBeforeRouteLeaveonBeforeRouteUpdate

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';

onBeforeRouteLeave(() => {
  console.log('组件即将离开');
});

onBeforeRouteUpdate(() => {
  console.log('路由更新,但组件未销毁');
});

🔹 区别

  • Vue 3 推荐在组合式 API 中使用 onBeforeRouteLeaveonBeforeRouteUpdate,代码更清晰。

4. Vue 3 的动态路由与 Vue 2 的区别

Vue 2:

const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User }
  ]
});

Vue 3:

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/user/:id', component: User }
  ]
});

动态参数获取:

import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.params.id); // Vue 3

🔹 区别

  • Vue 3 推荐 useRoute().params 代替 this.$route.params

5. Vue 2 和 Vue 3 的路由懒加载

Vue 2(import() 语法)

const Home = () => import('@/views/Home.vue');

const router = new VueRouter({
  routes: [
    { path: '/', component: Home }
  ]
});

Vue 3(更清晰)

const routes = [
  { path: '/', component: () => import('@/views/Home.vue') }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

🔹 区别

  • Vue 3 仍然支持 Vue 2 的懒加载方式,但通常更推荐写在 routes 里。

6. Vue 3 路由的 Suspense 支持

Vue 3 允许 Suspense 组件处理异步路由:

<Suspense>
  <template #default>
    <router-view />
  </template>
  <template #fallback>
    <LoadingSpinner />
  </template>
</Suspense>

🔹 区别

  • Vue 3 可直接用 Suspense 处理异步组件,提高用户体验。

7. 总结

关键点 Vue 2 Vue 3
路由创建 new VueRouter({}) createRouter({})
mode 'history' / 'hash' createWebHistory() / createWebHashHistory()
访问 route this.$route useRoute()
访问 router this.$router useRouter()
路由导航守卫 beforeRouteEnter onBeforeRouteLeaveonBeforeRouteUpdate
动态参数 this.$route.params useRoute().params
组件懒加载 import() import()
Suspense 支持

结论

  1. Vue 3 取消了 Vue 2 的 mode,改为 createWebHistory()createWebHashHistory()
  2. Vue 3 推荐 useRoute()useRouter() 取代 this.$routethis.$router,更适配 Composition API。
  3. Vue 3 允许用 Suspense 处理异步组件,提高渲染效率。
  4. Vue 2 代码仍然可用,但 Vue 3 提供更简洁、现代化的写法。

hash模式和history模式的区别

Vue Router 提供两种路由模式:Hash 模式(hashHistory 模式(history 。它们的主要区别如下:


1. Hash 模式

特点

  • URL 以 # 号(hash)作为分隔符,例如:

    https://example.com/#/home
    
  • 不需要服务器配置,前端直接管理路由。

  • 依赖 window.location.hash 变化监听 hashchange 事件。

  • 适用于 静态网站,因为浏览器不会将 # 后的部分发送到服务器。

实现

const router = createRouter({
  history: createWebHashHistory(), // 使用 Hash 模式
  routes: [
    { path: '/home', component: Home }
  ]
});

工作原理

  • # 后面的内容不会被发送到服务器,前端 JavaScript 监听 URL 变化,渲染对应的组件。
  • location.hash = '/about' 会触发 hashchange 事件,Vue Router 会根据 #/about 解析路由。

2. History 模式

特点

  • URL 结构更干净,没有 # 号,例如:

    https://example.com/home
    
  • 依赖 HTML5 History APIpushStatereplaceState)。

  • 需要服务器支持,否则刷新页面会导致 404

  • 适用于 现代 Web 应用,更符合 SEO 需求。

实现

const router = createRouter({
  history: createWebHistory(), // 使用 History 模式
  routes: [
    { path: '/home', component: Home }
  ]
});

工作原理

  • Vue Router 通过 pushState 修改 window.history,不会触发页面刷新。
  • 但服务器端需要配置 URL 重写,否则刷新页面时服务器找不到对应的路径,会返回 404

Nginx 配置示例

location / {
  try_files $uri /index.html;
}
  • 这样,所有请求都会重定向到 index.html,由 Vue 处理前端路由。

3. Hash 模式 vs History 模式对比

对比项 Hash 模式 History 模式
URL 结构 https://example.com/#/home https://example.com/home
依赖 API hashchange 事件 pushState / replaceState
刷新是否会 404 ❌ 否,后端不处理 # ✅ 是,需要服务器配置
SEO 友好 ❌ 不友好(搜索引擎不解析 # ✅ 友好(可被爬取)
服务器支持 ✅ 无需后端支持 ❌ 需服务器配置
适用场景 小型应用,静态站点 需要 SEO 和良好 URL 结构的应用

4. 选择哪种模式?

  • Hash 模式

    • 适合 不需要服务器端配置 的单页面应用。
    • 适合 GitHub Pages、静态站点 等托管环境。
    • SEO 不重要 的应用。
  • History 模式

    • 适合 需要 SEO 的应用,如博客、企业网站。
    • 适合 现代 Web 应用,如 SSR(Nuxt.js)。
    • 需要 服务器配置 以防止 404

🚀 推荐

  • 开发阶段:使用 Hash 模式,简单方便。
  • 生产环境:如果有服务器支持,优先选择 History 模式,提供更好的用户体验。

💡 总结

  • Hash 模式 适合前端单页面应用,简单无需服务器配置,但 URL 丑陋,SEO 差。
  • History 模式 需要服务器支持,但 URL 结构清晰、SEO 友好,推荐在生产环境使用。

15.路由其他

1 子路由和动态路由

2 路由传值

3 导航故障

4 r o u t e r 和 router和 routerroute区别

5导航守卫

4.2 子路由和动态路由

子路由

子路由用于在 父组件 中嵌套 子组件,适用于 多级菜单、嵌套路由 的场景。

示例:

const routes = [
  {
    path: '/parent',
    component: Parent,
    children: [
      { path: 'child', component: Child }  // 访问 /parent/child
    ]
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

特点:

  • children 数组定义子路由
  • 访问 /parent/child 会渲染 Child 组件
  • 需要在 Parent.vue 中使用 <router-view> 渲染子路由

动态路由

动态路由用于匹配 不同参数的路径,适用于 用户详情、文章详情等 场景。

示例:

const routes = [
  { path: '/user/:id', component: User }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

访问 /user/123/user/456 会渲染 User 组件,并可以在 User.vue 里获取动态参数:

import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.params.id);  // 123 or 456

4.3 路由传值

Vue Router 支持三种方式传递参数:

1. 动态路由参数(URL 传参)
{ path: '/user/:id', component: User }

获取:

const route = useRoute();
console.log(route.params.id);
2. Query 传参
router.push({ path: '/user', query: { name: 'Alice' } });

获取:

console.log(route.query.name);
3. 路由 props 传参
{ path: '/user/:id', component: User, props: true }

User.vue 直接作为 props 获取:

<script setup>
defineProps(['id']);
</script>

4.4 导航故障

导航故障(Navigation Failure) 可能发生在:

  • 重复跳转同一路由
  • beforeEach 拦截
  • beforeRouteLeave 阻止

示例:

router.push('/home').catch(err => console.log(err));

解决方案:

  • 忽略重复导航
router.push('/home').catch(err => err);
  • 检查路由是否发生变化
if (route.path !== '/home') router.push('/home');

4.5 $router$route 的区别

$router $route
类型 VueRouter 实例 当前路由对象
作用 用于导航(跳转、替换) 用于获取路由信息
例子 this.$router.push('/home') this.$route.params.id

在 Vue3 setup 语法中:

import { useRouter, useRoute } from 'vue-router';
const router = useRouter(); // 等同于 this.$router
const route = useRoute(); // 等同于 this.$route

4.6 导航守卫

导航守卫用于 控制路由跳转,比如:

  • 身份验证
  • 权限控制
  • 页面缓存
1. 全局守卫

(1) beforeEach(进入前拦截)

router.beforeEach((to, from, next) => {
  if (!isAuthenticated() && to.path !== '/login') {
    next('/login');  // 未登录跳转
  } else {
    next(); // 允许通行
  }
});

(2) beforeResolve(解析前)

router.beforeResolve((to, from, next) => {
  console.log('路由解析前');
  next();
});

(3) afterEach(进入后)

router.afterEach((to, from) => {
  console.log('进入路由', to.path);
});

2. 组件内守卫

(1) beforeRouteEnter

beforeRouteEnter(to, from, next) {
  next(vm => {
    console.log(vm); // 访问组件实例
  });
}

(2) beforeRouteUpdate

beforeRouteUpdate(to, from, next) {
  console.log('路由参数变化');
  next();
}

(3) beforeRouteLeave

beforeRouteLeave(to, from, next) {
  if (!confirm('确定要离开吗?')) {
    next(false);  // 阻止跳转
  } else {
    next();
  }
}

3. 路由独享守卫
const routes = [
  { 
    path: '/admin', 
    component: Admin,
    beforeEnter: (to, from, next) => {
      if (!isAdmin()) next('/login');
      else next();
    }
  }
];

总结

  • 子路由 适用于嵌套结构,children 定义
  • 动态路由 用于参数化页面(/user/:id
  • 路由传参 可以用 paramsqueryprops
  • 导航故障 发生在拦截或重复跳转时
  • $router vs $route:前者用于跳转,后者用于获取信息
  • 导航守卫 用于拦截或控制页面访问

16.关于API

关于 API - 面试题回答 & 使用场景

1 $set

面试官问题: 你有没有碰到过,数据更新视图没有更新的问题?
回答: Vue 2 的响应式系统无法检测到对象新属性的添加,因此直接给对象赋值新属性不会触发视图更新。这时需要使用 $set 来确保数据是响应式的。
使用方式:

this.$set(target, key, value);

使用场景:

  1. 给对象动态添加新属性,确保视图更新

    this.$set(this.user, 'age', 25);
    
  2. 修改数组某个元素,确保响应式更新

    this.$set(this.list, index, { ...this.list[index], name: '新名字' });
    

2 $nextTick

面试官问题: $nextTick 主要解决什么问题?
回答: $nextTick 主要用于在 DOM 更新后执行回调,确保获取到最新的 DOM 状态。Vue 的 DOM 更新是异步的,修改数据后,不能立即获取更新后的 DOM 结构,因此 $nextTick 确保操作的是最新的 DOM。
使用方式:

this.$nextTick(() => {
  console.log(this.$refs.modal.offsetHeight);
});

使用场景:

  1. v-if 控制的 DOM 生成后,获取最新的 DOM

    this.visible = true;
    this.$nextTick(() => {
      console.log(this.$refs.popup); // 确保能获取 DOM
    });
    
  2. 数据变更后,执行某些需要 DOM 变化后的操作

    this.items.push('新数据');
    this.$nextTick(() => {
      console.log(document.querySelector('.item').innerText);
    });
    

3 $refs

面试官问题: $refs 在 Vue 中的作用是什么?
回答: $refs 用于获取 DOM 或子组件实例。
使用方式:

<input ref="myInput" />
this.$refs.myInput.focus();

使用场景:

  1. 获取原生 DOM 元素

    this.$refs.input.focus();
    
  2. 调用子组件方法

    <ChildComponent ref="child" />
    
    this.$refs.child.someMethod();
    

4 $el

面试官问题: $el 的作用是什么?
回答: $el 返回当前 Vue 组件的根 DOM 元素。
使用方式:

console.log(this.$el.tagName);

使用场景:

  1. 操作组件的根元素

    this.$el.style.background = 'red';
    
  2. 判断组件是否挂载

    if (this.$el) {
      console.log('组件已挂载');
    }
    

5 $data

面试官问题: $data 在 Vue 里有什么作用?
回答: $data 访问组件实例的整个数据对象。
使用方式:

console.log(this.$data);

使用场景:

  1. 深拷贝数据

    const snapshot = JSON.parse(JSON.stringify(this.$data));
    
  2. 动态修改数据

    this.$data.userName = '新名字';
    

6 $children

面试官问题: $children$refs 的区别?
回答: $children 获取子组件实例的数组,而 $refs 是获取特定的子组件或 DOM。
使用方式:

console.log(this.$children);

使用场景:

  1. 批量调用子组件方法

    this.$children.forEach(child => child.doSomething());
    

7 $parent

面试官问题: $parent 主要用于什么场景?
回答: $parent 用于访问父组件的实例,通常用于子组件调用父组件的方法或获取父组件的数据。
使用方式:

this.$parent.showMessage('子组件调用父组件方法');

使用场景:

  1. 子组件调用父组件方法

    this.$parent.updateData();
    
  2. 获取父组件数据

    console.log(this.$parent.userName);
    

总结

API 作用 典型使用场景
$set 确保新增属性响应式 动态添加对象属性、更新数组中的对象
$nextTick 确保 DOM 更新后执行回调 获取最新 DOM 结构,避免 v-if 导致的获取失败
$refs 获取 DOM 或子组件实例 操作 DOM、调用子组件方法
$el 获取当前组件根元素 操作组件 DOM
$data 访问整个数据对象 深拷贝数据,做快照
$children 获取子组件实例数组 批量调用子组件方法(不推荐)
$parent 获取父组件实例 调用父组件方法,访问父组件数据

8 $root

作用

$root 指向 Vue 应用的根实例,通常用于在深层组件访问全局数据或方法。

示例
<template>
  <ChildComponent />
</template>

<script>
export default {
  data() {
    return { globalMessage: "Hello from root" };
  }
};
</script>
<template>
  <div>{{ $root.globalMessage }}</div>
</template>

注意:尽量少用 $root,避免让组件对根实例产生强依赖。


9 data 定义数据

Vue 组件的 data 必须是一个 函数,返回对象。

示例
export default {
  data() {
    return {
      count: 0
    };
  }
};

为什么 data 在组件里必须是函数?
因为组件是可复用的,每个实例都应该有自己的数据副本,避免多个组件共享同一数据对象。


10 computed 计算属性

作用
  • 用于基于已有数据计算新数据
  • 缓存结果,依赖的值不变时不会重复计算。
示例
export default {
  data() {
    return { firstName: "John", lastName: "Doe" };
  },
  computed: {
    fullName() {
      return this.firstName + " " + this.lastName;
    }
  }
};

特点

  • 依赖数据变更时才会重新计算
  • 结果会缓存,提高性能。

11 watch 监听

作用
  • 监听 data/computed 的变化,执行异步或副作用操作(如 API 请求)。
  • 适用于监听多个依赖或异步操作
示例
export default {
  data() {
    return { query: "" };
  },
  watch: {
    query(newVal) {
      console.log("搜索内容变化:", newVal);
      this.fetchData(newVal);
    }
  },
  methods: {
    fetchData(query) {
      console.log(`Fetching data for: ${query}`);
    }
  }
};

特点

  • computed 适合计算属性watch 适合监听副作用
  • 可以深度监听对象
watch: {
  user: {
    handler(newVal) {
      console.log("User info updated:", newVal);
    },
    deep: true
  }
}

12 methodscomputed 区别

特性 methods computed
适用场景 事件处理,逻辑运算 依赖现有数据计算新值
是否缓存 不缓存,每次调用都会执行 缓存,依赖不变不重新计算
适用情况 需要执行操作,不关心返回值 计算值依赖 data,并用于模板中
示例
computed: {
  fullName() {
    return this.firstName + " " + this.lastName; // 依赖 firstName 和 lastName
  }
},
methods: {
  getFullName() {
    return this.firstName + " " + this.lastName;
  }
}

区别

  • computed 适用于数据依赖计算,避免重复执行性能更好
  • methods 每次调用都会重新执行,适用于事件处理无缓存需求的计算

总结

  • $root:访问根实例,避免滥用

  • data:组件 data 必须是函数,避免数据污染。

  • computed缓存计算结果,适用于依赖数据变化的计算。

  • watch监听数据变化,适合异步请求或副作用操作。

  • methods vs computed

    • computed 有缓存,适用于计算值
    • methods 无缓存,每次调用都会执行

17.关于指令 - 面试题回答**

6.1 如何自定义指令

面试官问题: Vue 如何自定义指令?自定义指令的使用场景有哪些?
回答:
Vue 提供 directive API 允许开发者创建自定义指令,可以在 DOM 绑定时执行特定的行为。

使用方式:

  • 全局指令

    Vue.directive('focus', {
      inserted(el) {
        el.focus();
      }
    });
    

    使用:

    <input v-focus />
    
  • 局部指令

    directives: {
      color: {
        bind(el, binding) {
          el.style.color = binding.value;
        }
      }
    }
    

    使用:

    <p v-color="'red'">我是红色的</p>
    

使用场景:

  1. 自动聚焦输入框

    Vue.directive('focus', {
      inserted(el) {
        el.focus();
      }
    });
    
  2. 拖拽元素

    Vue.directive('drag', {
      bind(el) {
        el.onmousedown = function (e) { /* 拖拽逻辑 */ };
      }
    });
    

6.2 Vue 单项绑定

面试官问题: Vue 单项绑定的含义是什么?如何实现双向绑定?
回答:

  • 单项绑定 (v-bind)
    数据只能从 Vue 实例 → 视图,不能反向修改。

    <p v-bind:title="message">悬停查看</p>
    

    等价于:

    <p :title="message">悬停查看</p>
    
  • 双向绑定 (v-model)
    v-model 适用于表单元素,使数据和视图保持同步。

    <input v-model="username" />
    

使用场景:

  1. 单项绑定:用于 props 传值,避免子组件直接修改父组件的数据。
  2. 双向绑定:用于表单输入框、搜索框等需要用户输入的地方。

6.3 v-ifv-for 优先级

面试官问题: v-ifv-for 同时存在时,哪个优先级高?如何优化?
回答:

  • v-for 的优先级 高于 v-if,即 v-for 先执行,再判断 v-if

    <div v-for="item in list" v-if="item.show">{{ item.name }}</div>
    

    问题: 每个 item 都会被遍历,再执行 v-if,可能影响性能。
    优化方案:

    1. 外层包裹 v-if,减少不必要的遍历

      <template v-if="list.length">
        <div v-for="item in list" :key="item.id">{{ item.name }}</div>
      </template>
      
    2. 使用计算属性先过滤数据

      computed: {
        filteredList() {
          return this.list.filter(item => item.show);
        }
      }
      
      <div v-for="item in filteredList" :key="item.id">{{ item.name }}</div>
      

总结:

指令 作用 典型使用场景
v-bind 单项绑定 动态修改属性(如 classstyle
v-model 双向绑定 表单元素(如 inputtextarea
v-if 条件渲染(销毁/重建 按需加载组件
v-show 控制显示(CSS 隐藏 频繁切换内容
v-for 列表渲染 渲染数组数据
v-if vs v-for v-for 优先级高 优化:用 computed 过滤

掌握这些细节,在 Vue 面试中可以更好地回答相关问题!

18.关于原理

1 $nextTick 原理

1. $nextTick 作用

$nextTick 用于在 DOM 更新完成后 运行回调函数,适用于需要等待视图更新后执行操作的场景,例如获取更新后的 DOM 信息。

<template>
  <div ref="box">{{ message }}</div>
  <button @click="updateMessage">更新</button>
</template>

<script>
export default {
  data() {
    return {
      message: "Hello Vue"
    };
  },
  methods: {
    updateMessage() {
      this.message = "Updated!";
      this.$nextTick(() => {
        console.log(this.$refs.box.innerText); // "Updated!"
      });
    }
  }
};
</script>

如果不使用 $nextTick,可能会获取到未更新的 DOM


2. $nextTick 的原理

Vue 异步更新 DOM,当数据发生变化时:

  1. Vue 不会立即更新 DOM,而是将更新任务放入异步任务队列
  2. 批量执行更新,提高性能。
  3. $nextTick 通过 Promise.then / MutationObserver / setTimeout 等方式,将回调放入 微任务队列,等 Vue 更新 DOM 之后再执行。

核心实现(简化版)

const callbacks = [];
let pending = false;

function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    Promise.resolve().then(flushCallbacks);
  }
}

function flushCallbacks() {
  pending = false;
  callbacks.forEach(cb => cb());
  callbacks.length = 0;
}

2 双向绑定原理

1. Vue2 的双向绑定(基于 Object.defineProperty

Vue2 通过 Object.defineProperty 劫持对象属性的 gettersetter,在数据变化时通知视图更新,形成双向绑定

核心实现(简化版):

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`读取:${key}`);
      return val;
    },
    set(newVal) {
      console.log(`更新:${key} = ${newVal}`);
      val = newVal; // 触发视图更新
    }
  });
}

const data = {};
defineReactive(data, "message", "Hello Vue");
data.message = "Updated!"; // 触发 setter

2. Vue3 的双向绑定(基于 Proxy)

Vue3 使用 Proxy 代理整个对象,可以监听新增/删除属性,更强大更灵活。

核心实现(简化版):

const reactiveHandler = {
  get(target, key) {
    console.log(`读取:${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`更新:${key} = ${value}`);
    target[key] = value; // 触发视图更新
    return true;
  }
};

const data = new Proxy({}, reactiveHandler);
data.message = "Hello Vue3"; // 触发 Proxy

Vue2 vs Vue3 双向绑定对比

版本 方式 优势 劣势
Vue2 Object.defineProperty 兼容性好 不能监听数组索引和新增属性
Vue3 Proxy 监听整个对象,支持数组和新增属性 兼容性稍差(IE 不支持)

总结

  • $nextTick 主要用于等待 Vue 更新 DOM 后执行回调,原理是利用 Promise、MutationObserver 等实现微任务队列
  • Vue2 采用 Object.defineProperty 监听已有属性,Vue3 使用 Proxy 代理整个对象,实现更灵活的响应式系统

19.关于axios二次封装

### **Vue 中 Axios 二次封装面试题解析**

在 Vue 项目中,Axios 是常用的 HTTP 请求库。为了提高可维护性,通常会对 Axios 进行二次封装,提供更好的错误处理、请求拦截、全局配置等功能。


1. 为什么要对 Axios 进行二次封装?

回答思路

  • 减少代码重复:每个请求都要手动设置 headersbaseURLtimeout 等,封装后可全局管理。
  • 优化错误处理:统一处理网络异常、超时、接口错误,避免多个组件重复写逻辑。
  • 简化请求方式:封装 getpost 等方法,让请求更简洁。
  • 拦截器:在请求前添加 token,在响应后处理错误
  • 支持取消请求:避免用户频繁操作导致的并发请求问题

2. 如何封装 Axios?

回答思路

  • 创建 axios 实例
  • 添加请求拦截器(统一添加 token)
  • 添加响应拦截器(统一处理错误)
  • 封装 getpost 方法
  • 支持请求取消

示例

import axios from "axios";
import { ElMessage } from "element-plus"; // 适用于 Vue3

// 创建 Axios 实例
const service = axios.create({
  baseURL: "https://api.example.com", // API 基础路径
  timeout: 5000 // 请求超时时间
});

// 请求拦截器:在请求前添加 token
service.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem("token");
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器:处理错误
service.interceptors.response.use(
  (response) => {
    return response.data; // 直接返回数据,避免每次都 .data 取值
  },
  (error) => {
    let message = "请求错误";
    if (error.response) {
      switch (error.response.status) {
        case 401:
          message = "未授权,请重新登录";
          break;
        case 403:
          message = "拒绝访问";
          break;
        case 404:
          message = "请求地址不存在";
          break;
        case 500:
          message = "服务器内部错误";
          break;
      }
    } else if (error.message.includes("timeout")) {
      message = "请求超时";
    }
    ElMessage.error(message);
    return Promise.reject(error);
  }
);

// 封装 get 和 post 方法
const request = {
  get(url, params) {
    return service.get(url, { params });
  },
  post(url, data) {
    return service.post(url, data);
  },
  put(url, data) {
    return service.put(url, data);
  },
  delete(url) {
    return service.delete(url);
  }
};

export default request;

3. 如何在 Vue 组件中使用?

回答思路

  • 直接 import 封装后的请求方法
  • async/await 处理异步请求
  • 处理请求异常

示例

<script setup>
import request from "@/utils/request"; // 导入封装的 axios

const fetchData = async () => {
  try {
    const res = await request.get("/users");
    console.log("用户数据:", res);
  } catch (error) {
    console.error("请求失败", error);
  }
};

fetchData();
</script>

4. 如何取消请求?

回答思路

  • 场景:如搜索框,用户频繁输入,每次输入都发请求,可能会导致数据错乱。
  • 解决方案:使用 axios.CancelTokenAbortController 取消未完成的请求。

示例

import axios from "axios";

const controller = new AbortController(); // 创建请求控制器
const service = axios.create({ baseURL: "/api" });

export const fetchUserData = () => {
  return service.get("/user", { signal: controller.signal });
};

// 需要取消请求时调用
export const cancelRequest = () => {
  controller.abort();
};

5. 如何在 Vue 3 中使用全局 Axios?

回答思路

  • 使用 app.config.globalProperties
  • main.js 注册
  • 全局调用 this.$http

示例

import { createApp } from "vue";
import App from "./App.vue";
import request from "@/utils/request";

const app = createApp(App);
app.config.globalProperties.$http = request;
app.mount("#app");

在组件中使用:

this.$http.get("/data").then((res) => console.log(res));

6. Axios vs Fetch

特性 Axios Fetch
封装性 默认 JSON 处理,支持拦截器 需手动解析 res.json()
错误处理 catch 直接捕获 HTTP 错误 仅捕获网络错误,需手动判断 response.ok
取消请求 CancelToken AbortController
支持老浏览器 支持(可用于 IE) 仅支持现代浏览器

总结

  • Axios 更适合 Vue 项目,提供拦截器、自动 JSON 解析、错误处理
  • Fetch 适用于原生 JS,但需手动处理超时、错误

7. 面试高频问题总结

(1)如何封装 Axios?

  • 创建 axios 实例
  • 添加 请求/响应拦截器
  • 封装 getpost 方法
  • 统一 错误处理
  • 支持 请求取消

(2)Axios 拦截器的作用?

  • 请求拦截器

    • 添加 token
    • 统一 headers
  • 响应拦截器

    • 处理 401403500 等错误
    • 解析 data,避免 response.data 冗余

(3)如何处理 Axios 超时?

  • axios.create 设置 timeout
  • 监听 error.message.includes("timeout")

(4)Vue 3 如何全局使用 Axios?

  • app.config.globalProperties.$http = request

(5)如何取消 Axios 请求?

  • AbortControlleraxios.CancelToken

总结

掌握 Vue 中 Axios 的二次封装,不仅能优化代码,还能提升项目的可维护性。面试时重点关注:

  1. 为什么封装 Axios?
  2. 如何使用请求/响应拦截器?
  3. 如何处理错误(401、500)?
  4. 如何封装 getpost 方法?
  5. 如何取消请求?

20. ### **Vue2 和 Vue3 的 Fragment & Tree-Shaking 区别


1. Fragment(多根节点支持)

Vue2 的限制

在 Vue2 中,每个组件必须有一个根节点,否则会报错:

<template>
  <p>第一段</p>
  <p>第二段</p>
</template>

错误:Vue2 组件必须有单一根节点

必须包裹一个 div

<template>
  <div>
    <p>第一段</p>
    <p>第二段</p>
  </div>
</template>

但这会导致:

  • 生成不必要的 div,影响 DOM 结构和样式
  • 增加嵌套层级,使组件结构变得冗余

Vue3 的 Fragment

Vue3 支持多个根节点,可以直接返回多个元素:

<template>
  <p>第一段</p>
  <p>第二段</p>
</template>

Vue3 通过 Fragment 技术,内部会自动包裹多个元素,但不会额外生成 div,优化了 DOM 结构。

Fragment 的好处
  1. 减少无意义的 DOM 节点:避免 div 嵌套层级过深。
  2. 提升渲染性能:减少 div 影响 CSS 选择器的复杂度。
  3. 优化 Flex/Grid 布局:避免多余的 div 影响布局结构。

2. Tree-Shaking(按需导入,减少打包体积)

Vue2 的问题

Vue2 的全局 API,如 Vue.set()Vue.observable(),即使在代码中未使用,也会被打包进最终的文件里。

例如:

import Vue from "vue";
  • Vue2 的打包 会包含所有 Vue 提供的 API,即使你只用了部分功能,最终导致打包体积较大
  • 这在大型应用中,会严重影响加载速度,尤其是移动端设备。

Vue3 的 Tree-Shaking

Vue3 采用 ES Module 方式,支持 按需加载,未使用的代码不会被打包,极大地优化了体积。

Vue3 按需导入示例
import { ref, computed } from "vue";
  • 只导入 refcomputed,其他未使用的 API 不会被打包
  • 减少最终的 bundle 体积,提高加载速度。
Tree-Shaking 的优势
  1. 减少不必要的代码打包:提高应用运行效率。
  2. 优化前端性能:减少 JS 文件大小,加快页面加载速度。
  3. 适用于 Webpack/Rollup/Vite:更好地兼容现代前端工具链。

总结

特性 Vue2 Vue3
Fragment 需要 div 作为根节点 支持多个根节点,减少冗余 DOM
Tree-Shaking 全局 API 被强制打包 采用 ES Module,按需加载
性能优化 DOM 结构复杂,渲染效率低 精简 DOM,提升渲染性能
打包体积 代码冗余较多,影响加载速度 仅包含用到的 API,减少 JS 体积

Vue3 的 Fragment 和 Tree-Shaking 的优势

  • 更轻量:Vue3 移除了不必要的全局 API,优化了 Tree-Shaking 。
  • 更灵活:Vue3 支持多个根节点,避免 div 嵌套问题 。
  • 更高效:Vue3 减少 JS 文件大小,加快应用启动速度 。

21.**Vue2 vs Vue3 区别


1. Vue2 和 Vue3 的区别?

对比项 Vue2 Vue3
响应式原理 Object.defineProperty() Proxy(性能更优,支持更多数据操作)
Composition API 提供 setup,支持更灵活的代码组织
全局 API Vue.component()Vue.mixin() 迁移到 app.config.globalProperties
Teleport 内置 Teleport,支持将组件渲染到指定位置
Fragment 需要 div 包裹 直接支持多个根节点
生命周期 beforeCreatecreated 统一 setup 逻辑,更简洁
Tree-shaking 体积较大 支持按需导入,减少打包体积

22 Vue3 中如何使用 setup 组织代码?

setup 中:

  • 直接使用 ref()reactive() 定义数据
  • 使用 computed() 定义计算属性
  • 监听数据变化用 watch()watchEffect()
  • 通过 onMounted() 等钩子管理生命周期

示例:

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

const count = ref(0);
const double = computed(() => count.value * 2);

onMounted(() => {
  console.log("组件已挂载");
});
</script>

<template>
  <button @click="count++">点击:{{ count }}</button>
  <p>双倍值:{{ double }}</p>
</template>

23 Vue3 setup 如何获取 Vue2 中的 this

setup 中,this 不可用,但可以通过 getCurrentInstance() 获取组件实例:

import { getCurrentInstance } from 'vue';

const instance = getCurrentInstance();
console.log(instance.proxy); // 相当于 Vue2 的 this

常见用途:

  • instance.proxy.$emit() 触发事件
  • instance.proxy.$router 访问路由
  • instance.proxy.$store 访问 Vuex(Vuex 4)

24 Vue3 常用 API

API 作用
ref() 定义基本类型响应式数据
reactive() 定义复杂对象的响应式数据
computed() 计算属性
watch() 监听数据变化
watchEffect() 自动运行监听
onMounted() 组件挂载时执行
onUnmounted() 组件销毁时执行
provide/inject 依赖注入,组件通信
getCurrentInstance() 获取当前组件实例
defineProps() 组件接收 props
defineEmits() 组件事件触发

25 Vue3 常见的响应式数据类型

响应式数据 适用场景 备注
ref(value) 基础数据类型 .value 访问
reactive(object) 复杂对象 直接操作对象属性
readonly(obj) 只读数据 防止数据被修改
shallowRef(value) 浅层响应式 仅对顶层数据响应式
shallowReactive(obj) 浅层响应式对象 适用于性能优化
toRefs(obj) 响应式对象转换为 ref 用于解构 reactive 数据

示例:

const state = reactive({ count: 0 });
const count = toRefs(state).count;

26 Vue3 Teleport 组件及使用场景

作用: Teleport 允许将组件的 DOM 结构渲染到 Vue 组件树外的 HTML 位置,适用于:

  • 模态框(Dialog)
  • 通知(Notification)
  • 全屏遮罩(Overlay)

示例:

<template>
  <button @click="show = true">打开弹窗</button>
  <Teleport to="body">
    <div v-if="show" class="modal">
      <p>我是弹窗</p>
      <button @click="show = false">关闭</button>
    </div>
  </Teleport>
</template>

<script setup>
import { ref } from "vue";
const show = ref(false);
</script>

优点:

  • 避免层级限制,弹窗不受父组件 overflow: hidden 影响
  • 提高代码可读性和结构清晰度

总结

  1. Vue3 响应式优化Proxy)、Composition API 让代码更清晰。
  2. setup 代替 Vue2 选项式 API,更灵活地组织逻辑。
  3. getCurrentInstance() 获取 this 访问组件实例。
  4. 常用 API ref()reactive()computed()watch() 提高开发效率。
  5. Teleport 让模态框等组件更易管理,提升用户体验。

27.# Vue 中的渐进式框架(Progressive Framework)

1. 什么是渐进式框架?

Vue 被称为渐进式框架(Progressive Framework) ,意味着 它可以根据项目需求逐步引入不同的功能,而不是一开始就要求使用完整的框架。Vue 既可以用作一个简单的 UI 库,也可以扩展成一个功能完整的前端框架。
在vue.js的核心库上按需不断的增加vuex,vue-router…渐进式完善


2. Vue 为什么是渐进式的?

Vue 采用模块化设计,你可以根据项目规模和需求逐步引入以下功能:

  1. 仅使用核心功能(轻量级)

    • 直接使用 <script> 引入 Vue,仅用于数据绑定和模板渲染。
    • 适合简单的页面交互,如表单绑定、动态显示数据。
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <div id="app">{{ message }}</div>
    <script>
      Vue.createApp({ data() { return { message: "Hello Vue!" }; } }).mount("#app");
    </script>
    
  2. 组件化开发(中等规模应用)

    • 使用 单文件组件(SFC) ,提高代码复用性,方便维护。
    • 适用于中等规模的单页面应用(SPA)。
    <template>
      <h1>{{ title }}</h1>
    </template>
    <script>
    export default {
      data() {
        return { title: "Vue Component" };
      },
    };
    </script>
    
  3. 完整生态(大型应用)

    • Vue Router:管理前端路由,实现单页面应用(SPA)。
    • Vuex / Pinia:状态管理,适用于复杂数据交互。
    • Vue CLI / Vite:搭建完整的 Vue 项目,支持现代开发工具链。

3. Vue 渐进式框架的优势

灵活性高:可以只使用 Vue 核心,也可以扩展成完整的前端框架。
易上手:适合初学者,可以从简单的绑定数据逐步深入。
按需引入:小项目只用 Vue 核心功能,大项目可以集成 Vue 生态。
生态丰富:支持 Vue Router、Pinia、Vite 等现代前端工具。


4. 适用场景
  • 小型项目(仅 Vue 核心 + 模板渲染)
  • 中等规模(组件化开发 + Vue Router)
  • 大型应用(完整 Vue 生态 + SSR)

总结

Vue 的渐进式特性让它可以适应不同规模的项目,开发者可以按需使用,逐步引入复杂功能,从简单到复杂,既能当库用,也能当框架用,这就是 Vue 的渐进式设计理念! 🚀

28. **Vue 中如何做样式穿透?**什么是样式穿透

什么是样式穿透?

样式穿透(Style Penetration) 指的是在 Vue 组件的 scoped 样式 中,默认情况下父组件的样式不会影响子组件,但我们可以通过特定的方法 突破 scoped 限制,让父组件的样式作用于子组件内部的元素。

在 Vue 中,每个 scoped 组件的样式 默认只能作用于当前组件的 DOM,不会影响其他组件。
但是,在某些场景下,我们需要让父组件的样式影响子组件,例如:

  • 修改第三方组件的默认样式
  • 在父组件中统一控制子组件样式
  • 对动态插入的内容(如 v-html)应用样式

在 Vue 组件中,scoped 作用域的样式默认不会影响到子组件,但有时候我们需要穿透 scoped 样式,使其影响子组件。这可以通过以下几种方式实现:

方法 1:使用 ::v-deep(Vue 3)或 /deep/(Vue 2)
<style scoped>
::v-deep(.child-class) {
  color: red;
}
</style>

说明:

  • Vue 3 使用 ::v-deep(最新标准)

  • Vue 2 使用 /deep/>>>

    <style scoped>
    /deep/ .child-class {
      color: red;
    }
    </style>
    

    <style scoped>
    >>> .child-class {
      color: red;
    }
    </style>
    

方法 2:使用 global 作用域
<style scoped>
:global(.child-class) {
  color: red;
}
</style>

作用: :global 使 .child-class 变为全局样式,不受 scoped 限制。


方法 3:取消 scoped

如果不希望样式受限制,可以去掉 scoped

<style>
.child-class {
  color: red;
}
</style>

作用: 这样该样式将全局生效,包括子组件。


方法 4:使用 CSS 变量

在父组件定义 CSS 变量:

<style scoped>
:root {
  --text-color: red;
}
</style>

在子组件使用:

<style scoped>
.child-class {
  color: var(--text-color);
}
</style>

作用: CSS 变量可以穿透 scoped 限制,被子组件使用。


scoped 原理

Vue 的 scoped 样式是通过 动态添加属性选择器 来限制样式作用范围的,例如:

示例
<template>
  <div class="parent">Hello Vue</div>
</template>

<style scoped>
.parent {
  color: red;
}
</style>
编译后的代码
<div class="parent" data-v-123abc> Hello Vue </div>
.parent[data-v-123abc] {
  color: red;
}

核心原理:

  • Vue 自动给组件的 HTML 标签添加一个 data-v-xxx 的属性xxx 是编译时生成的唯一哈希值)。
  • Vue 给 CSS 选择器添加 [data-v-xxx] ,确保该样式只作用于该组件内部的 HTML 元素,而不会影响全局。
scoped 样式不会影响子组件

由于子组件的 data-v-xxx 是不同的,所以父组件的 scoped 样式不会影响子组件。


总结

  1. 样式穿透方式:

    • Vue 3: 使用 ::v-deep(.class)
    • Vue 2: 使用 /deep/ .class>>> .class
    • :global 让样式变成全局作用域
    • 去掉 scoped 让样式影响全局
    • CSS 变量 通过 var(--color) 传递样式
  2. scoped 原理:

    • Vue 自动添加 data-v-xxx 属性 来限制样式作用域
    • 样式选择器加 [data-v-xxx] ,确保仅影响当前组件
    • 不会影响子组件,除非使用 ::v-deep 进行穿透

🚀 结论: scoped 让 Vue 组件样式隔离,避免全局污染,但可以通过 ::v-deep/deep/:global 等方式穿透样式。

29.Vuex是单向数据流还是双向数据流?

Vuex 是单向数据流

  • 核心机制
    Vuex 强制遵循「状态变更必须通过提交 Mutation」的规则,确保状态的变更是可追踪和可维护的。

    • 组件中通过 this.$store.dispatch() 触发 Action;
    • Action 调用后端接口或处理异步操作,然后提交 Mutation(commit);
    • Mutation 直接修改 State(必须是同步操作);
    • State 变化后,依赖它的组件会自动更新视图(通过 Vue 的响应式系统)。
  • 为什么不是双向?
    如果组件直接修改 State(如 this.$store.state.xxx = value),虽然技术上可行,但会破坏 Vuex 的设计原则,导致状态变更难以追踪和调试。


30.讲一下MVVM

MVVM(Model-View-ViewModel)是一种前端架构模式,核心是数据驱动视图关注点分离

  1. Model(模型)

    • 代表应用的数据和业务逻辑(如后端接口返回的 JSON 数据)。
    • 不涉及任何视图或 UI 相关代码。
  2. View(视图)

    • 用户看到的界面(如 HTML 模板、CSS 样式)。
    • 在 Vue 中对应组件的 <template> 部分。
  3. ViewModel(视图模型)

    • 连接 Model 和 View 的桥梁。

    • 负责:

      • 数据绑定:将 Model 转换为 View 需要的格式(如格式化时间字符串);
      • 双向绑定:View 中的用户输入(如 v-model)自动更新 Model 数据;
      • 事件监听:响应用户交互事件(如点击按钮触发方法)。

Vue中的 MVVM 实现

  • Model ⇨ Vue 的 data 或 Vuex 的 State;
  • View ⇨ 模板(Template);
  • ViewModel ⇨ Vue 实例(处理数据绑定、计算属性、方法等)。

优点

  • 开发效率高:数据变更自动更新视图,无需手动操作 DOM;
  • 维护性强:各层职责清晰,适合大型应用。

示例

<!-- View -->  
<template>
  <div>{{ formattedMessage }}</div>
</template>

<script>
export default {
  // ViewModel
  data() { 
    return { message: 'Hello' }; // Model 
  },
  computed: {
    formattedMessage() { 
      return this.message.toUpperCase(); // ViewModel 处理数据 
    }
  }
}
</script>

31.computed、methods、watch 的区别

  • computed(计算属性):

    • 缓存机制:基于依赖的响应式数据缓存,只有依赖变化时才会重新计算。
    • 适用场景:复杂逻辑或需要缓存结果的场景,如格式化数据。
    • 使用方式:作为属性调用(无需括号),例如:{{ fullName }}
  • methods(方法):

    • 无缓存:每次调用都会执行函数。
    • 适用场景:事件处理或需主动触发的操作。
    • 使用方式:通过方法名调用(需括号),如:{{ formatDate() }}@click="submit"
  • watch(侦听器):

    • 观察数据变化:响应特定数据的变化,执行副作用(如异步操作或复杂逻辑)。
    • 适用场景:数据变化后需要执行额外操作(如接口请求)。
    • 高级配置:支持深度监听(deep: true)和立即执行(immediate: true)。

32. props 和 data 的优先级

  • 优先级结论

    • data 的优先级更高。若 propsdata 存在同名属性,data 的值会覆盖 props 的值。
    • 开发警告:Vue 会在开发环境下发出警告,提示避免命名冲突。
  • 根源解析
    Vue 初始化顺序为:propsmethodsdatacomputedwatch
    dataprops 之后初始化,若同名属性存在,后初始化的 data 会覆盖 props 的值。


33. 双向绑定原理

  • 核心机制(以 Vue 2 为例):

    • 数据劫持:通过 Object.defineProperty 劫持数据的 getter/setter,在数据读写时触发依赖追踪和更新。
    • 依赖收集:每个组件实例对应一个 Watcher,在渲染过程中访问数据属性时会记录依赖关系。
    • 视图更新:数据变化时,setter 通知相关 Watcher 更新视图。
    • 指令实现v-model 本质是语法糖,通过 :value 绑定数据 + @input 监听输入事件实现双向同步。
  • Vue 3 优化
    使用 Proxy 代替 Object.defineProperty,支持监听动态新增属性和数组索引变化,性能更优。


34. 什么是虚拟 DOM

  • 定义
    虚拟 DOM 是一个轻量级 JavaScript 对象,模拟真实 DOM 的层次结构(如标签名、属性、子节点等)。

  • 作用与优势:

    • 减少 DOM 操作:通过批量更新和差异对比(diff 算法),最小化对真实 DOM 的直接操作。
    • 跨平台能力:不依赖浏览器 API,可用于 SSR(服务端渲染)、Native 开发等场景。
  • 结构示例

    const vnode = {
      tag: 'div',
      attrs: { id: 'app' },
      children: [
        { tag: 'p', text: 'Hello World' }
      ]
    };
    

35. diff 算法

  • 核心策略

    1. 同层比较:仅在同一层级对比节点,不跨级比较(时间复杂度从 O(n^3) 优化至 O(n))。
    2. 双端指针:用新旧节点的“首、尾”四个指针快速比对(减少遍历次数)。
    3. Key 优化:通过唯一的 key 标识节点身份,复用相同 key 的节点(提升列表更新效率)。
  • 步骤说明

    • 类型不同:直接替换整个节点(如 divspan)。
    • 类型相同:复用节点,仅更新属性和子节点(递归对比子节点)。
    • 列表对比:优先复用相同 key 的节点,避免不必要的重新渲染。
  • 示例场景

    旧节点: <div>A</div><div>B</div>
    新节点: <div>B</div><div>A</div>
    

    优化前:销毁旧节点,完全新建两节点。
    优化后:通过 key 复用节点,仅交换位置。

36.vue项目打包后出现空白页是为什么怎么解决

Vue 项目打包后出现空白页的原因可能有以下几种,并且可以通过不同的方法来解决:


1. 资源路径问题

原因

Vue 项目默认使用相对路径来引用静态资源,但如果项目部署在子目录下,可能导致资源无法正确加载。

解决方案

vue.config.js 中设置 publicPath

module.exports = {
  publicPath: process.env.NODE_ENV === 'production' ? '/your-sub-path/' : '/'
}

注意

  • /your-sub-path/ 需要替换为你部署的实际路径。
  • 如果部署在根目录,设置为 / 即可。

2. 依赖 history 模式的路由配置问题

原因

Vue Router 默认使用 history 模式,但如果服务器未正确配置,直接刷新页面或访问非首页地址可能会导致 404。

解决方案
  • 方法 1(推荐):使用 hash 模式

    const router = createRouter({
      history: createWebHashHistory(), // 改为 hash 模式
      routes
    })
    
  • 方法 2:如果坚持使用 history 模式,需要配置服务器 rewrite

    • Nginx

      location / {
        try_files $uri $uri/ /index.html;
      }
      
    • Apache.htaccess 添加:

      <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteBase /
        RewriteRule ^index.html$ - [L]
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule . /index.html [L]
      </IfModule>
      

3. 依赖未正确安装

原因

可能由于 node_modules 缺失或依赖版本不兼容导致。

解决方案
  1. 删除 node_modulespackage-lock.json,然后重新安装依赖:

    rm -rf node_modules package-lock.json
    npm install
    

    或者使用 pnpm

    rm -rf node_modules pnpm-lock.yaml
    pnpm install
    
  2. 确保 vuevue-routervite(或 webpack)等核心依赖版本匹配。


4. index.html 缺少 app 挂载节点

原因

如果 index.html 中缺少 #app 容器,Vue 无法正确挂载。

解决方案

检查 index.html,确保包含:

<div id="app"></div>

5. 生产环境 API 请求错误

原因
  • dev 环境使用了 localhost:3000 这样的本地 API,而 prod 环境未正确配置。
  • CORS 跨域问题导致请求失败。
解决方案
  1. .env.production 配置正确的 VITE_API_BASE_URL

    VITE_API_BASE_URL=https://your-api.com
    

    然后在 axios 配置:

    axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL;
    
  2. 确保后端允许 CORS


6. vitewebpack 配置问题

原因
  • vite 打包时 base 配置错误。
  • webpack 可能未正确配置 output.publicPath
解决方案
Vite

检查 vite.config.js

export default defineConfig({
  base: '/your-sub-path/', // 确保和部署路径匹配
})
Webpack

vue.config.js 确保:

module.exports = {
  publicPath: process.env.NODE_ENV === 'production' ? '/your-sub-path/' : '/'
}

7. 生产环境控制台报错

解决方案
  • 在浏览器按 F12 打开控制台,查看 ConsoleNetwork 面板的错误信息。

  • 常见错误

    • 404 Not Found:可能是 publicPath 配置错误。
    • Uncaught TypeError: Cannot read property 'xxx' of undefined:检查 API 是否返回正确数据。
    • Failed to fetch:可能是 CORSAPI 地址问题。

总结

问题 解决方案
资源路径错误 配置 publicPath
history 模式刷新 404 服务器 rewrite 配置
依赖问题 重新安装 node_modules
缺少 #app 容器 确保 index.html 里有 <div id="app"></div>
API 请求错误 检查 .env.production 里的 VITE_API_BASE_URL
vite/webpack 配置错误 确保 basepublicPath 设正确
控制台错误 F12 检查 ConsoleNetwork
.### 面试题解答

37. 介绍一下 SPA 以及 SPA 有什么缺点?

SPA(单页应用) 是指通过前端动态替换内容来实现页面切换的 Web 应用,整个应用仅加载一个 HTML 页面,后续通过 JavaScript 操作 DOM 更新视图。

核心原理

  • 初始加载 HTML、CSS、JavaScript 后,通过前端路由(如 Vue Router)管理不同“页面”组件的按需渲染。
  • 数据交互通过 Ajax/API 异步获取,减少整页刷新。

SPA 的优点

  • 用户体验流畅:切换页面无白屏,接近原生应用体验。
  • 前后端分离:前端独立开发,后端专注提供 API。
  • 组件化开发:便于复用和维护代码。

SPA 的缺点

  1. 首屏加载时间较长

    • 首次需加载全部框架代码(如 Vue、Vuex、Vue Router),可能影响性能。
    • 解决方案:代码分割(Code Splitting)、懒加载路由。
  2. SEO 不友好

    • 初始 HTML 内容为空,依赖 JavaScript 渲染,搜索引擎爬虫难以解析。
    • 解决方案:SSR(服务端渲染,如 Nuxt.js)或预渲染(Prerendering)。
  3. 内存管理复杂

    • 长时间运行可能积累内存泄漏(如未销毁的全局事件监听)。
  4. 浏览器兼容性

    • 依赖现代 JavaScript 特性(如 ES6+),旧版本浏览器可能不兼容。

38.Vue 路径传值

在 Vue 中,主要通过 Vue Router 实现路径传值,常用方式如下:

方式一:Params 动态路由
  1. 定义动态路由

    // router.js
    {
      path: '/user/:id',
      component: User,
      props: true // 允许通过 props 接收参数
    }
    
  2. 传参

    // 编程式导航
    this.$router.push({ path: `/user/123` });
    
  3. 接收参数

    // User.vue
    export default {
      props: ['id'] // 直接通过 props 获取
    };
    
    • 或通过 this.$route.params.id 访问。

注意

  • Params 参数会显示在 URL 中(如 /user/123)。
  • 页面刷新后参数不会丢失。
方式二:Query 参数
  1. 传参

    this.$router.push({
      path: '/user',
      query: { id: 123 }
    });
    

    URL 会变为 /user?id=123

  2. 接收参数

    this.$route.query.id; // "123"
    

特点

  • Query 参数通过 key=value 附加在 URL 中,适用于非敏感数据的少量传值。
  • 刷新页面后参数保留。
方式三:Props 解耦传值

在路由配置中启用 props: true,将 Params 映射为组件的 Props,使组件更独立:

{
  path: '/user/:id',
  component: User,
  props: true
}
方式四:编程式导航的 state 传值(隐藏传参)
this.$router.push({
  name: 'User',
  state: { secretKey: 'abc' } // 需 Vue Router v4+ 支持
});
  • 通过 this.$route.state.secretKey 获取。
  • 特点:参数不在 URL 中显示,但页面刷新后会丢失。

总结与对比

方式 显示在 URL 刷新保留 适用场景
Params ✔️ ✔️ 需 SEO 或直接分享的路径
Query ✔️ ✔️ 筛选条件、分页等
State ✖️ ✖️ 敏感数据或临时参数