vue2的defineProperty和vue3的Proxy的区别
1. defineProperty和Proxy的本质
- Proxy可以为一个对象创建一个代理,这个代理可以拦截重定义对对像的一些基本操作,基本操作是值:读取,删除,冻结对象,不可继承的对象,设置对象的原型,读取对象的原型,或者去循环对象中的每一个健,无论是通过语法执行或者api都行它最终在执行引擎的内部(内部方法),这些操作都转成基本操作。
- Proxy是在拦截对象的基本方法或者是对像的内部的方法
- defineProperty只是众多内部方法的其中之一。
2. 框架应用方面:
- vue2:defineProperty 拦截现有对象的读写,拦截有缺陷有很多拦截不到,不精准有遗漏的,所以vue中用get用手动触发
- vue3:Proxy 针对对象的所有内部方法都可以重定义拦截全面无死角的,不管是这个对象是函数对象还是普通对象,不管对对象的任何操作全面拦截,所以在vue3中set,get都没有了因为不需要了
二、生命周期
2. Vue 子组件和父组件执行顺序
加载渲染过程:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
更新过程:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
销毁过程:
- 父组件 beforeDestroy
- 子组件 beforeDestroy
- 子组件 destroyed
- 父组件 destoryed
3. created和mounted的区别
- created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
- mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
4. 一般在哪个生命周期请求异步数据
我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
- SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
三、组件通信
1. props/$emit
父组件通过props向子组件传递数据,子组件通过$emit和父组件通信
2. eventBus事件总线($emit / $on)
eventBus事件总线适用于父子组件、非父子组件等之间的通信
3. 依赖注入(provide / inject)
Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了
4. ref / $refs
ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法
四、路由
路由的hash模式与history模式的核心区别 Vue-Router提供两种路由模式,分别基于不同的浏览器特性实现前端路由跳转,核心区别如下:
1. hash模式(默认模式)
定义:URL中包含#符号(如http://www.abc.com/#/vue),#后的部分称为hash值,其变化不会触发页面重新加载。
核心原理: 依赖浏览器的onhashchange事件,当hash值(#后的内容)变化时,事件被触发,前端可通过监听该事件实现路由切换,无需向后端发送请求。
特点:
- URL特性:hash值不会被包含在HTTP请求中,仅作为前端标识,对后端无影响。
- 兼容性:支持所有浏览器(包括低版本IE),兼容性极佳。
- 功能限制:
- 仅能修改
#后的部分,URL范围受限(同文档内)。 - hash值变化时,若与原hash相同则不会触发历史记录更新。
- 仅支持字符串类型的hash值存储。
- 仅能修改
- 优势:无需后端配置,刷新页面不会出现404(因请求URL始终为
#前的部分)。
2. history模式
定义:URL中无#符号(如http://abc.com/user/id),通过HTML5的History API实现路由管理。
核心原理: 借助History API的pushState()、replaceState()方法修改浏览器历史记录,实现URL变化但不刷新页面;通过forward()、back()、go()方法切换历史状态。
特点:
- URL特性:URL与传统路由一致,美观且符合用户习惯。
- 功能扩展:
- 可设置与当前URL同源的任意URL(不限于同文档)。
- 支持添加任意类型数据到历史记录(通过
stateObject参数)。 - 可重复添加相同URL到历史栈,且支持自定义
title属性。
- 兼容性:仅支持HTML5标准的浏览器(IE10+)。
- 依赖后端:刷新页面时,浏览器会按当前URL向后端发起请求,若后端无对应路由配置,会返回404错误(需后端配合配置
fallback页面,如指向index.html)。
3. 核心对比表
| 维度 | hash 模式 | history 模式 |
|---|---|---|
| URL 格式 | 含#(如xxx.com/#/path) | 无#(如xxx.com/path) |
| 底层依赖 | onhashchange事件 | HTML5 History API(pushState等) |
| 后端依赖 | 无需配置,无 404 风险 | 需后端配置路由 fallback,否则刷新可能 404 |
| 兼容性 | 所有浏览器(包括 IE 低版本) | 仅支持 HTML5 的浏览器(IE10+) |
| URL 修改范围 | 仅#后部分,同文档内 | 同源下任意 URL |
| 历史记录数据 | 仅支持短字符串 | 支持任意类型数据(通过stateObject) |
| 相同 URL 处理 | 相同 hash 值不触发历史记录更新 | 相同 URL 可添加到历史栈 |
4. 适用场景
- hash模式:适合兼容性要求高(如需支持IE9及以下)、无需后端配合的场景(如静态站点)。
- history模式:适合追求URL美观、需要复杂历史记录管理的场景(如多页面交互应用),需后端同步配置。
5. 模式切换配置
// 切换为history模式(需后端配合)
const router = new VueRouter({
mode: 'history', // 默认是'hash'
routes: [...]
})
总结:两种模式均通过前端技术实现无刷新路由跳转,hash模式简单且兼容性强,history模式更符合URL规范但依赖后端配置,需根据项目需求选择。
Vue-router 路由钩子
1. 路由钩子分类
Vue Router 的钩子分为三类,分别在不同阶段执行:
全局守卫
- 全局前置守卫:
router.beforeEach
在路由导航开始时触发,可用于登录验证、权限控制。 - 全局解析守卫:
router.beforeResolve
在所有组件内守卫和异步路由组件被解析后触发。 - 全局后置钩子:
router.afterEach
在导航完成后触发,不影响导航流程,常用于页面分析、日志记录。
路由独享守卫
- 路由配置守卫:
beforeEnter
定义在路由配置中的守卫,只对当前路由生效。
组件内守卫
-
组件内钩子:
beforeRouteEnter:在路由进入前触发,此时组件实例尚未创建。beforeRouteUpdate:在当前路由改变但组件被复用时触发(如参数变化)。beforeRouteLeave:在导航离开当前组件时触发,可用于阻止用户意外离开(如表单未保存)。
2. 钩子与生命周期的结合
当触发路由导航时,钩子的执行顺序与组件生命周期的关系如下:
完整导航流程(新路由加载)
1. 导航触发
2. 调用当前路由的beforeRouteLeave守卫(如果存在)
3. 调用全局前置守卫:router.beforeEach
4. 调用路由配置中的beforeEnter守卫(如果存在)
5. 解析异步路由组件(如果存在)
6. 调用全局解析守卫:router.beforeResolve
7. 导航被确认
8. 调用全局后置钩子:router.afterEach
9. 触发DOM更新
10. 调用组件内的beforeRouteEnter守卫中的next回调(此时组件实例已创建)
路由参数变化(组件复用)
当仅路由参数变化(如/user/1 → /user/2)时,组件实例会被复用,触发以下钩子:
1. 调用组件内的beforeRouteUpdate守卫
2. 调用全局前置守卫:router.beforeEach
3. 调用路由配置中的beforeEnter守卫(如果存在)
4. 解析异步路由组件(如果存在)
5. 调用全局解析守卫:router.beforeResolve
6. 导航被确认
7. 调用全局后置钩子:router.afterEach
8. 触发DOM更新
导航离开当前组件
当用户从当前路由导航到其他路由时:
1. 调用组件内的beforeRouteLeave守卫
2. 后续流程与新路由加载一致
3. 关键钩子详解
beforeRouteEnter
-
特点:在导航确认前触发,此时组件实例尚未创建,无法访问
this。 -
用途:可通过
next回调访问组件实例,常用于路由进入前的数据预加载。 -
示例:
export default { beforeRouteEnter(to, from, next) { // 通过next回调获取组件实例 next(vm => { // vm 即组件实例,可访问data、methods等 vm.fetchData(to.params.id); }); } }
beforeRouteUpdate
-
特点:在组件复用时触发,可访问
this。 -
用途:处理路由参数变化(如更新组件数据),避免重复创建组件。
-
示例:
javascript
export default { beforeRouteUpdate(to, from, next) { // 当路由参数变化时更新数据 this.fetchData(to.params.id); next(); } }
beforeRouteLeave
-
特点:在导航离开前触发,可访问
this。 -
用途:阻止用户意外离开(如表单未保存),需显式调用
next()。 -
示例:
export default { beforeRouteLeave(to, from, next) { if (this.formDirty) { // 阻止导航并提示用户 if (confirm('表单尚未保存,确定离开?')) { next(); // 允许离开 } else { next(false); // 取消导航 } } else { next(); // 无修改,直接离开 } } }
4. 异步操作与守卫
若在守卫中执行异步操作(如 API 请求),需等待操作完成后再调用next()。
示例:权限校验
router.beforeEach(async (to, from, next) {
const isAuthenticated = await checkAuth(); // 异步校验登录状态
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login'); // 未登录则跳转登录页
} else {
next(); // 允许访问
}
});
5. 总结
路由钩子通过精确控制导航流程,与组件生命周期紧密配合,实现了以下功能:
- 权限控制:全局前置守卫和路由独享守卫。
- 数据预加载:
beforeRouteEnter和beforeRouteUpdate。 - 用户体验优化:
beforeRouteLeave阻止意外操作。 - 性能优化:复用组件时避免重复渲染。
vue框架的虚拟dom 为什么要用虚拟dom
Vue 框架使用虚拟 DOM(Virtual DOM)的核心目的是在保持声明式编程体验的同时,高效地更新真实 DOM。以下从性能优化、开发体验和跨平台能力三个维度详细解析其必要性:
1. 虚拟 DOM 的核心价值
(1)性能优化:最小化 DOM 操作
-
直接操作 DOM 的代价:
浏览器的 DOM 是一个复杂的树形结构,每次修改都会触发重排(reflow)和重绘(repaint),性能开销大。 -
虚拟 DOM 的解决方案:
- 抽象层:用 JavaScript 对象(虚拟 DOM)表示真实 DOM 结构。
- 差异计算:通过对比新旧虚拟 DOM 的差异(diff 算法),只更新需要变化的真实 DOM 节点。
- 批量更新:将多次虚拟 DOM 的修改合并为一次真实 DOM 操作,减少浏览器重排重绘次数。
示例对比:
修改一个包含 100 个节点的列表时:
- 直接操作 DOM:需执行 100 次独立 DOM 操作。
- 虚拟 DOM:计算差异后可能只需执行 3 次 DOM 操作(如插入、移动、删除)。
(2)开发体验:声明式编程
- 虚拟 DOM 支持声明式语法:
通过模板或 JSX 描述 UI 状态,Vue 自动将状态变化映射到虚拟 DOM 的更新,开发者无需手动操作 DOM。 - 状态管理与视图分离:
虚拟 DOM 使组件状态与 DOM 操作解耦,代码更易维护(如 Vue 的响应式系统与虚拟 DOM 配合)。
(3)跨平台能力
-
虚拟 DOM 是抽象层:
不仅可以渲染为浏览器 DOM,还能转换为其他平台的 UI(如移动端的原生组件)。 -
Vue 的多平台支持:
- Vue SSR(服务器端渲染):将虚拟 DOM 渲染为 HTML 字符串,提升 SEO 和首屏加载速度。
- Vue Native:将虚拟 DOM 转换为 iOS/Android 原生组件,实现跨平台移动应用开发。
2. 虚拟 DOM 的工作流程
1. 组件初始化时,Vue将模板编译为渲染函数(render function)
2. 渲染函数执行后返回虚拟DOM树(JavaScript对象)
3. 当组件状态变化时,重新执行渲染函数生成新的虚拟DOM树
4. 通过diff算法对比新旧虚拟DOM树,计算出最小差异
5. 将差异应用到真实DOM上
3. 虚拟 DOM 的性能优势
(1)高效的差异计算
-
Vue 的 diff 算法优化:
- 同层比较:只比较同一层级的节点,避免跨层级比较的复杂计算。
- 静态节点优化:编译时标记不变的静态节点(如纯文本),跳过比较。
- PatchFlag(Vue3):编译时为动态节点添加标记,仅对比变化的类型(如文本、属性)。
(2)减少不必要的 DOM 操作
-
示例:修改数组中的一个元素时:
// 原始数组 const list = ['a', 'b', 'c']; // 修改后 const newList = ['a', 'x', 'c'];虚拟 DOM 只会更新
b→x的变化,而非重新渲染整个列表。
4. 虚拟 DOM vs 直接操作 DOM
| 场景 | 直接操作 DOM | 虚拟 DOM |
|---|---|---|
| 代码复杂度 | 高(需手动管理 DOM 操作) | 低(声明式描述 UI) |
| 大规模数据更新性能 | 差(频繁重排重绘) | 优(批量更新、最小化 DOM 操作) |
| 维护成本 | 高(DOM 操作与状态管理耦合) | 低(状态与视图分离) |
| 跨平台能力 | 仅支持浏览器 | 支持 SSR、移动端等多平台 |
5. 常见误区
(1)虚拟 DOM 一定比直接操作 DOM 快?
- 真相:
对于小规模 DOM 更新,虚拟 DOM 可能稍慢(因存在 diff 计算开销)。但在大规模复杂应用中,虚拟 DOM 的批量优化效果显著,总体性能更优。
(2)虚拟 DOM 就是 JS 对象?
- 本质:
虚拟 DOM 不仅是 JS 对象,更是一种编程范式。它通过抽象层将 UI 描述与渲染分离,使框架具备更强的灵活性和可维护性。
6. 总结
Vue 使用虚拟 DOM 的核心原因是在保持开发体验的同时,平衡性能与可维护性:
- 性能:通过 diff 算法和批量更新,减少真实 DOM 操作次数。
- 开发体验:声明式语法降低 DOM 操作复杂度,提升代码可维护性。
- 跨平台:虚拟 DOM 作为中间层,支持多平台渲染。
虚拟 DOM 是现代前端框架的基石之一,它让开发者既能享受声明式编程的简洁,又能获得接近原生的性能表现。
vuex与pinia
Vuex 和 Pinia 均为 Vue.js 的状态管理库,用于管理应用的共享状态。Pinia 是 Vuex 的继任者,在保持核心功能的同时进行了诸多改进。以下从设计理念、API 风格、类型支持和性能等维度对比两者的差异:
1. 设计理念
Vuex
- 基于 Flux 架构:单向数据流(State → View → Action → Mutations → State),强制使用 mutations 修改状态。
- 模块系统:通过 modules 组织代码,但嵌套层级过深时结构复杂。
- 插件机制:依赖插件实现时间旅行调试、持久化等功能。
Pinia
- 简化设计:移除 mutations 概念,仅保留 state、getters、actions,更贴近自然的 Vue 使用方式。
- 扁平化结构:以 store 为单位组织代码,避免深层嵌套,提高可维护性。
- 组合式 API 优先:原生支持 TypeScript 和 Composition API,无需额外配置。
2. API 风格对比
Vuex
javascript
// Vuex 示例
import { createStore } from 'vuex';
const store = createStore({
state() {
return {
count: 0
};
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
},
getters: {
doubleCount(state) {
return state.count * 2;
}
}
});
Pinia
javascript
// Pinia 示例
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++;
},
incrementAsync() {
setTimeout(() => {
this.increment();
}, 1000);
}
},
getters: {
doubleCount: (state) => state.count * 2,
}
});
3. 核心差异点
| 特性 | Vuex | Pinia |
|---|---|---|
| Mutations | 强制使用(同步操作) | 移除,直接在 actions 中修改状态 |
| 类型支持 | 需额外配置(如 vuex-module-decorators) | 原生支持,类型自动推导 |
| 模块化方式 | 嵌套 modules(结构复杂) | 扁平化 store(无嵌套) |
| 代码结构 | 按功能拆分(state/mutations/actions) | 按 store 拆分(每个 store 包含完整逻辑) |
| 异步处理 | 依赖 actions | 直接在 actions 中使用 async/await |
| 插件系统 | 自定义插件机制 | 更灵活的插件 API |
| DevTools 支持 | 需手动集成 | 内置支持时间旅行调试、状态快照 |
4. TypeScript 支持
Vuex
-
类型定义繁琐,需手动为 state、getters 等编写类型。
-
示例:
import { GetterTree, MutationTree, ActionTree } from 'vuex'; interface State { count: number; } const state: State = { count: 0 }; const getters: GetterTree<State, State> = { doubleCount: (state) => state.count * 2 };
Pinia
-
类型自动推导,无需额外配置。
-
示例:
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, name: 'John', }), getters: { // 自动推导返回类型为number doubleCount: (state) => state.count * 2, }, actions: { increment() { this.count++; // 类型安全 }, }, });
5. 性能对比
-
Pinia:
- 初始化速度更快(无 mutations 处理)。
- 内存占用更低(扁平化结构)。
- 状态更新时直接触发响应式,无需额外订阅。
-
Vuex:
- 在大型应用中因模块嵌套和 mutations 机制,可能存在性能损耗。
6. 适用场景
-
Pinia:
- 新项目或 Vue 3 应用。
- 需要更好的 TypeScript 支持。
- 偏好简洁 API 和组合式写法。
-
Vuex:
- 现有 Vue 2 项目的维护。
- 严格遵循 Flux 架构的团队。
- 需要依赖 Vuex 生态插件(如持久化、中间件)。
7. 迁移指南
从 Vuex 迁移至 Pinia 的关键点:
-
移除 mutations:将所有 mutations 逻辑合并到 actions 中。
-
模块化调整:将嵌套 modules 转换为扁平化的 store。
-
API 更新:
mapState→storeToRefsmapGetters→storeToRefs(getters 自动转为 ref)mapActions→ 直接调用 store 的 actions
-
类型定义简化:移除冗余的类型声明,依赖 Pinia 的自动推导。
总结
Pinia 作为 Vuex 的演进,通过简化 API、强化类型支持和提升性能,成为 Vue 状态管理的首选方案。对于新项目,推荐使用 Pinia;对于现有 Vuex 项目,可根据团队技术栈和维护成本决定是否迁移
Vue 插槽(Slots)
Vue 插槽(Slots)是组件复用和内容分发的核心机制,允许父组件向子组件注入自定义内容,实现灵活的布局和逻辑分离。以下从基础用法、作用域插槽到高级应用全面解析:
1. 基础插槽:内容分发
单插槽(默认插槽)
子组件通过 <slot> 定义插槽位置,父组件可直接传入内容:
<!-- 子组件:MyComponent.vue -->
<div class="child">
<slot></slot> <!-- 父组件内容将插入此处 -->
</div>
<!-- 父组件使用 -->
<MyComponent>
<p>这是父组件传入的内容</p>
</MyComponent>
具名插槽
通过 name 属性定义多个插槽,父组件使用 v-slot 或简写 # 指定内容位置:
<!-- 子组件 -->
<div class="child">
<header>
<slot name="header"></slot> <!-- 头部插槽 -->
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot> <!-- 底部插槽 -->
</footer>
</div>
<!-- 父组件使用 -->
<MyComponent>
<template #header>
<h1>标题</h1>
</template>
<p>主要内容</p> <!-- 自动匹配默认插槽 -->
<template #footer>
<p>页脚信息</p>
</template>
</MyComponent>
2. 作用域插槽:子组件数据暴露
原理
子组件通过 <slot> 的 v-bind 传递数据,父组件在接收时使用 (props) 解构数据:
<!-- 子组件:UserList.vue -->
<ul>
<li v-for="user in users">
<slot :user="user"> <!-- 暴露user数据给父组件 -->
{{ user.name }} <!-- 默认内容 -->
</slot>
</li>
</ul>
<script>
export default {
data() {
return {
users: [{ name: 'Alice' }, { name: 'Bob' }]
}
}
}
</script>
<!-- 父组件使用 -->
<UserList v-slot="slotProps">
<template #default="slotProps"> <!-- 完整写法 -->
<span>{{ slotProps.user.name.toUpperCase() }}</span>
</template>
</UserList>
<!-- 简写:直接解构props -->
<UserList #default="{ user }">
<span>{{ user.name.toUpperCase() }}</span>
</UserList>
具名作用域插槽
每个插槽可独立传递不同数据:
vue
<!-- 子组件 -->
<slot name="header" :title="pageTitle"></slot>
<slot :user="currentUser"></slot>
<!-- 父组件 -->
<MyComponent>
<template #header="{ title }">
<h1>{{ title }}</h1>
</template>
<template #default="{ user }">
<p>{{ user.name }}</p>
</template>
</MyComponent>
3. 高级应用
动态插槽名(Vue 3)
使用表达式作为插槽名:
vue
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 简写 -->
<template #[dynamicSlotName]>
...
</template>
作用域插槽的高级解构
支持默认值和别名:
vue
<UserList #default="{ user = { name: 'Guest' } }">
<!-- 当user为null时使用默认值 -->
</UserList>
<UserList #default="{ user: person }">
<!-- 将user重命名为person -->
</UserList>
后备内容(Fallback Content)
当父组件未提供内容时,显示插槽内的默认内容:
vue
<slot>
<p>默认内容</p>
</slot>
4. 插槽与组件设计
组合式组件
通过插槽实现灵活的组件组合:
<!-- 模态框组件 -->
<div class="modal">
<header>
<slot name="title">默认标题</slot>
</header>
<div class="content">
<slot></slot>
</div>
<footer>
<slot name="footer">
<button @click="close">关闭</button>
</slot>
</footer>
</div>
<!-- 使用 -->
<Modal>
<template #title>自定义标题</template>
<p>自定义内容</p>
<template #footer>
<button @click="submit">提交</button>
<button @click="cancel">取消</button>
</template>
</Modal>
渲染函数中的插槽
在 render 函数中访问插槽内容:
// Vue 3
export default {
render() {
return h('div', [
this.$slots.header?.(), // 具名插槽
this.$slots.default?.() // 默认插槽
]);
}
}
5. Vue 3 与 Vue 2 的差异
| 特性 | Vue 3 | Vue 2 |
|---|---|---|
| 语法 | v-slot 或 # | slot 和 slot-scope(已废弃) |
| 作用域插槽简写 | 直接解构 #default="{ user }" | 需使用 slot-scope |
| 动态插槽名 | 支持 #[expression] | 不支持 |
| 渲染函数 | this.$slots.header() | this.$scopedSlots.header() |
6. 最佳实践
- 合理使用具名插槽:当组件有多个内容区域时,使用具名插槽提高可读性。
- 暴露必要数据:通过作用域插槽将子组件数据暴露给父组件,避免过度耦合。
- 避免深层嵌套:插槽嵌套过深会降低代码可读性,可考虑组件拆分。
- 提供后备内容:为插槽添加默认内容,增强组件健壮性。
总结
Vue 插槽通过内容分发和作用域机制,实现了组件的高度复用和灵活扩展:
- 基础插槽:实现内容的简单注入。
- 作用域插槽:让父组件能够访问子组件数据,动态定制渲染逻辑。
- 具名插槽:支持多区域内容分发,构建复杂布局。
掌握插槽是构建可复用组件库和复杂应用的关键,尤其在开发 UI 组件库时,插槽设计直接影响组件的灵活性和扩展性。