关于生命周期
1. vue3和vue2的生命周期 每个阶段分别可以做些什么,以及各自的使用时机和场景区别?父子组件之间生命周期的执行顺序进去总结
Vue 3 与 Vue 2 的生命周期有很多相似之处,但也有明显的变化。Vue 3 对生命周期钩子做了重命名和优化,使得生命周期更加灵活,特别是在组合式 API 中。以下是 Vue 3 和 Vue 2 的生命周期对比、使用时机、以及常见使用场景。
Vue 2 生命周期
在 Vue 2 中,生命周期钩子如下:
beforeCreate
:实例刚创建时调用,数据和事件还未初始化。created
:实例创建完成,数据和事件已完成初始化,但尚未挂载到 DOM 上。beforeMount
:模板已经编译,挂载前调用,DOM 还未渲染。mounted
:实例挂载完成,模板编译生成的 DOM 已插入页面。beforeUpdate
:响应式数据更新时触发,DOM 尚未更新。updated
:组件 DOM 更新完成后调用。activated
: 组件激活时调用。deactivated
: 组件停用时调用。beforeDestroy
:组件销毁前调用。destroyed
:组件销毁完成。
Vue 3 生命周期
Vue 3 的生命周期与 Vue 2 类似,但重命名了一些钩子以适应组合式 API。以下是 Vue 3 的生命周期钩子:
setup
:组合式 API 的初始化阶段,用于创建响应式数据、定义方法等。onBeforeMount
(相当于 Vue 2 的beforeMount
):DOM 未挂载。onMounted
(相当于 Vue 2 的mounted
):DOM 已挂载。onBeforeUpdate
(相当于 Vue 2 的beforeUpdate
):数据更新,DOM 未更新。onUpdated
(相当于 Vue 2 的updated
):数据更新后 DOM 已更新。onBeforeUnmount
(相当于 Vue 2 的beforeDestroy
):组件销毁前。onUnmounted
(相当于 Vue 2 的destroyed
):组件销毁后。onActivated
: 组件激活。onDeactivated
: 组件停用。
此外,Vue 3 引入了一些新的生命周期钩子函数,提供更灵活的控制:
onRenderTracked
:用于追踪组件的渲染依赖。onRenderTriggered
:当组件重新渲染时触发,调试渲染性能非常有用。
使用时机与场景
1. 数据初始化:created
(Vue 2) / setup
(Vue 3)
- Vue 2:
created
阶段用于初始化数据、调用 API 等操作。 - Vue 3:在组合式 API 中使用
setup
,可以直接定义ref
或reactive
变量,同时可以进行异步操作,如调用 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. 组件更新:updated
和 beforeUpdate
- Vue 2:
beforeUpdate
和updated
钩子分别在更新前和更新后触发,适用于需要监听和处理数据变化的情况。 - Vue 3:通过
onBeforeUpdate
和onUpdated
实现类似效果。
示例:
// Vue 2
beforeUpdate() {
console.log('组件即将更新');
},
updated() {
console.log('组件已更新');
}
// Vue 3
import { onBeforeUpdate, onUpdated } from 'vue';
onBeforeUpdate(() => {
console.log('组件即将更新');
});
onUpdated(() => {
console.log('组件已更新');
});
4. 组件销毁:beforeDestroy
和 destroyed
(Vue 2) / onBeforeUnmount
和 onUnmounted
(Vue 3)
- Vue 2:在
beforeDestroy
中可以做一些清理工作,比如移除事件监听器或销毁定时器。 - Vue 3:
onBeforeUnmount
和onUnmounted
用于相同场景,且支持组合式 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. 依赖追踪:onRenderTracked
和 onRenderTriggered
(Vue 3 特有)
Vue 3 新增的 onRenderTracked
和 onRenderTriggered
,适合在调试中使用,帮助开发者了解组件渲染的依赖关系,找出潜在性能问题。
示例:
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>
输出顺序:
Parent beforeCreate
Parent created
Child beforeCreate
Child created
Child beforeMount
Child mounted
Parent beforeMount
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>
输出顺序(点击按钮后):
Parent beforeUpdate
Child beforeUpdate
Child updated
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>
输出顺序(销毁子组件后):
Parent beforeUnmount
Child beforeUnmount
Child unmounted
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 在销毁阶段,重命名了销毁相关的生命周期钩子为
onBeforeUnmount
和onUnmounted
,并支持组合式 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
阶段:
组件实例尚未创建,
data
、computed
、methods
还不可用。不能操作
this
,否则会报错:beforeCreate() { console.log(this.someData); // undefined axios.get("/api/data").then(response => { this.someData = response.data; // ❌ 报错:this 还未初始化 }); }
因此,不能在
beforeCreate
发送请求,因为无法处理响应数据。
3. beforeCreate
和 created
的区别
生命周期钩子 | 组件实例创建 | 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
,因为此时组件实例已创建,可以访问 data
和 methods
,且不会依赖 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,但可以:
- 提前存储需要的选择器信息
- 在
mounted
里操作 DOM - 如果数据请求在
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-view
或 v-if
控制下,每次进入都会重新创建:
beforeCreate
created
beforeMount
mounted
- (如果离开)
beforeUnmount
- (如果离开)
unmounted
📌 示例
<template>
<button @click="show = !show">切换组件</button>
<my-component v-if="show" />
</template>
- 每次
show = true
,组件都会完整重新执行生命周期。
情况 2:keep-alive
组件(缓存)
如果组件使用了 keep-alive
,那么不会销毁,而是被缓存:
第一次进入
beforeCreate
created
beforeMount
mounted
activated
(keep-alive
组件独有)
第二次及后续进入
- 不会重新执行
created
和mounted
- 直接触发
activated
- 不会重新执行
离开
deactivated
(不会销毁)
📌 示例
<keep-alive>
<router-view />
</keep-alive>
activated
:组件从缓存中激活时执行deactivated
:组件被缓存,不显示时执行unmounted
:组件完全销毁时才执行
情况 3:v-if
vs v-show
方式 | 生命周期 | 适用场景 |
---|---|---|
v-if |
销毁 & 重新创建 | 大量 DOM 操作,减少性能开销 |
v-show |
仅控制显示/隐藏,不触发生命周期 | 频繁切换的 UI(如 Tab 切换) |
总结
进入方式 | 第一次执行 | 后续执行 | 退出时 |
---|---|---|---|
普通组件 | beforeCreate → created → beforeMount → mounted |
重新执行所有生命周期 | beforeUnmount → unmounted |
keep-alive 组件 |
同上 + activated |
只执行 activated |
deactivated (不会销毁) |
v-if 切换 |
组件销毁 & 重新创建 | 完整生命周期重新执行 | unmounted |
v-show 切换 |
只执行 mounted 一次 |
不会重新执行生命周期 | 无需处理 |
👉 如果组件是被缓存的 keep-alive
,则 activated
和 deactivated
取代了 mounted
和 unmounted
。
👉 如果组件是 v-if
控制的,则每次都会重新走完整生命周期。
5. 加入keep-alive会执行哪些生命周期?
## **`keep-alive` 组件的生命周期**
当组件被 keep-alive
缓存时,它不会被销毁,而是进入“缓存状态”,因此它的生命周期和普通组件不同。
1. 第一次进入(组件被创建并挂载)
第一次进入时,keep-alive
组件和普通组件的生命周期几乎相同:
beforeCreate
created
beforeMount
mounted
activated
(keep-alive 专属)
📌 说明
activated
表示组件被keep-alive
缓存,并且被激活(显示出来)。mounted
只会在组件首次挂载时触发,后续缓存激活不会再触发mounted
。
2. 第二次及后续进入(组件未销毁,只是重新显示)
第二次及之后的进入时,不会重新创建组件,只会触发:
activated
📌 说明
activated
会在组件从缓存状态恢复时触发,而不会触发created
或mounted
。
3. 离开组件(切换到其他路由/隐藏)
当 keep-alive
组件离开但未销毁时,触发:
deactivated
📌 说明
deactivated
代表组件被缓存并切换到后台,但没有销毁,数据依然存在。
4. 组件被销毁
如果组件彻底被销毁(比如 keep-alive
被移除),则触发:
beforeUnmount
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>
总结
✅ activated
:keep-alive
组件每次进入都会触发(代替 mounted
)。
✅ deactivated
:keep-alive
组件离开时触发(不会销毁)。
✅ created
和 mounted
只在首次进入时触发,后续进入不会重复执行。
✅ beforeUnmount
和 unmounted
仅在 keep-alive
失效或组件被销毁时执行。
📌 记住: keep-alive
组件不会销毁,只是缓存 & 激活,所以多次进入不会触发 created
和 mounted
,而是触发 activated
!
6. keep-alive的实现原理
keep-alive
的实现原理
keep-alive
是 Vue 提供的一个内置组件,用于缓存组件,避免不必要的重复创建和销毁,提高性能。它的核心原理是通过动态管理组件的挂载与卸载,而不是彻底销毁组件。
1. keep-alive
组件的工作流程
当 keep-alive
包裹某个组件时:
第一次渲染
- 组件被正常创建 (
beforeCreate → created → beforeMount → mounted
)。 - 组件实例会被缓存,而不是销毁。
- 组件被正常创建 (
再次进入(缓存命中)
- 组件不会重新执行
created
或mounted
。 - 只会触发
activated
(表示组件从缓存中恢复)。
- 组件不会重新执行
离开(缓存未销毁)
- 组件不会被
unmounted
,而是触发deactivated
(表示组件被缓存,但未销毁)。
- 组件不会被
如果
keep-alive
被销毁- 组件才会执行
beforeUnmount → unmounted
,彻底销毁实例。
- 组件才会执行
2. keep-alive
的核心实现
Vue 内部通过 虚拟 DOM 维护 keep-alive
组件的缓存,核心是:
- 利用
cache
对象存储已经创建过的组件实例 - 动态控制
VNode
的patch
过程,使组件不会被销毁 - 在
activated
和deactivated
钩子中控制组件的显示和隐藏
核心源码简化版
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
里有对应实例,则直接复用 - 避免重复执行
created
和mounted
,仅触发activated
和deactivated
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-alive
的 include
和 exclude
keep-alive
提供了 include
和 exclude
选项,用于控制哪些组件需要缓存,哪些不需要缓存。
(1)include
:指定需要缓存的组件
<keep-alive include="A,B">
<component :is="view" />
</keep-alive>
📌 说明
- 只有
A
和B
组件会被缓存,其他组件不会。
(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
里手动清理)。
- 比如 WebSocket、定时器、全局事件监听等,可能导致资源泄露(需要在
6. keep-alive
的优化
如果 keep-alive
组件缓存过多,可能会导致内存占用增加,可以通过以下方式优化:
使用
include
/exclude
限制缓存的组件数量手动清理缓存
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
事件:子组件通知父组件,而不是直接修改父数据。 - 使用
ref
或v-model
:父组件需要修改子组件时。 - 多级组件推荐
provide/inject
,全局数据推荐 Vuex/Pinia。
8. vue2和vue3父子祖孙组件通讯的方法以及区别对比
在 Vue 2 和 Vue 3 中,父子、祖孙组件的通信方式有一些不同,Vue 3 引入了新的 API,使组件通信更加灵活和高效。以下是它们的主要通信方式和对比:
1. 父组件与子组件通信
Vue 2
父传子:
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>
- Vue 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
父传子:
props
(同 Vue 2)<script setup> defineProps(['msg']); </script>
子传父:
defineEmits
<script setup> const emit = defineEmits(['childEvent']); const sendData = () => { emit('childEvent', '子组件数据'); }; </script>
🔹 区别
- Vue 3 直接使用
defineProps
和defineEmits
,更简洁,无需this.$emit
。 setup
语法更加直观,避免this
作用域的问题。
2. 兄弟组件通信
Vue 2
事件总线(
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>
Vuex(推荐用于全局状态)
- Vuex 适用于大项目管理数据。
Vue 3
事件总线(
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>
- Vue 3 移除了
Pinia(Vuex 替代方案)
- Vue 3 推荐使用 Pinia 作为 Vuex 的替代方案。
🔹 区别
- Vue 3 不能使用
$on
,需要mitt
。 - 推荐用
Pinia
代替 Vuex。
3. 祖孙组件(跨级组件通信)
Vue 2
provide/inject
<!-- 父组件 --> <script> export default { provide() { return { parentData: '父组件数据' }; } }; </script>
<!-- 孙组件 --> <script> export default { inject: ['parentData'], created() { console.log(this.parentData); } }; </script>
Vuex(全局管理)
- Vuex 适合大规模数据管理。
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>
🔹 区别
- Vue 3 支持
ref
,祖孙组件通信变得更方便。
总结对比
方式 | Vue 2 | Vue 3 | 适用场景 |
---|---|---|---|
父传子 | props |
defineProps |
组件间传递数据 |
子传父 | $emit |
defineEmits |
子组件通知父组件 |
兄弟通信 | $emit + EventBus / Vuex |
mitt / Pinia |
组件间消息传递 |
祖孙通信 | provide/inject (仅基本数据) |
provide/inject (支持 ref ) |
跨层级数据传递 |
全局状态 | Vuex | Pinia | 适用于大项目 |
选择建议
小型项目
props
/emit
/provide/inject
足够。- 兄弟组件可以用
mitt
。
中大型项目
- 推荐 Pinia(比 Vuex 更轻量) 。
- 跨层级组件通信可以用
provide/inject
+ref
。
实时通信
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. 总结
- 默认插槽:适用于简单的内容插入。
- 具名插槽:适用于多区域内容插入。
- 作用域插槽:适用于子组件向父组件传递数据。
- 动态插槽:适用于动态切换插槽内容。
在 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 API
(setup
),让封装更加灵活。
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
语法糖,简化visible
的props
和$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
处理props
,defineEmits
处理事件。 - 语法更简洁。
② 组合式 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. 总结
基础封装:组件
props
+slot
让 UI 组件更灵活。逻辑复用:
- Vue 2:
mixins
(缺点:变量冲突)。 - Vue 3:
composables
(推荐)。
- Vue 2:
依赖注入:
- Vue 3 支持响应式
provide/inject
,更适合全局共享数据。
- Vue 3 支持响应式
Vue 3 更简洁:
setup
语法 +defineProps
+defineEmits
让代码更清晰。
如果是新项目,建议直接用 Vue 3 进行组件封装,代码更简洁、逻辑更易复用!🚀
关于Vuex
13.1 Vuex有哪些属性,Vuex如何使用state值,Vuex的getters值修改,Vuex的mutations和actions区别,Vuex持久化存储
Vuex 核心属性 & 使用
Vuex 是 Vue 2 的官方状态管理库,主要用于管理全局共享状态。它的核心属性包括:
- state(状态):存储应用数据。
- getters(计算属性):类似
computed
,从state
派生数据。 - mutations(同步修改
state
):唯一能直接修改state
的方法。 - actions(异步操作):提交
mutations
,处理异步逻辑(如 API 请求)。 - 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. mutations
和 actions
的区别
对比项 | 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 默认存储在内存中,刷新页面会丢失数据。持久化存储的常见方案:
- 本地存储(localStorage/sessionStorage)
- 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 适用场景
✅ 适用:
- 需要多个组件共享状态,如用户信息、购物车数据。
- 需要集中管理复杂的状态逻辑(如权限、异步数据)。
- 需要对状态进行时间旅行(如撤销/重做)。
❌ 不适用:
- 简单组件间通信(可用
props
、emit
、provide/inject
)。 - 只在某个组件中用到的状态(可用
ref
、reactive
)。
6. 总结
- Vuex 主要属性:
state
(数据)、getters
(计算属性)、mutations
(同步修改)、actions
(异步操作)、modules
(模块化)。 state
通过mapState
使用,getters
通过mapGetters
计算派生数据。mutations
只能同步修改state
,actions
可异步操作。- Vuex 持久化存储可用
localStorage
或vuex-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 提供两种模式:
hash
模式(默认):基于 URL#
号的哈希值,使用location.hash
进行更新,无需服务器配置。history
模式:使用 HTML5 的history.pushState
和history.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(推荐 useRoute
和 useRouter
):
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
router.push('/home');
console.log(route.path);
🔹 区别:
- Vue 3 推荐使用
useRoute()
和useRouter()
代替this.$router
和this.$route
,更符合组合式 API 设计。
3. beforeRouteEnter
、beforeRouteUpdate
、beforeRouteLeave
的区别
Vue 2 中,beforeRouteEnter
只能用于 选项式 API:
export default {
beforeRouteEnter(to, from, next) {
next(vm => {
console.log(vm); // 访问组件实例
});
}
};
Vue 3 中,推荐使用 onBeforeRouteLeave
、onBeforeRouteUpdate
:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
onBeforeRouteLeave(() => {
console.log('组件即将离开');
});
onBeforeRouteUpdate(() => {
console.log('路由更新,但组件未销毁');
});
🔹 区别:
- Vue 3 推荐在组合式 API 中使用
onBeforeRouteLeave
和onBeforeRouteUpdate
,代码更清晰。
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 |
onBeforeRouteLeave 、onBeforeRouteUpdate |
动态参数 | this.$route.params |
useRoute().params |
组件懒加载 | import() |
import() |
Suspense 支持 |
❌ | ✅ |
结论
- Vue 3 取消了 Vue 2 的
mode
,改为createWebHistory()
和createWebHashHistory()
。 - Vue 3 推荐
useRoute()
和useRouter()
取代this.$route
和this.$router
,更适配 Composition API。 - Vue 3 允许用
Suspense
处理异步组件,提高渲染效率。 - Vue 2 代码仍然可用,但 Vue 3 提供更简洁、现代化的写法。
hash模式和history模式的区别
Vue Router 提供两种路由模式:Hash 模式(hash
) 和 History 模式(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 API(
pushState
和replaceState
)。需要服务器支持,否则刷新页面会导致
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和 router和route区别
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
) - 路由传参 可以用
params
、query
或props
- 导航故障 发生在拦截或重复跳转时
$router
vs$route
:前者用于跳转,后者用于获取信息- 导航守卫 用于拦截或控制页面访问
16.关于API
关于 API - 面试题回答 & 使用场景
1 $set
面试官问题: 你有没有碰到过,数据更新视图没有更新的问题?
回答: Vue 2 的响应式系统无法检测到对象新属性的添加,因此直接给对象赋值新属性不会触发视图更新。这时需要使用 $set
来确保数据是响应式的。
使用方式:
this.$set(target, key, value);
使用场景:
给对象动态添加新属性,确保视图更新
this.$set(this.user, 'age', 25);
修改数组某个元素,确保响应式更新
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);
});
使用场景:
在
v-if
控制的 DOM 生成后,获取最新的 DOMthis.visible = true; this.$nextTick(() => { console.log(this.$refs.popup); // 确保能获取 DOM });
数据变更后,执行某些需要 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();
使用场景:
获取原生 DOM 元素
this.$refs.input.focus();
调用子组件方法
<ChildComponent ref="child" />
this.$refs.child.someMethod();
4 $el
面试官问题: $el
的作用是什么?
回答: $el
返回当前 Vue 组件的根 DOM 元素。
使用方式:
console.log(this.$el.tagName);
使用场景:
操作组件的根元素
this.$el.style.background = 'red';
判断组件是否挂载
if (this.$el) { console.log('组件已挂载'); }
5 $data
面试官问题: $data
在 Vue 里有什么作用?
回答: $data
访问组件实例的整个数据对象。
使用方式:
console.log(this.$data);
使用场景:
深拷贝数据
const snapshot = JSON.parse(JSON.stringify(this.$data));
动态修改数据
this.$data.userName = '新名字';
6 $children
面试官问题: $children
与 $refs
的区别?
回答: $children
获取子组件实例的数组,而 $refs
是获取特定的子组件或 DOM。
使用方式:
console.log(this.$children);
使用场景:
批量调用子组件方法
this.$children.forEach(child => child.doSomething());
7 $parent
面试官问题: $parent
主要用于什么场景?
回答: $parent
用于访问父组件的实例,通常用于子组件调用父组件的方法或获取父组件的数据。
使用方式:
this.$parent.showMessage('子组件调用父组件方法');
使用场景:
子组件调用父组件方法
this.$parent.updateData();
获取父组件数据
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 methods
和 computed
区别
特性 | 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
vscomputed
: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>
使用场景:
自动聚焦输入框
Vue.directive('focus', { inserted(el) { el.focus(); } });
拖拽元素
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" />
使用场景:
- 单项绑定:用于
props
传值,避免子组件直接修改父组件的数据。 - 双向绑定:用于表单输入框、搜索框等需要用户输入的地方。
6.3 v-if
和 v-for
优先级
面试官问题: v-if
和 v-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
,可能影响性能。
优化方案:外层包裹
v-if
,减少不必要的遍历:<template v-if="list.length"> <div v-for="item in list" :key="item.id">{{ item.name }}</div> </template>
使用计算属性先过滤数据:
computed: { filteredList() { return this.list.filter(item => item.show); } }
<div v-for="item in filteredList" :key="item.id">{{ item.name }}</div>
总结:
指令 | 作用 | 典型使用场景 |
---|---|---|
v-bind |
单项绑定 | 动态修改属性(如 class 、style ) |
v-model |
双向绑定 | 表单元素(如 input 、textarea ) |
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,当数据发生变化时:
- Vue 不会立即更新 DOM,而是将更新任务放入异步任务队列。
- 批量执行更新,提高性能。
$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
劫持对象属性的 getter
和 setter
,在数据变化时通知视图更新,形成双向绑定。
核心实现(简化版):
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 进行二次封装?
回答思路
- 减少代码重复:每个请求都要手动设置
headers
、baseURL
、timeout
等,封装后可全局管理。 - 优化错误处理:统一处理网络异常、超时、接口错误,避免多个组件重复写逻辑。
- 简化请求方式:封装
get
、post
等方法,让请求更简洁。 - 拦截器:在请求前添加 token,在响应后处理错误。
- 支持取消请求:避免用户频繁操作导致的并发请求问题。
2. 如何封装 Axios?
回答思路
- 创建 axios 实例
- 添加请求拦截器(统一添加 token)
- 添加响应拦截器(统一处理错误)
- 封装
get
、post
方法 - 支持请求取消
示例
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.CancelToken
或AbortController
取消未完成的请求。
示例
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
实例 - 添加 请求/响应拦截器
- 封装
get
、post
方法 - 统一 错误处理
- 支持 请求取消
(2)Axios 拦截器的作用?
请求拦截器:
- 添加
token
- 统一
headers
- 添加
响应拦截器:
- 处理
401
、403
、500
等错误 - 解析
data
,避免response.data
冗余
- 处理
(3)如何处理 Axios 超时?
- 在
axios.create
设置timeout
- 监听
error.message.includes("timeout")
(4)Vue 3 如何全局使用 Axios?
app.config.globalProperties.$http = request
(5)如何取消 Axios 请求?
AbortController
或axios.CancelToken
总结
掌握 Vue 中 Axios 的二次封装,不仅能优化代码,还能提升项目的可维护性。面试时重点关注:
- 为什么封装 Axios?
- 如何使用请求/响应拦截器?
- 如何处理错误(401、500)?
- 如何封装
get
、post
方法? - 如何取消请求?
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 的好处
- 减少无意义的 DOM 节点:避免
div
嵌套层级过深。 - 提升渲染性能:减少
div
影响 CSS 选择器的复杂度。 - 优化 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";
- 只导入
ref
和computed
,其他未使用的 API 不会被打包。 - 减少最终的 bundle 体积,提高加载速度。
Tree-Shaking 的优势
- 减少不必要的代码打包:提高应用运行效率。
- 优化前端性能:减少 JS 文件大小,加快页面加载速度。
- 适用于 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 包裹 |
直接支持多个根节点 |
生命周期 | beforeCreate ,created |
统一 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
影响 - 提高代码可读性和结构清晰度
总结
- Vue3 响应式优化(
Proxy
)、Composition API 让代码更清晰。 setup
代替 Vue2 选项式 API,更灵活地组织逻辑。getCurrentInstance()
获取this
访问组件实例。- 常用 API
ref()
、reactive()
、computed()
、watch()
提高开发效率。 Teleport
让模态框等组件更易管理,提升用户体验。
27.# Vue 中的渐进式框架(Progressive Framework)
1. 什么是渐进式框架?
Vue 被称为渐进式框架(Progressive Framework) ,意味着 它可以根据项目需求逐步引入不同的功能,而不是一开始就要求使用完整的框架。Vue 既可以用作一个简单的 UI 库,也可以扩展成一个功能完整的前端框架。
在vue.js的核心库上按需不断的增加vuex,vue-router…渐进式完善
2. Vue 为什么是渐进式的?
Vue 采用模块化设计,你可以根据项目规模和需求逐步引入以下功能:
仅使用核心功能(轻量级)
- 直接使用
<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>
- 直接使用
组件化开发(中等规模应用)
- 使用 单文件组件(SFC) ,提高代码复用性,方便维护。
- 适用于中等规模的单页面应用(SPA)。
<template> <h1>{{ title }}</h1> </template> <script> export default { data() { return { title: "Vue Component" }; }, }; </script>
完整生态(大型应用)
- 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
样式不会影响子组件。
总结
样式穿透方式:
- Vue 3: 使用
::v-deep(.class)
- Vue 2: 使用
/deep/ .class
或>>> .class
:global
让样式变成全局作用域- 去掉
scoped
让样式影响全局 - CSS 变量 通过
var(--color)
传递样式
- Vue 3: 使用
scoped 原理:
- Vue 自动添加
data-v-xxx
属性 来限制样式作用域 - 样式选择器加
[data-v-xxx]
,确保仅影响当前组件 - 不会影响子组件,除非使用
::v-deep
进行穿透
- Vue 自动添加
🚀 结论: 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)是一种前端架构模式,核心是数据驱动视图和关注点分离。
Model(模型)
- 代表应用的数据和业务逻辑(如后端接口返回的 JSON 数据)。
- 不涉及任何视图或 UI 相关代码。
View(视图)
- 用户看到的界面(如 HTML 模板、CSS 样式)。
- 在 Vue 中对应组件的
<template>
部分。
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
的优先级更高。若props
和data
存在同名属性,data
的值会覆盖props
的值。- 开发警告:Vue 会在开发环境下发出警告,提示避免命名冲突。
根源解析:
Vue 初始化顺序为:props
→methods
→data
→computed
→watch
。
data
在props
之后初始化,若同名属性存在,后初始化的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 算法
核心策略:
- 同层比较:仅在同一层级对比节点,不跨级比较(时间复杂度从 O(n^3) 优化至 O(n))。
- 双端指针:用新旧节点的“首、尾”四个指针快速比对(减少遍历次数)。
- Key 优化:通过唯一的
key
标识节点身份,复用相同key
的节点(提升列表更新效率)。
步骤说明:
- 类型不同:直接替换整个节点(如
div
→span
)。 - 类型相同:复用节点,仅更新属性和子节点(递归对比子节点)。
- 列表对比:优先复用相同
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
缺失或依赖版本不兼容导致。
解决方案
删除
node_modules
和package-lock.json
,然后重新安装依赖:rm -rf node_modules package-lock.json npm install
或者使用
pnpm
:rm -rf node_modules pnpm-lock.yaml pnpm install
确保
vue
、vue-router
、vite
(或webpack
)等核心依赖版本匹配。
4. index.html
缺少 app
挂载节点
原因
如果 index.html
中缺少 #app
容器,Vue 无法正确挂载。
解决方案
检查 index.html
,确保包含:
<div id="app"></div>
5. 生产环境 API 请求错误
原因
- 在
dev
环境使用了localhost:3000
这样的本地 API,而prod
环境未正确配置。 CORS
跨域问题导致请求失败。
解决方案
在
.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;
确保后端允许
CORS
。
6. vite
或 webpack
配置问题
原因
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
打开控制台,查看Console
和Network
面板的错误信息。常见错误
404 Not Found
:可能是publicPath
配置错误。Uncaught TypeError: Cannot read property 'xxx' of undefined
:检查 API 是否返回正确数据。Failed to fetch
:可能是CORS
或API
地址问题。
总结
问题 | 解决方案 |
---|---|
资源路径错误 | 配置 publicPath |
history 模式刷新 404 |
服务器 rewrite 配置 |
依赖问题 | 重新安装 node_modules |
缺少 #app 容器 |
确保 index.html 里有 <div id="app"></div> |
API 请求错误 | 检查 .env.production 里的 VITE_API_BASE_URL |
vite/webpack 配置错误 |
确保 base 和 publicPath 设正确 |
控制台错误 | F12 检查 Console 和 Network |
.### 面试题解答 |
37. 介绍一下 SPA 以及 SPA 有什么缺点?
SPA(单页应用) 是指通过前端动态替换内容来实现页面切换的 Web 应用,整个应用仅加载一个 HTML 页面,后续通过 JavaScript 操作 DOM 更新视图。
核心原理:
- 初始加载 HTML、CSS、JavaScript 后,通过前端路由(如 Vue Router)管理不同“页面”组件的按需渲染。
- 数据交互通过 Ajax/API 异步获取,减少整页刷新。
SPA 的优点:
- 用户体验流畅:切换页面无白屏,接近原生应用体验。
- 前后端分离:前端独立开发,后端专注提供 API。
- 组件化开发:便于复用和维护代码。
SPA 的缺点:
首屏加载时间较长
- 首次需加载全部框架代码(如 Vue、Vuex、Vue Router),可能影响性能。
- 解决方案:代码分割(Code Splitting)、懒加载路由。
SEO 不友好
- 初始 HTML 内容为空,依赖 JavaScript 渲染,搜索引擎爬虫难以解析。
- 解决方案:SSR(服务端渲染,如 Nuxt.js)或预渲染(Prerendering)。
内存管理复杂
- 长时间运行可能积累内存泄漏(如未销毁的全局事件监听)。
浏览器兼容性
- 依赖现代 JavaScript 特性(如 ES6+),旧版本浏览器可能不兼容。
38.Vue 路径传值
在 Vue 中,主要通过 Vue Router 实现路径传值,常用方式如下:
方式一:Params 动态路由
定义动态路由
// router.js { path: '/user/:id', component: User, props: true // 允许通过 props 接收参数 }
传参
// 编程式导航 this.$router.push({ path: `/user/123` });
接收参数
// User.vue export default { props: ['id'] // 直接通过 props 获取 };
- 或通过
this.$route.params.id
访问。
- 或通过
注意:
- Params 参数会显示在 URL 中(如
/user/123
)。 - 页面刷新后参数不会丢失。
方式二:Query 参数
传参
this.$router.push({ path: '/user', query: { id: 123 } });
URL 会变为
/user?id=123
。接收参数
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 | ✖️ | ✖️ | 敏感数据或临时参数 |