文章目录
- Vue基础
-
- [1. Vue的基本原理](#1. Vue的基本原理)
- [2. 双向数据绑定的原理](#2. 双向数据绑定的原理)
- [4. MVVM、MVC、MVP](#4. MVVM、MVC、MVP)
- [5. Computed 、 Watch 、Methods的区别](#5. Computed 、 Watch 、Methods的区别)
- [6. slot是什么?有什么作用?原理是什么?](#6. slot是什么?有什么作用?原理是什么?)
- [7. 过滤器的作用,如何实现一个过滤器](#7. 过滤器的作用,如何实现一个过滤器)
- [8. 如何保存页面的当前的状态](#8. 如何保存页面的当前的状态)
- [9. 常见的事件修饰符及其作用](#9. 常见的事件修饰符及其作用)
- 10、v-if、v-show、v-html
- 11、v-model
- 12、data为什么是一个函数而不是对象
- 13、对keep-alive的理解,它是如何实现的,具体缓存的是什么
- [14、nextTick原理及作用](#14、nextTick原理及作用)
- 15、操作vue中data中的对象属性
-
- 15.1、vue中data中的对象属性添加一个新的属性时会发生什么?如何解决
- [15.2、Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?](#15.2、Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?)
- 知识点详解
- 16、Vue中封装的数组方法有些,其如何实现页面更新
- 17、Vue单页应用与多页应用的区别
- [18、Vue template到render的过程(Vue模版编译原理)](#18、Vue template到render的过程(Vue模版编译原理))
- 19、vue中的mixin、extends
- 20、描述下Vue自定义指令,及应用场景
- 21、Vue是如何收集依赖的?
- 22、对React和Vue的理解,它们的异同
- 23、Vue的优点
- 24、assets和static的区别
- 25、delete和vue.delete删除数组的区别
- 26、对SSR的理解
- 27、Vue的性能优化有些
- 28、template和jsx的有什么分别?
- 29、vue初始化面闪动间题
- 30、vue组件里的定时器怎么销毁??
- 31、vue权限控制
- 32、vue组件的scoped属性作用
- 33、v-on事件绑定原理
- 34、vue2中常用的内置API
- 35、vue动态组件
- 36、编写可复用组件
- 37、v-if和v-for优先级
- vue生命周期
- 组件通信方式有些
- 路由Vue-Router
-
- 1、Vue-Router是干什么的?原理
- 2、Vue-Router的使用方式
- 3、路由跳转方法
- 4、动态路由?如何获取传过来的动态参数
- 5、Vue-Router的懒加载如何实现
- 6、路由的hash和history模式的区别
- 7、如何获取页面的hash变化
- 8、`route和router`的区别
- 9、Vue-rouer路由钩子在生命周期的体现
- 10、Vue-router的跳转和location.href有什么区别
- 11、编程式导航传参
- [Router 的传参方式有哪些?Vue2 和 Vue3 有什么区别?](#Router 的传参方式有哪些?Vue2 和 Vue3 有什么区别?)
- 12、params和query的区别
- 13、Vue-router导航守卫有哪些
- 14、对前路由的理解
- 15、在组件中监听路由参数变化??
- 16、vue-router导航故障
- vuex
- vue3
- 虚拟DOM
Vue基础
1. Vue的基本原理
面试简答
关于Vue的响应式原理,Vue2是通过Object.defineProperty来实现的。
1、在页面加载时,Vue会遍历data中的所有属性,并使用Object.defineProperty将它们转换为getter/setter。
2、当用户访问或设置属性时,会触发getter/setter方法,然后通知每个组件实例对应的watcher方法,从而实现视图的更新。
但是,Object.defineProperty也有一些缺点 :
1️⃣ 对于复杂对象需要深度监听,计算量较大。(对对象的每个属性递归定义 getter/setter)
2️⃣ 对于对象的新增/删除属性的操作无法监听,需要使用Vue.$set、Vue.$delete辅助。(如新增属性 obj.newProp = 'value' 不会触发视图更新 )
3️⃣ 需要重写数组原生方法来实现数组的监听。无法监听以下操作:
-
直接通过索引赋值:arr[0] = 'new';
-
修改数组长度:arr.length = 0
因此,在Vue3中,使用了Proxy来代替Object.defineProperty。Proxy有以下优势 :
1️⃣ 可以直接监听整个对象,性能提升。
2️⃣ 可以直接监听数组的变化。
3️⃣ Proxy有多达13种拦截方法,功能更强大。
1.1、defineProperty和proxy的区别
1.2、vue3.0为什么要用proxy
2. 双向数据绑定的原理
什么是双向绑定
双向绑定 指的是在 数据模型(Model) 和 用户界面(View) 之间建立的一种双向连接。
"数据变,视图变;视图变,数据也变"
双向绑定原理
vue.js采用数据劫持结合发布者-订阅者模式 的方式,通过Object.defineProperty()来劫持各个属性的setter和getter ,在数据发生变动时发布消息给订阅者 ,触发 相应的监听回调 。
分为一下几个步骤:
- 对需要监听的数据对象进行递归遍历 ,包括子属性对象的属性,都加上setter、getter ,改变其中值 就会触发setter,Observer就能监听到了
- compile解析模板指令 ,将模板指令替换成数据 ,然后初始化渲染页面 视图,绑定更新函数 ,添加订阅者 ,一旦有数据变化 就通知watcher进行更新
- watcher是observer和compile的通信桥梁 ,主要作用:
- 自身实例化时往Dep中添加订阅者
- 自身必须要有一个
update()函数 - 等属性变动时Dep会调用
dep.notify()通知watcher,watcher调用自身的update()方法进行更新,并触发compile绑定的回调函数
Dep:属性订阅器
Observer:监听器(对所有属性进行监听)
Compile: 解析器 (对每个节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数)
Watcher:观察者(负责建立 Vue 实例属性与视图之间的联系,在属性值发生变化时更新视图)

4. MVVM、MVC、MVP
MVVM、MVC、MVP的区别
都是项目的架构模式 (不是类的设计模式),即:一个项目的结构,如何分层,不同层负责不同的职责
M:model,模型:主要完成业务功能 ,在数据库相关的项目中,数据库的增删改查 属于模型(重点)。没有页面,是纯粹的逻辑
V:view,视图:主要负责数据的显示 (HTML+ CSS ,动态网页(jsp,含有html的php文件))页面的展示和用户的交互 。
C:controller,控制器:主要负责每个业务的核心流程 ,在项目中体现在路由以及中间件上(nodeJS中的routes文件夹)
MVP是把MVC中的C改成了P(Presenter表示器) 。主要限制了M和V之间不能直接通信 (互相调用,传递数据)。需要P连接M和V,完成Model层与View层的交互,还可以进行业务逻辑的处理。
MVVM是把MVP中P改成了VM(ViewModel)。主要体现的是M和V之间数据通信,且是双向绑定。View的变动可以同步响应在Model,反之亦然。无需人为干涉。
MVVM的优缺点(面试)
优缺点:
- 优点:开发效率高,代码量少;View与Model解耦彻底;可测试性好。
- 缺点:过度依赖数据绑定可能导致调试困难;内存泄漏风险
详细知识点
MVC(Model-View-Controller)
1、核心特点:
Controller作为协调者,接收用户输入,操作Model并选择View进行渲染。而在某些实现中View可以直接访问Model,导致View与Model之间存在一定耦合
2、工作流程: 用户操作 →触发 Controller → 更新 Model → 通知 View 更新(或直接由 Controller 更新 View)3、优缺点:
- 优点:结构清晰,分离了视图层和业务层,耦合度相对较低,易于理解。
- 缺点:View 与 Model 耦合,难以测试;复杂应用中 Controller 可能变得臃肿
4、适用场景: 传统多页面应用(如服务端渲染的电商网站)
MVP(Model-View-Presenter)
1、核心特点: Presenter完全隔离了View与Model。View不直接使用Model,所有交互都通过Presenter进行,View变得被动(通常称为Passive View)
2、工作流程: 用户操作 → View → 调用 Presenter → Presenter 更新 Model → Presenter 更新 View
3、优缺点:
- 优点:View与Model完全解耦,View可高度复用和组件化;便于单元测试
- 缺点:需手动同步View和Model的状态,可能变得臃肿,维护成本较高。
4、适用场景: Android 应用开发
MVVM(Model-View-ViewModel)
1、核心特点: 引入ViewModel,并通过数据绑定(特别是双向绑定)实现View与Model的自动同步。View的变动自动反映在ViewModel,反之亦然,无需手动更新
2、工作流程: 用户操作 → View → 触发 ViewModel 命令 → 更新 Model → ViewModel 自动同步到 View(通过数据绑定)
3、优缺点:
- 优点:开发效率高,代码量少;View与Model解耦彻底;可测试性好。
- 缺点:过度依赖数据绑定可能导致调试困难;内存泄漏风险
4、适用场景: 现代 SPA(如 Vue 或 Angular 应用),通过数据绑定实现动态 UI
5. Computed 、 Watch 、Methods的区别
Computed:侧重"计算",依赖其他属性值, 派生出新数据,并缓存结果
Watch:侧重"响应",监听数据变化,执行操作
Methods:方法,调用后才执行
区别
1、缓存机制: computed具有缓存机制(依赖变化时才重新计算,依赖不变时多次使用读取缓存结果); watch没有(每次变化都会触发);watch、methods没有
2、返回值: computed必须有返回值(返回由依赖属性值派生出的新数据);watch、methods没有
3、异步支持: computed不支持异步操作;watch、watch支持
4、执行时机: computed在数据变化时执行;而watch可以设置immediate初始化时就执行;watch调用时才执行
5、使用场景: computed 用于由多个值派生出一个值,且该值经常被使用时(如:过滤、格式化等);watch用于执行复杂逻辑、异步操作、监听路由参数等;methods处理复杂逻辑操作
javascript
//data数据
age:10
//computed
computed:{
// 仅可读(getter)
fullAge(){ return 22 }
//可读可写(getter、setter)
fullAge:{
get(){ return 22 },
set(val){
this.data = val
}
}
}
//watch
watch:{
//不需要配置项时
watchAge(newValue,oldValue){}
//需要配置项时
watchAge:{
deep:true,
immediate:true,
handler(){newValue,oldValue}
}
}
//methods
methods:{
changeMethod(){}
}
6. slot是什么?有什么作用?原理是什么?
slot是插槽,是Vue中的占位符,可以通过slot标签向组件内部插入内容
原理:
子组件渲染时,遇到 标签,会用父组件传入的内容替换该位置;作用域插槽通过 v-slot 接收子组件传递的数据,实现双向通信
作用:
- 提高组件复用性
- 支持定制化渲染(如布局组件、弹窗、表格列等)
- 分为三类:默认插槽、具名插槽、作用域插槽
分类
默认插槽(匿名插槽):- 无 name 属性,父组件未指定插槽的内容会自动填充到这里
- 使用场景:卡片、弹窗、布局组件的内容区域
具名插槽:- 通过 name 属性标识,子组件可以放多个插槽,父组件根据名字填充对应内容
- 使用场景:页面头部、导航、页脚等分区
作用域插槽:- 作用域插槽其实就是
带参数的插槽,简单的来说就是子组件传参数给父组件,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容 - 使用场景:列表项、表格单元格、下拉选项等需要自定义内容
- 作用域插槽其实就是
html
<!--父组件-->
<template>
<son>
<!--默认插槽-->
<!--若只有一个默认插槽时,直接放内容就行-->
这个内容是放到没起名的slot
<template>有其他插槽时,要用template包裹内容</template>
<!--具名插槽:v-slot可简写成#-->
<template v-slot="header">放到叫header的slot里</template>
<template #header>放到叫header的slot里</template>
<!--作用域插槽-->
<template #default="data">
<ul>
<li v-for="item in user.data">{{item}}</li>
</ul>
</template>
</son>
</template>
<!--子组件son:放置插槽待父组件填内容-->
<template>
<p><slot name="header" /></p>
<slot />
<slot :data="data" />
</template>
<script>
//定义一个data数据
</script>
7. 过滤器的作用,如何实现一个过滤器
作用: 在Vue中使用
filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)使用方式:使用管道符
|,两种位置<div v-bind:id="rawId | changeId">{``{ age | changeAge }}</div>
js
filters: {
changeId(id) {
// 编写过滤的逻辑,即对入参str的处理
return ...;
}
}
分类
- 全局过滤器:通过
Vue.filter()方法定义的过滤器,在整个应用程序中都可以使用。 - 局部过滤器:在Vue
组件选项中通过filters属性定义的过滤器,只能在该组件及其子组件中使用
html
<script>
// 全局过滤器
Vue.filter('changeId', (id) => {
return id + '~~~'
})
</script>
<script>
const vm1 = new Vue({
el: '#app1',
data: {
id:11
},
// 私有过滤器,只能被当前 vm 所控制的区域所使用
filters: {
changeId(id) {
return id+ '-'
},
},
})
</script>
过滤器传参
js
<p>{{ message | filterA(arg1, arg2)}}</p>
Vue.filter('filterA', (msg, arg1, arg2) => {
// 过滤器逻辑代码
})
8. 如何保存页面的当前的状态
问题:
"用户从列表页进入详情页,返回时列表滚动位置丢失。"
"表单填写了一半,刷新后内容全没了。"
"多个标签页切换,每个页面的状态都该独立保存。"
面试简答
两种情况及解决方案
组件会被卸载(如路由跳转)- LocalStorage / SessionStorage(适用于刷新后需保留)
- 组件销毁前 (beforeDestroy) 保存状态 ,写法:
localStorage.setItem('listState', JSON.stringify(state)); - 组件创建时 (created 或 mounted) 恢复状态 ,写法:
JSON.parse(localStorage.getItem('listState')) - 缺点:JSON不能处理Function、Date等;需手动恢复逻辑
- 组件销毁前 (beforeDestroy) 保存状态 ,写法:
- 路由传参(适用于短期传递,同域跳转)
- 利用
vue-router 的 route.state(vue3)或route.query(vue2);将状态作为路由参数传递;目标页面通过this.$route 读取 - 存
this.$router.push({ name:xx, state/query:{} }); 取this.$route.query/state - 缺点:页面刷新后状态丢失;多入口需重复逻辑
- 利用
- LocalStorage / SessionStorage(适用于刷新后需保留)
组件不会被卸载(如动态切换)- 父组件统一管理(Single Render)(适用于简单多视图切换)
- 将要切换的组件 都定义为每个子组件 ,都引入装载到父组件上
- 缺点:父组件逻辑臃肿;无法通过 URL 直接定位页面
- keep-alive(适用于路由级缓存)
- 用于缓存动态组件或路由视图;有两个生命周期钩子
activated:组件激活时调用(注意避免重复请求);deactivated:组件失活时调用 - 使用
keep-alive包裹要缓存的组件,在当前组件的路由定义中设置配置项meta:{ keepAlive:true },在当前的vue文件中使用生命周期钩子刷新
- 用于缓存动态组件或路由视图;有两个生命周期钩子
- 父组件统一管理(Single Render)(适用于简单多视图切换)
使用选择
- 对于需要
跨组件共享的状态,使用Vuex; - 对于需要
跨页面共享的状态,考虑使用localStorage或sessionStorage; - 对于需要
保持组件激活状态,使用<keep-alive>; - 对于
简单的数据保存,直接使用localStorage或sessionStorage; - 对于需要
在路由变化时保存状态的场景,结合Vue Router的导航守卫和Vuex使用
知识点详解
方法一:LocalStorage / SessionStorage
原理:利用浏览器的本地存储;
组件销毁前 (beforeDestroy) 保存状态;组件创建时 (created 或 mounted) 恢复状态写法:
- 存
localStorage.setItem('listState', JSON.stringify(state));;- 取
JSON.parse(localStorage.getItem('listState'))优点:
- 兼容性好;
- 页面刷新后状态仍在
缺点:
- JSON.stringify() 无法处理 Date, RegExp, Function, Symbol 等类型;
- 需手动管理恢复逻辑;
- 存储大小有限(~5MB)
js
// 恢复状态
created() {
const saved = localStorage.getItem('listState');
if (saved) {
const state = JSON.parse(saved);
// 只在"返回"时恢复(通过 flag 控制)
if (state.fromDetail) {
Object.assign(this.$data, state.data);
}
}
},
// 保存状态
beforeDestroy() {
const state = {
fromDetail: true, // 标记来源
data: {
list: this.list,
page: this.page,
searchQuery: this.searchQuery,
scrollTop: this.scrollTop
},
timestamp: Date.now()
};
localStorage.setItem('listState', JSON.stringify(state));
},
//使用
methods: {
saveScroll() {
this.scrollTop = this.$el.scrollTop;
}
}
注意:防止重复恢复,在路由守卫中清除标记
js
// 在路由守卫中清除标记
router.beforeEach((to, from, next) => {
if (to.name !== 'List' && from.name === 'List') {
const saved = localStorage.getItem('listState');
if (saved) {
const state = JSON.parse(saved);
state.fromDetail = false; // 下次进入不恢复
localStorage.setItem('listState', JSON.stringify(state));
}
}
next();
});
方法二:路由传参
原理:利用
vue-router 的 route.state(vue3)或route.query(vue2);将状态作为路由参数传递;目标页面通过this.$route 读取写法:
- 存
this.$router.push({ name:xx, state/query:{} });- 取
this.$route.query/state优点:
- 可传递复杂对象(无 JSON 序列化问题);
- 不污染本地存储
缺点:
- 页面刷新后状态丢失;
- URL 变长,不够优雅;
- 多入口需重复逻辑
js
//vue2
// 模拟 state 传递
this.$router.push({
name: 'List',
query: {
restore: 'true',
page: this.page,
q: this.searchQuery
}
});
// 接收
created() {
const { restore, page, q } = this.$route.query;
if (restore) {
this.page = Number(page);
this.searchQuery = q;
// 触发数据加载
}
}
//vue3
// 跳转时携带状态
this.$router.push({
name: 'List',
state: {
list: this.list,
page: this.page,
searchQuery: this.searchQuery
}
});
// 在目标组件中读取
created() {
const { state } = this.$route;
if (state) {
Object.assign(this.$data, state);
}
}
方法三:父组件统一管理(Single Render)
原理:要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态
写法:将要切换的组件都定义为每个子组件,都引入装载到父组件上,父组件判断管理
优点:
- 状态天然保留,无需额外处理;
- 切换极快,无重渲染开销
缺点:
- 所有组件常驻内存,占用高;
- 父组件逻辑臃肿;
- 无法通过 URL 直接定位页面(不利于分享和 SEO)
js
<!-- Parent.vue -->
<template>
<div class="container">
<!-- 所有子页面作为全屏组件渲染 -->
<ListComponent v-if="currentView === 'list'" />
<DetailComponent v-if="currentView === 'detail'" />
</div>
</template>
<script>
export default {
data() {
return {
currentView: 'list'
};
},
provide() {
return {
switchTo: (view) => {
this.currentView = view;
}
};
}
};
</script>
方法四:keep-alive(Vue 官方缓存方案)
介绍:
keep-alive是 Vue 内置组件,用于缓存动态组件或路由视图;有两个生命周期钩子activated:组件激活时调用(注意避免重复请求);deactivated:组件失活时调用原理:被包裹的组件在切换时不会被销毁,而是被缓存
写法:使用
keep-alive包裹要缓存的组件,在当前组件的路由定义中设置配置项meta:{ keepAlive:true },在当前的vue文件中使用生命周期钩子优点:
- Vue 官方支持,稳定可靠;
- 自动管理组件实例,状态无缝保留;
- 支持条件缓存(通过 include / exclude)
<keep-alive include="List,Search">缺点:
- 缓存组件常驻内存,可能影响性能;
- 需要合理设置 max 属性控制缓存数量
<keep-alive :max="5">
html
<!-- App.vue -->
<template>
<div id="app">
<!-- 缓存需要 keepAlive 的路由 -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<!-- 不需要缓存的路由 -->
<router-view v-if="!$route.meta.keepAlive" />
</div>
</template>
js
// router.js
const routes = [
{
path: '/list',
name: 'List',
component: () => import('@/views/List.vue'),
meta: {
keepAlive: true // 标记需要缓存
}
},
{
path: '/detail/:id',
name: 'Detail',
component: () => import('@/views/Detail.vue')
// 默认不缓存
}
];
js
// List.vue
export default {
data() {
return {
list: [],
page: 1,
searchQuery: ''
};
},
activated() {
console.log('List 组件被激活');
// 可在此处执行刷新逻辑(如果需要)
},
deactivated() {
console.log('List 组件被缓存');
// 组件状态自动保留
}
}
9. 常见的事件修饰符及其作用
.stop:等同于 js 中的 event.stopPropagation() ,阻止事件冒泡(孙到父);.prevent :等同于 js 中的 event.preventDefault() ,阻止默认事件.capture :事件捕获(父到孙);.self :事件绑定在自身,阻止冒泡.once :只触发一次。
10、v-if、v-show、v-html
10.1、v-if、v-show、v-html的原理
v-if:根据条件选择性地渲染元素 。它在编译阶段决定是否创建DOM元素 。具体操作包括调用addIfCondition方法来决定在 render 过程中是否渲染该节点v-show:根据条件控制元素的显示/隐藏 。v-show始终保留DOM元素,仅通过修改CSS属性来切换显示状态。在操作上,会生成对应的vnode,通过修改节点的属性来控制显示v-html:用于插入HTML内容 。需注意潜在的XSS攻击风险。在操作上,会移除节点下所有子节点 ,并调用 html 方法以设置节点的 innerHTML 属性 。这其实会通过addProp来添加 innerHTML 属性
10.2、v-if和v-show的区别
**相同点:**都是用来控制dom元素的显示和隐藏
区别
- 原理上:
v-if:通过 添加和删除dom元素,来 控制dom元素的显示和隐藏v-show:通过 控制dom元素样式的display属性的值,来 控制dom元素的显示和隐藏
- 编译条件:
v-if:是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译v-show:在任何条件下都被编译并缓存,DOM元素保留
- 性能消耗:
v-if:有更高的切换 消耗v-show:有更高的初始渲染 消耗
- 适用场景:
v-if:适合不常切换的场景v-show:适合频繁切换的场景
11、v-model
11.1、v-model是如何实现的,语法糖实际是什么?
实现了表单输入和应用状态之间的双向数据绑定;实现方式取决于绑定元素的类型
-
作用在表单元素上:
动态绑定了 input 的 value 到指定的变量,并在触发 input 事件时动态更新这个变量
使用场景- 对于
text和textarea元素,v-model 使用value属性和input事件; - 对于
checkbox和radio元素,使用checked属性和change事件; - 对于
select字段,将value属性和change事件
html<input v-model="message" /> // 等同于 <input :value="message" @input="message=$event.target.value"> //$event 指代当前触发的事件对象; //$event.target 指代当前触发的事件对象的dom; //$event.target.value 就是当前dom的value值; //在@input方法中,value => message; //在:value中,message => value; - 对于
-
作用在组件上:
在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件 。这本质上是父子组件通信 的语法糖,通过 prop 和 $emit 实现 。html// 父组件 <child v-model="aa"></child> // 等价于 <child :value="aa" @input="aa=$event.target.value"></child> // 子组件: <input :value="aa" @input="onmessage" /> props:{value:aa,} methods:{ onmessage(e){ $emit('input',e.target.value) } }
11.2、v-model可以被用在自定义组件上吗?如果可以,如何使用
在自定义组件上使用 v-model 时,实际上是在创建一个双向绑定
vue2实现方式
使用 v-model 的步骤
1、在子组件中定义model和props。
2、通过发送在model里定义的事件通知v-model绑定值的改变
html
//父组件引入子组件
<custom-input v-model="parentData"></custom-input>
<!-- 等价于 -->
<custom-input :value="parentData" @input="parentData = $event" />
//子组件
<template>
<input :value="value" @input="updateValue($event)">
//或直接写方法
<input :value="value" @input="$emit('input', $event.target.value)">
<template>
<script>
export default {
model:{
prop:'value',
event:'change'
},
props:{
value:number
},
methods:{
updateValue(event){
this.$emit('change',event.target.value)
}
}
};
</script>
vue3实现方式
v-model 不再固定绑定 value 和 input,而是更灵活地支持多个 v-model 绑定
html
<CustomComponent v-model:title="title" v-model:content="content" />
<!-- 等价于 -->
<CustomComponent :title="title" @update:title="title = $event" :content="content" @update:content="content = $event" />
html
// 修改prop名称
<template>
<CustomComponent v-model:customModel="data" />
</template>
<script>
export default defineComponent({
props: ['customModel'],
emits: ['update:customModel'],
setup(props, { emit }) {
const handleChange = (e) => { emit('update:customModel', e.target.value);};
return { handleChange };
}
});
</script>
12、data为什么是一个函数而不是对象
总结:
为了保证组件实例之间的数据隔离,避免数据污染
当一个组件被复用多次时:
- 如果data是对象:
所有实例共享同一个data对象(引用类型),一个实例修改数据会影响其他所有实例 - 如果data是函数:
每次创建实例时调用函数,返回一个全新的数据对象,实现数据隔离
js
data() { return{data数据} }
13、对keep-alive的理解,它是如何实现的,具体缓存的是什么
keep-alive是什么:
是一个内置组件,用于缓存不活动的组件实例,避免重复渲染,从而优化应用性能。
它常用于需要保留组件状态或避免重复加载的场景(如标签页切换、路由视图缓存)
核心作用:
- **缓存组件实例:**当组件被切换时,不会销毁,而是保留在内存中
- **保留组件状态:**保持当前组件的所有状态(data、DOM 结构等)
- **避免重复渲染:**再次激活时直接复用缓存,跳过创建/挂载过程
用法:
- include: 定义 缓存白名单,缓存命中的组件;
- exclude: 定义缓存黑名单 ,不缓存命中组件;
- max: 定义缓存组件上限,最多可以缓存多少组件实例
html
//在动态组件中使用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
//在vue-router中使用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<router-view></router-view>
</keep-alive>
缓存的生命周期:
- activated: 组件被激活(进入缓存视图)时触发,用于刷新数据、启动动画
- deactivated: 组件被停用(离开缓存视图)时触发,用于停止定时器、保存临时状态
缓存后获取数据:
如果需要
强制重新渲染被缓存的组件,可以使用this.$forceUpdate()方法
- beforeRouteEnter: 每次组件渲染都会执行
- actived: 缓存组件被激活时
js
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
//actived
activated(){
this.getData() // 获取数据
},
缓存使用场景:
- 缓存想要缓存的页面,实现后退不刷新:
- 实现方法:
1、在 App.vue 中的路由组件 router-view 上加上 keep-alive
2、在 router.js 中为需要缓存的路由设置缓存参数
注意点:用 keep-alive 包裹需要缓存的路由,但是判断条件不要写在 keep-alive 上,需要写在 router-view 上,不然会没有作用
- 实现方法:
- 页面切换时保留滚动位置:
- 方法一:scrollBehavior(在路由表中配置),仅在history模式下有
- 方法二:activated+deactivated
- 详情页返回列表页不刷新,其他页面进入列表页刷新:
- 方法:
1、在路由的 mate 中添加一个变量,表示是否需要刷新
2、在页面组件的 data 中定义一个变量,表示是否是第一次进入该页面
3、在相关的钩子函数中判断是否是第一次进入页面,是否需要重新加载数据
- 方法:
缓存想要缓存的页面,实现后退不刷新:
html
//App.vue
<!--缓存想要缓存的页面,实现后退不刷新-->
<!--加上v-if的判断,可以自定义想要缓存的组件,自定义在router里面-->
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
js
//router.js
{path: '/msg', name: 'Message', meta: {keepAlive: true}, component: Message},
页面切换时保留滚动位置:
js
//方法一:scrollBehavior(在路由表中配置)
//router.js
const router = new Router({
mode: 'history',
routes: [{path: '/',name: 'Home',component: Home}],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
// 如果存在savedPosition,则滚动到之前的位置
return savedPosition;
} else {
// 否则,滚动到页面顶部或其他特定位置
return { x: 0, y: 0 }; // 滚动到页面顶部
// 或者返回特定的选择器,例如:document.querySelector('#scrollToMe').offsetTop
}
}
//方法二:activated+deactivated
//在当前页面组件中
export default {
data() {
return { scrollY: 0 };
},
activated() {
// 恢复滚动位置
window.scrollTo(0, this.scrollY);
},
deactivated() {
// 保存滚动位置
this.scrollY = window.scrollY;
},
};
详情页返回列表页不刷新,其他页面进入列表页刷新:
js
//route.js 配置keepAlive: true, isNeedRefresh: true
{path: '/msg', name: 'Message', meta: {keepAlive: true, isNeedRefresh: true}, component: Message}
//页面组件中
data () {
return {
isFirstEnter: false,
}
}
//在相关钩子函数
beforeRouteEnter (to, from, next) {
// 利用路由元信息中的 meta 字段设置变量,方便在各个位置获取。这就是为什么在 meta 中定义 isNeedRefresh。
// 当从详情页返回时,将 isNeedRefresh 设为 false,表示不刷新数据
if (from.name === 'MsgDetail') {
to.meta.isNeedRefresh = false
}
next()
},
mounted () {
// 只有第一次进入或者刷新页面后才会执行此钩子函数,使用keep-alive后(2+次)进入不会再执行此钩子函数
// isFirstEnter 用来标记是否第一次进入该页面,以防止用户在详情页手动刷新页面,回到该页面后不再请求数据
this.isFirstEnter = true
},
activated () {
if (this.$route.meta.isNeedRefresh || this.isFirstEnter) {
// 如果 isNeedRefresh 是 true,表明需要获取新数据,否则就不再请求,直接使用缓存的数据
this.queryListData()
}
// 恢复成默认的 false,避免 isFirstEnter 一直是 true,导致重复获取数据
this.isFirstEnter = false
// 恢复成默认的 true,避免 isNeedRefresh 一直是 false,导致下次无法获取数据
this.$route.meta.isNeedRefresh = true
}
14、$nextTick原理及作用
原理
基于 js 事件循环(Event Loop),$nextTick 利用微任务(Microtask)或宏任务(Macrotask)将回调推迟 到当前同步代码执行完毕后、浏览器渲染前执行
实现机制 :1、将回调函数推入 callbacks 队列。
2、调用 timerFunc 函数,在浏览器的异步队列中放入刷新callbacks(flashcallbacks ) 函数 ,延迟执行 (根据运行环境判断将flashcallbacks() 放入宏任务或者是微任务队列, 使得 flashcallbacks 被延迟调用)
3、事件循环到了微任务或者宏任务,依次遍历执行 callbacks 数组中的所有函数,此时 DOM 已完成更新
作用
确保在 DOM 更新完成后执行回调 ,Vue 在数据变化时采用
异步批量更新机制,不会立即更新 DOM。nextTick 用于在`下一次 DOM 更新循环结束后执行`指定代码,从而安全地操作最新状态的 DOM,可以使用 `setTimeout` 来模拟 nextTick 的效果
使用场景
- 修改数据后立即获取或操作 依赖该数据的 DOM 元素(如获取尺寸、聚焦输入框)。
- 在 created() 钩子中执行 DOM 操作(此时视图尚未渲染)。
- 使用第三方库 (如 图表库、地图库)时,需等待 DOM 渲染完成后再初始化
- 表单验证
js
// Vue 2
this.message = 'updated';
this.$nextTick(() => {
console.log(this.$el.textContent); // 输出 'updated'
});
// Vue 3
import { nextTick } from 'vue';
this.message = 'updated';
await nextTick();
console.log(this.$refs.el.textContent); // 输出 'updated'
//表单验证
this.$nextTick(() => {
const input = document.querySelector('input');
if (input.value === '验证数据') {
console.log('输入验证成功');
} else {
console.log('输入验证失败');
}
});
15、操作vue中data中的对象属性
15.1、vue中data中的对象属性添加一个新的属性时会发生什么?如何解决
动态给Vue的data添加新属性时,Vue无法检测到该属性的变化,导致视图不会更新
解决
- 使用
Vue.set或this.$set,写法:Vue.$set(对象或数组, 对象属性名或数组下标, 值) Object.assign或展开运算符创建新对象,写法:Object.assign(target, ...sources)、{ ...obj1, ...obj2 }- 预先声明所有可能用到的属性,初始化属性值为null/空对象
- 替换整个对象(适用于嵌套对象)
15.2、Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会。Vue使用响应式系统,当data中的某个属性发生改变时,Vue会触发视图更新。但实际的更新可能会受到一些因素的影响,比如DOM更新是异步的,Vue可能会在下一个事件循环中批量处理DOM更新,而不是立即同步执行。这种批量处理可以提高性能。
知识点详解
1、Vue 的异步更新机制
- Vue 使用异步更新队列来优化性能 。当 data 中的某个属性发生变化 时,Vue 并不会立即更新 DOM,而是把这个更新操作放入一个队列中 。Vue 会等待当前的 JavaScript 执行栈中的所有任务完成后 ,才会开始处理 DOM 更新。
- 这种方式可以避免在一次事件循环中多次更新 DOM,从而提升性能。
2、为什么采用异步更新机制
- **性能优化:**如果每次数据变化都立即更新视图,尤其是在大量数据变化时,可能会导致多次 DOM 更新,造成性能问题 。通过异步更新,Vue 能够将多个更新合并成一个,从而减少 DOM 操作的次数。
- 事件循环机制: Vue 利用 JavaScript 的事件循环机制,首先执行同步代码 ,然后把视图更新放到事件队列中,等待所有同步任务完成后再执行更新。
3、如何处理视图更新
- Vue 在内部使用
nextTick来控制视图更新的时机 。Vue.nextTick()是 Vue 提供的一个方法,它允许在 DOM 更新完成后执行回调函数。这意味着我们可以在数据变化之后,使用 nextTick 来执行某些操作,确保在视图更新之后获取到最新的 DOM 状态。
js
this.message = 'Hello, Vue!';
this.$nextTick(() => {
console.log('DOM 已更新');
});
4、更新队列的合并
- Vue 会在同一事件循环中多次修改同一个属性时,合并多次更新操作 ,这样只会触发一次视图更新。这是通过事件队列实现的,因此即使在同一个 tick 内部多次改变数据,视图也只会更新一次。
5、强制同步更新
- 如果确实需要强制立即同步更新视图 ,可以使用
Vue.nextTick方法,但这种做法通常不推荐,除非确实有性能优化的需要
16、Vue中封装的数组方法有些,其如何实现页面更新
响应式处理利用的是
Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化
重写数组方法
push、pop、shift、unshift、splice、sort 、reverse
数组实现页面更新原理
重写了数组中的那些原生方法,首先获取到这个数组的ob,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化,然后手动调用notify,通知渲染watcher,执行update
视图更新
页面更新是通过Vue对这些方法进行特殊处理,确保当数组发生变化时,Vue能够自动更新相关的页面内容
数组
- 内置API:
push、pop、shift、unshift、splice、sort 、reverse - 将数组重新赋值,修改引用地址:深拷贝,使用slice,JSON都可以
Vue.$set():为data对象添加一个新的响应式属性,写法:Vue.$set(对象或数组, 对象属性名或数组下标, 值)
17、Vue单页应用与多页应用的区别
单页面应用(SPA):只有一个页面 的web应用,进入页面只需要加载一次相关资源(css、js等),所有内容都包含在此页面中,对每一个功能内容做组件化。
多页面应用(MPA):有多个独立页面的web应用 ,且进入每个页面都必须重复加载相关资源(js、css等)
区别
- 刷新方式
- SPA:相关组件的切换,只做局部刷新或更改
- MPA:整页资源刷新
- 页面跳转模式
- SPA:hash/history做前端路由跳转
- MPA:整体页面跳到另一个页面
- 用户体验
- SPA:用户体验良好,但首次进入白屏时间稍长
- MPA:用户体验差,页面切换满,流畅度不够
- 转场动画
- SPA:可实现,比如界面切换之类加一些骨架屏和loading之类的
- MPA:无法实现,每次切换都是白屏
- 搜索引擎优化(SEO)
- SPA:需要单独方案,实现比较困难
- MPA:实现方法容易,直接在页面上加关键字
- 开发成本和维护成本
- SPA:借助框架实现,成本低
- MPA:开发成本低,维护成本高
SPA
- 优点 :
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
- 缺点 :
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
- 前进后退路由管理 :由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
18、Vue template到render的过程(Vue模版编译原理)
在Vue中,template会被编译成一个render函数。整个过程包括以下步骤:
- 模板编译: Vue通过模板编译器将template转换为渲染函数。
- 生成AST(抽象语法树): 模板编译生成AST,表示模板的抽象语法结构。
- 生成render函数: 将AST转换为render函数,该函数返回VNode(虚拟DOM)。
- 渲染: 执行render函数,得到渲染的VNode。
- 补丁: 将新的VNode与旧的VNode进行对比,计算出最小的更新,然后将差异更新到真实DOM上。
19、vue中的mixin、extends
19.1、mixin是什么?mixin和mixins的区别
mixin是用来复用组件选项 的方式,允许将多个组件共用的数据、方法、生命周期钩子 等提取到一个独立对象中,并**"混入"到目标组件**中。
本质 :一个普通的 JavaScript 对象 ,可包含 data、methods、created、computed 等任意组件选项。
作用:避免重复代码,提升可维护性。
mixin 和 mixins 的区别
- Mixin :指的是单个 mixin 对象 ,它是一个包含组件选项的对象,通过 mixins 选项来引入单个 mixin。如
mixins: [myMixin] - Mixins :指的是多个 mixin 对象的数组 ,可以同时引入多个 mixin 来实现代码复用和扩展,如
mixins: [mixin1, mixin2, mixin3]
mixin使用方式:
- 局部混入 :通过
组件的 mixins 选项引入。 - 全局混入 :通过
Vue.mixin()应用于所有组件(不推荐滥用)
合并规则:
- 同名方法或数据:组件内的优先,覆盖 mixin 中的。
- 生命周期钩子:mixin 中的先执行,组件中的后执行
缺点: 不能明确判断来源,且有可能和当前组件内变量命名冲突
19.2、extend是什么?作用?extend和extends的区别
创建一个子类,参数是包含组件选项的对象
extend和extends的区别
extend:创建组件构造器,Vue.extend({ vue对象 })extends:组件继承,通过组件的 extends 选项引入
19.3、简述mixin、extends的区别,及覆盖逻辑
mixin、extends的区别:
- 共同的:mixin 和 extends 都是用于实现组件复用和逻辑共享的方式。
- 不同点 :
mixin用于将公共选项和逻辑混入到多个组件 中,从而实现代码复用;extends用于基于一个已有的组件创建新的组件 ,并且可以对新组件进行定制化的修改
覆盖优先级(从高到低):组件自身选项 > mixin 选项 > extends 选项
20、描述下Vue自定义指令,及应用场景
- 全局定义 :
Vue.directive( "focus",{} ),参数(指令名称,函数) - 局部定义 :
directives:{ focus:{} }
钩子函数 :指令定义对象提供钩子函数(常用:bind、inserted、update)
bind :只调用一次,指令第一次绑定到元素时调用 。在这里可以进行一次性的初始化设置。
inserted :被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update :所在组件的VNode更新时调用 ,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
ComponentUpdate :指令所在组件的 VNode及其子VNode全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
钩子函数参数 (常用:el、binding )
el:绑定元素
binding: 指令核心对象,描述指令全部信息属性
name
value
oldValue
expression
arg
modifers
vnode 虚拟节点
oldVnode:上一个虚拟节点(更新钩子函数中才有用)
21、Vue是如何收集依赖的?
依赖收集目的 :数据变化可更新视图
如何收集:
- 每个属性/对象 都有一个dep属性
- 每个组件渲染 过程中都会创建一个渲染watcher (watcher和dep是多对多的关系),一个属性可能有多个watcher,反之一样
- 调用取值方法 时,若有watcher就收集
- 数据变化后通知 自己对应的dep触发更新调用
watcher.update()
22、对React和Vue的理解,它们的异同
23、Vue的优点
- 数据双向绑定
- 组件化开发,减少代码编写
- 界面效果响应式
- 路由实现页面切换和跳转,不回刷新页面
- 单页面应用,使页面局部刷新,加快访问速度,提升用户体验
- 使用第三方UI库,节省开发时间
24、assets和static的区别
25、delete和vue.delete删除数组的区别
26、对SSR的理解
27、Vue的性能优化有些
- 编码阶段
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 如果需要使用v-for给每项元素绑定事件时使用事件代理
- SPA 页面采用keep-alive缓存组件
- 在更多的情况下,使用v-if替代v-show
- key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
- SEO优化
- 预渲染
- 服务端渲染SSR
- 打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
- 用户体验
- 骨架屏
- PWA
- 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等
28、template和jsx的有什么分别?
29、vue初始化面闪动间题
30、vue组件里的定时器怎么销毁??
js
beforeDestroy(){
//多个定时器,在data中创建一个timer对象,存储每个定时器名字
for(let k in this.timer){ clearInterval(k) }
}
//单个定时器
this.$once('hook:beforeDestroy',()=>{ clearInterval(k) })
31、vue权限控制
- 接口权限:在axios中配置,用户在登录后获取token,在axios的请求拦截器中添加token
- 路由权限
- 方法一(meta+beforeEach) :初始化时即挂载全部路由,并且在路由中的 元信息
meta标记相应的权限信息 ,通过设置全局路由守卫beforeEach,每次路由跳转前做校验。 - 方法二(addRoutes+beforeEach) :登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用
addRoutes添加路由
- 方法一(meta+beforeEach) :初始化时即挂载全部路由,并且在路由中的 元信息
- 菜单权限
- 方法一(前端定义路由,后端返回菜单) :在
beforeEach中判断,根据路由name找不到对应的菜单 ,就表示用户有没权限访问 ;路由很多时,初始化 的时候只挂载不需要权限控制的路由 ,根据菜单与路由的对应 关系,筛选出可访问的路由 ,通过addRoutes动态挂载 - 方法二(路由和菜单全由后端返回) :前端统一定义组件 ,再将后端返回的路由需要将数据处理一下,将component字段换为真正的组件 ,再通过 addRoutes 动态挂载
- 方法一(前端定义路由,后端返回菜单) :在
- 视图权限(按钮权限)
- 方法一 :通过
v-if控制按钮的显示或隐藏 - 方法二 :通过自定义指令进行按钮权限的判断(1、在路由中meta设置按钮权限;2、自定义指令;3、模版上v-xx使用)
- 方法一 :通过
1、接口权限
javascript
axios.interceptors.request.use(config => {
config.headers['token'] = cookie.get('token')
return config
})
2、路由权限
方法一:meta+beforeEach
javascript
const routerMap = [
{
path: '/permission',
component: Layout,
redirect: '/permission/index',
alwaysShow: true, // will always show the root menu
meta: {
title: 'permission',
icon: 'lock',
roles: ['admin', 'editor'] // you can set roles in root nav
},
children: [{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'pagePermission',
meta: {
title: 'pagePermission',
roles: ['admin'] // or you can only set roles in sub nav
}
}, {
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'directivePermission',
meta: {
title: 'directivePermission'
// if do not set roles, means: this page does not require permission
}
}]
}]
方法二:addRoutes+beforeEach
javascript
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // getToken from cookie
//加载进度条
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
NProgress.configure({ showSpinner: false })// NProgress Configuration
function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
if (!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role) >= 0)
}
const whiteList = ['/login', '/authredirect']// no redirect whitelist
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetUserInfo').then(res => { // 拉取user_info
const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
})
} else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
if (hasPermission(store.getters.roles, to.meta.roles)) {
next()//
} else {
next({ path: '/401', replace: true, query: { noGoBack: true }})
}
// 可删 ↑
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login') // 否则全部重定向到登录页
NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
}
}
})
router.afterEach(() => {
NProgress.done() // finish progress bar
})
3、菜单权限
方法一 : 前端定义路由,后端返回菜单
我们需要做的是路由的权限控制:
- 在全局路由守卫 中做判断,菜单的name 与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的
- 如果根据路由name找不到对应的菜单 ,就表示用户有没权限访问
- 如果路由有很多 ,可以在应用初始化 的时候, 只挂载不需要权限控制的路由 ,比如login, 404等, 当后端返回菜单后, 在进行匹配 ,筛选出可以访问的路由 , 通过 addRoutes 动态挂载
javascript
{
name: 'login',
path: '/login',
component: () => import('/@/pages/Login.vue')
}
javascript
function hasPermission(router, accessMenu) {
if (whiteList.indexOf(router.path) !== -1) {
return true;
}
let menu = Util.getMenuByName(router.name, accessMenu);
if (menu.name) {
return true;
}
return false;
}
Router.beforeEach(async (to, from, next) => {
if (getToken()) {
let userInfo = store.state.user.userInfo;
if (!userInfo.name) {
try {
await store.dispatch("GetUserInfo")
await store.dispatch('updateAccessMenu')
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
//Util.toDefaultPage([...routers], to.name, router, next);
next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路由
}
}
catch (e) {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
} else {
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
if (hasPermission(to, store.getters.accessMenu)) {
Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
} else {
next({ path: '/403',replace:true })
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
Util.title(menu.title);
});
Router.afterEach((to) => {
window.scrollTo(0, 0);
});
方法二 :
前端
javascript
const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {
home: Home,
userInfo: UserInfo
};
4、视图权限(按钮权限)
方法一: 通过v-if控制按钮的显示或隐藏
方法一: 自定义指令 进行按钮权限的判断
配置路由
javascript
{
path: '/permission',
component: Layout,
name: '权限测试',
meta: {
btnPermissions: ['admin', 'supper', 'normal']
},
//页面需要的权限
children: [{
path: 'supper',
component: _import('system/supper'),
name: '权限测试页',
meta: {
btnPermissions: ['admin', 'supper']
} //页面需要的权限
},
{
path: 'normal',
component: _import('system/normal'),
name: '权限测试页',
meta: {
btnPermissions: ['admin']
} //页面需要的权限
}]
}
自定义权限鉴定指令
javascript
import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
bind: function (el, binding, vnode) {
// 获取页面按钮权限
let btnPermissionsArr = [];
if(binding.value){
// 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
btnPermissionsArr = Array.of(binding.value);
}else{
// 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
}
if (!Vue.prototype.$_has(btnPermissionsArr)) {
el.parentNode.removeChild(el);
}
}
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
let isExist = false;
// 获取用户按钮权限
let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
return false;
}
if (value.indexOf(btnPermissionsStr) > -1) {
isExist = true;
}
return isExist;
};
export {has}
在使用的按钮中只需要引用v-has指令
html
<el-button @click='editClick' type="primary" v-has>编辑</el-button>
32、vue组件的scoped属性作用
作用: 相当于将样式私有化,即样式只用于与当前组件,使得组件之间的样式不互相污染
vue2穿透方式
css
第一种
.类名 >>> .类名{ 样式 }
第二种
/deep/ .类名{ 样式 }
第三种
::v-deep .类名{ 样式 }
vue3穿透方式
css
第一种::deep()
:deep( 样式选择器 )
第二种:::v-deep()
::v-deep( 样式选择器 )
33、v-on事件绑定原理
v-on或@
原理:先模板编译--生成-->AST抽象语法树,生成render函数后执行得到vNode,vNode--生成-->真DOM时使用addEventListenter绑定
34、vue2中常用的内置API
内置属性(this.$xxx):
$data当前组件datael当前组件根节点children当前组件所有字组件$refs通过 ref 属性标记的DOM元素或子组件实例
内置方法(this.$xxx()):
$set(target, key, value)响应式对象添加新属性(或修改数组元素)(常问:数据更新,视图未更新怎么解决;更新某下标的某个属性)$delete(target, key)删除响应式对象的属性(或数组元素)$nextTick(callback)获取更新后的dom(常问:修改数据后立即操作DOM,DOM未更新;在created中获取dom)
35、vue动态组件
作用:vue 动态组件用于实现在指定位置上,动态加载不同的组件
html
<component :is="currentCom"></component>
<script>
data(){
return {
//componentA,当前存储的是A组件
currentCom:'componentA'
}
}
</script>
36、编写可复用组件
可复用主要条件:
prop:允许 外部传数据给组件事件:允许 触发外部的actionslot:允许 外部将内容插到组件的视图结构内
37、v-if和v-for优先级
1、实践中,v-if和v-for不应该放一起
2、vue2 中,v-for 优先级高于v-if ,先循环再判断 ;vue3 中,正相反 ,所以v-if执行时 ,它调用的变量还不存在 ,就会导致异常
vue生命周期
1、简单说一下Vue的生命周期
2、Vue子组件和父组件执行顺序
调用顺序 :先父后子,子先执行渲染完后父再执行挂载(父前三个,子前四个,父第四个)
更新顺序 :父更新导致子更新,子更新完成后父
销毁顺序 :先父后子,子先完成销毁后父才销毁
加载渲染过程 :父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
子组件更新过程 :父beforeUpdate->子beforeUpdate->子updated->父updated
销毁过程:父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
第一次进入组件执行的生命周期:
- 没加keep-alive :创建前后 (beforeCreate、created)、挂载前后(beforeMount、mounted)
- 加keep-alive:前四个+activated
第二次进入组件执行的生命周期:
- 没加keep-alive :创建前后 (beforeCreate、created)、挂载前后(beforeMount、mounted)
- 加keep-alive:只执行activated
3、created和beforeCreated的区别
beforeCreated:什么都拿不到,包括$el、data、methods
created:可以拿到data和methods,但是拿不到$el;怎么能拿到呢?写到异步 就可,比如setTimeout、nextTick
3、created和mounted的区别
- created :在模板渲染成html前 调用,即通常初始化某些属性值,然后再渲染成视图。
- mounted :在模板渲染成html后 调用,通常是初始化页面完成后 ,再对html的dom节点进行一些需要的操作
4、一般在哪个生命周期请求异步数据
我们可以在钩子函数 created、beforeMount、mounted 中进行调用 ,因为在这三个钩子函数中,data 已经创建 ,可以将服务端端返回的数据进行赋值。
推荐在 created 钩子函数 中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
- SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
组件通信方式有些
1、父传后代
props:- 父 通过绑定属性 的方式传递 ;子使用props接收
- 注意点:子不能直接修改父的数据 ,因为prop 使得组件间是单向数据流
$parent:(不常用)- 子通过
this.$parent.xx使用父的数据 - 子可以直接修改父的数据
- 子通过
provide/inject依赖注入:- 祖通过
provide提供变量 ,子孙使用inject引入变量 - 祖 可以直接向后代传值
- 缺点:变量来源不明
- 祖通过
2、后代传父
$emit:- 子 定义自定义事件
this.$emit( 方法名,值),父通过监听子的自定义事件 来接收数据
- 子 定义自定义事件
$refs:(不常用)- 父直接拿子的数据
this.$refs.child.xx - 父可直接修改子的数据
- 父直接拿子的数据
3、平辈间传值
eventBus(事件总线):$emit:发送数据,this.$bus.$emit( 'foo' )$on:接收数据,this.$bus.$on( 'foo', (value) => {} )
vuex/pinia:状态管理库
子组件能直接修改父组件数据吗
- 使用
this.$parent可以 - 使用
prop不可以,因为prop使得父子间形成单向数据流,更新只会向下流到子组件,反之不行,用$emit
路由Vue-Router
1、Vue-Router是干什么的?原理
Vue-Router是路由插件 ,用于构建单页面应用
vue单页面应用基于路由和组件 ,路由用于访问路径 ,并将路径和组件映射起来
路由模块本质 :建立起url和页面之间的映射关系
2、Vue-Router的使用方式
3、路由跳转方法
4、动态路由?如何获取传过来的动态参数
5、Vue-Router的懒加载如何实现
6、路由的hash和history模式的区别
简答
在前端路由中,我们常用Hash和History两种模式。
Hash模式 通过URL的#后面部分实现,兼容性好但URL不够美观;
History模式 利用HTML5 API实现更自然的URL,但需要服务器配合。我们项目选择了History模式,因为现代 浏览器 支持良好,且我们能够配置Nginx将所有路径重定向到index.html,配置
try_files $uri $uri/ /index.html
Vue-Router有两种模式 :hash模式和history模式。默认的路由模式是hash模式
它们都是单页应用 (SPA)实现路由跳转而不刷新页面的解决方案。
两者的主要区别在于:
Hash模式
- 实现原理 :利用
URL中的hash(#)部分,监听hashchange事件 - 特点 :
- URL中会有#符号,如example.com/#/home
- 完全由前端控制,改变hash不会触发页面刷新
- 兼容性好,支持所有浏览器,不需要服务器特殊配置
- 不会将#后面的部分发送到服务器
History模式
- 实现原理 :基于
HTML5 HistoryAPI(pushState/replaceState) - 特点 :
- URL更简洁美观,如example.com/home
- 需要服务器支持,否则直接访问子路由会404
- 需要浏览器支持HTML5 History API
- 对SEO更友好(但需要配合服务器配置)
在实际项目中:
- Hash模式适合 :
- 需要兼容老旧浏览器的项目
- 无法配置服务器的情况
- 快速原型开发
- History模式适合 :
- 现代浏览器项目
- 能控制服务器配置的环境
- 对URL美观度有要求的项目
7、如何获取页面的hash变化
8、$route和$router的区别
9、Vue-rouer路由钩子在生命周期的体现
10、Vue-router的跳转和location.href有什么区别
11、编程式导航传参
Router 的传参方式有哪些?Vue2 和 Vue3 有什么区别?
4 种传参方式:
- params 动态路由:参数作为路径一部分,如 /user/123,SEO 友好,适合资源标识
- query 查询参数:拼接在 URL 后面,如 ?id=123,适合筛选、分页等可分享场景
- props 解耦传参:官方推荐,支持布尔、对象、函数三种模式,让组件与路由解耦,提高复用性
- state 隐式传参:Vue Router 4 新增,数据不显示在 URL,适合临时敏感数据
Vue2 和 Vue3 的主要区别:
- API 层面 :Vue3 使用
useRoute() 和 useRouter()组合式函数,替代 this.$route - 重大变更 :Vue Router 4 移除了隐式 params 传参 ,必须在路由中定义动态参数或改用 query/state
- 新增特性 :支持 history.state 传参,TypeScript 类型推导更完善
场景选择:
- 详情页用 params
- 筛选用 query
- 组件复用用 props
- 复杂数据用 Pinia 管理
12、params和query的区别
- 用法 :
params(pp): 引入{ path:'',params:{id} };接收this.$route.params.idquery(nq): 引入{ name:'',query:{id} };接收this.$route.query.id
- URL地址显示 :
params:类似post方式,不会出现在URL地址上;刷新会丢失里面的数据query: 类似get方式,会将参数拼接到URL地址栏;刷新不会丢数据
13、Vue-router导航守卫有哪些
Vue Router的导航守卫分为三大类:
- 全局守卫 :
beforeEach(最常用):拦截所有路由跳转 ,做登录权限控制(未登录跳登录页,已登录跳首页)beforeResolve:afterEach(常用):- 修改浏览器标签页的标题(document.title)
- 页面埋点(统计用户访问了哪些页面)
- 关闭加载动画
- 路由独享守卫 :
beforeEnter,在路由配置中定义,只对特定路由生效 - 组件内守卫 :
beforeRouteEnter:beforeRouteUpdate:beforeRouteLeave:- 用户输入了表单内容未保存就想离开,提示"您输入的内容尚未保存,确定要离开吗?";
- 清除组件内的定时器
执行顺序是 :离开组件守卫 → 全局前置 → 路由独享 → 组件进入守卫 → 全局解析 → 全局后置
注意点: 1、beforeRouteEnter中不能访问this;2、必须调用next(),否则导航会被阻塞
Vue2和Vue3的主要区别:
Vue3使用Composition API ,通过onBeforeRouteUpdate等函数式API调用
Vue3中next参数变为可选 ,可以通过返回值控制导航
Vue3提供了更好的TypeScript支持 和动态路由管理API(addRoute、removeRoute等)
14、对前路由的理解
15、在组件中监听路由参数变化??
js
//方法一
watch:{
'$route':(to,from){}
}
//方法二
beforeRouteUpdate(to,from,next){}
16、vue-router导航故障
解决方案 :重置VueRouter.prototype.push ,VueRouter.prototype.push.call(this,location).catch(err=>err)
vuex
1、Vuex的原理以及自己的理解
2、vuex有哪几种属性
3、如何在组件中批量使用Vuex的getter属性
4、Vuex中action和mutation的区别
5、为什么Vuex的mutation中不能异步操作?
6、vuex和localStorage的区别
7、Redux和Vuex
8.1、Redux和Vuex有什么区别,它们的共同思想
8.2、为什么要用Vuex或者Redux
9、vuex和单纯的全局对象有什么区别?
10、vuex的严格模式是什么?有什么作用,如何开启
定义: 是一种开发工具,用于确保所有状态变更 都必须通过 mutation 提交,禁止直接修改 store 中的 state
作用:
- 调试方便 :严格模式可以帮助开发者 更容易地发现 那些没有通过 mutation 进行的状态更改,从而避免潜在的bug。
- 保持状态一致性 :通过强制所有状态变更都必须通过 mutation,可以确保状态的变化是有序且可预测的,减少了意外错误的可能性。
- 提高代码质量 :鼓励良好的编程习惯,即所有的状态更新都应该有明确的意图,并且可以通过时间线或日志来跟踪。
开启方法:
只需在创建 Vuex store 的时候,将 strict 选项设置为 true
在开发环境 中启用严格模式以帮助调试 ,而在生产环境中关闭 它以获得更好的性能
javascript
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++;
}
},
// 开启严格模式(在开发环境中启用严格模式以帮助调试,而在生产环境中关闭它以获得更好的性能)
strict: process.env.NODE_ENV !== 'production'
});
export default store;
11、vuex的数据传递流程
答:
当组件进行数据的修改 时,我们需要调用dispatch来触发actions里面的方法 ;
actions 里面的每个方法 中都会有一个commit方法 ,当方法执行的时候会通过commit 来触发mutations 里面的方法进行数据的修改 ;
mutations 里面的每个函数 都会有一个state参数 ,这样就可以在mutations里 面进行state的数据的修改 ;
当数据修改完 毕后,会传导给页面 ,页面的数据也会发生改变
12、vuex在什么阶段挂载上去的
vuex、vue-router都是在beforeCreate阶段挂载上去的
vue3
1、Vue3.0有什么更新
2、Vue3.0中的VueComponentAPI
虚拟DOM
1、对虚拟DOM的理解?
虚拟 DOM 是一种编程概念 ,它不是真实存在于内存中的 DOM 树,而是一个轻量级的 JavaScript 对象 ,它通过不同属性(tag、props、children)去描述一个视图结构。
javascript
{
tag:'div',
props:{
id:'xx'
},
children:[]
}
2、虚拟DOM的解析过程
1、数据变化 :当你的 Vue 应用中的数据发生变化 时,Vue 会触发更新过程 。
2、虚拟 DOM 的更新 :Vue 使用新的数据创建一个新的虚拟 DOM 树 。这个新的树是基于当前的组件状态和数据。
3、比较差异 :Vue 使用高效的算法(如 Diff 算法)来比较 新旧两个虚拟 DOM 树 ,找出它们之间的差异 。
4、最小化更新 :一旦找出差异,Vue 会计算出最少的操作 来将这些变化应用到真实的 DOM 上,以实现视图的更新。这通常涉及添加、删除或修改实际的 DOM 节点。
5、DOM 更新 :最后,这些变化被应用到真实的 DOM 上 ,用户界面得到更新。
3、为什么要用虚拟DOM
也就是引入虚拟DOM好处:
- 将真实元素节点抽象成vNode,有效减少直接操作dom次数,提高性能
- 实现跨平台
- 无需手动操作DOM
4、虚拟DOM真的比真实DOM性能好吗
- 首次渲染大量DOM 时,由于多了一层虚拟DOM的计算 ,会比innerHTML插入慢。
- 正如它能保证性能下限 ,在真实DOM操作的时候进行针对性的优化时,还是更快的
5、DiFF算法的原理
在新老虚拟DOM对比时:
- 首先,对比节点本身,判断是否为同一节点
- 如果不为相同节点 ,则删除该节点重新创建节点进行替换
- 如果为相同节点 ,进行
patchVnode
- patchVnode判断如何 对该节点的子节点进行处理
- 先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点 ,则进行
updateChildren
- updateChildren判断如何 对这些新老节点的子节点进行操作 (diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较 ,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
6、vue中key的作用
Vue中的key属性主要用于在列表渲染(v-for)中为每个节点提供唯一标识,以优化虚拟DOM的diff算法,确保高效更新和避免状态错乱。
- 提高虚拟DOM的性能:通过唯一标识,快速定位和减少重绘。
- 强制组件重新渲染:通过改变key的值,重新初始化组件。
- 维持元素的状态:确保元素在更新时保持其状态,防止状态丢失
7、为什么不建议用index作为key
使用index 作为 key和没写基本上没区别 ,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。