关于Vue很全的一篇知识点和面试总结,面试前可以学习一遍!
1. 什么是MVVM及实现原理?
MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
2. Vue2.x和Vue3.x响应式数据原理
Vue2.x在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher)如果属性发生变化会通知相关依赖进行更新操作(发布订阅)。
Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
3. vue2.x中如何监测数组变化
使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
4. nextTick是什么,实现原理是什么?
在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用:
Promise
MutationObserver
setImmediate
如果以上都不行则采用setTimeout
定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
解决的问题:有些时候在改变数据后立即要对dom进行操作,此时获取到的dom仍是获取到的是数据刷新前的dom,无法满足需要,这个时候就用到了$nextTick。
5. Vue的生命周期
1、beforeCreate是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。
2、created在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。
3、beforeMounted发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。
4、mounted在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到DOM节点,使用ref属性对Dom进行操作。
5、beforeUpdate发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
6、updated发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
7、beforeDestroy发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
8、destroyed发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
6. Computed和Watch的区别
Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。
Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销哦。
7. v-if和v-show的区别
当条件不成立时,v-if不会渲染DOM元素,v-show操作的是样式(display),切换当前DOM的显示和隐藏。
8. 组件中的data为什么是一个函数?
一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。
9. v-model的原理
v-model本质就是一个语法糖,可以看成是value + input方法的语法糖。可以通过model属性的prop和event属性来进行自定义。原生的v-model,会根据标签的不同生成不同的事件和属性。
10. Vue事件绑定原理
原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的。
11. Vue模版编译
Vue的编译过程就是将template转化为render函数的过程
12. 虚拟Dom以及key属性的作用
由于在浏览器中操作DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。这就是虚拟Dom的产生原因。
Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点。是对真实DOM的一层抽象。
key的作用是尽可能的复用 DOM 元素。
新旧 children 中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的。
需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key也就是children中节点的唯一标识。
13. keep-alive作用
keep-alive可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
常用的两个属性include/exclude,允许组件有条件的进行缓存。
两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。
1.activated:当组件激活时,钩子触发的顺序是created->mounted->activated
2.deactivated: 组件停用时会触发deactivated,当再次前进或者后退的时候只触发activated
14. Vue中组件生命周期调用顺序
组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
组件的销毁操作是先父后子,销毁完成的顺序是先子后父
加载渲染过程:父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
15. Vue2.x组件通信有哪些方式
父子组件通信:
父->子props,子->父 o n 、 on、 on、emit
获取父子组件实例 p a r e n t 、 parent、 parent、children
Ref 获取实例的方式调用组件的属性或者方法
兄弟组件通信:
Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue
跨级组件通信:
Vuex
a t t r s 、 attrs、 attrs、listeners
16. SSR了解
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。
SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子
17. Vue的性能优化
一、编码阶段
1、尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
2、v-if和v-for不能连用
3、SPA 页面采用keep-alive缓存组件
4、在更多的情况下,使用v-if替代v-show
5、key保证唯一
6、使用路由懒加载、异步组件
7、第三方模块按需导入
8、图片懒加载
9、防抖、节流
10、长列表滚动到可视区域动态加载
二、SEO优化
1、预渲染
2、服务端渲染SSR
三、打包优化
1、压缩代码
2、使用cdn加载第三方模块
3、splitChunks抽离公共文件
4、sourceMap优化
5、多线程打包happypack
四、用户体验
1、PWA
2、客户端缓存、服务端缓存
18.hash路由和history路由实现原理
vue-router 有两种模式,hash 模式和 history 模式
- hash 模式
1、url 中带有#的便是 hash 模式,#后面是 hash 值,它的变化会触发 hashchange 这个事件。通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听 hashchange 来实现更新页面部分内容的操。
2、另外,hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。 - history 模式
history api 可以分为两大部分,切换和修改
1、切换历史状态
包括 back,forward,go 三个方法,对应浏览器的前进,后退,跳转操作
2、修改历史状态
包括了 pushState,replaceState 两个方法,这两个方法接收三个参数:stateObj,title,url - history 缺点:
1、hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如http://www.a12c.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
2、history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致。如http://www.a12c.com/book/a。如果后端缺少对/book/a 的路由处理,将返回 404 错误
19.vue动态设置img的src不生效的问题
<img class="logo" :src="logo" alt="公司logo">
data() {
return {
logo:require("./../assets/images/logo.png"),
};
}
因为动态添加src被当做静态资源处理了,没有进行编译,所以要加上require
20.vue中怎么重置data
Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };
var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。
注意,具有相同属性的对象,同名属性,后边的会覆盖前边的。
由于Object.assign()有上述特性,所以我们在Vue中可以这样使用:
Vue组件可能会有这样的需求:在某种情况下,需要重置Vue组件的data数据。此时,我们可以通过this.
d
a
t
a
获
取
当
前
状
态
下
的
d
a
t
a
,
通
过
t
h
i
s
.
data获取当前状态下的data,通过this.
data获取当前状态下的data,通过this.options.data()获取该组件初始状态下的data。
然后只要使用**Object.assign(this.data,this.options.data())**就可以将当前状态的data重置为初始状态。
21.vue实现强制刷新组件
① v-if ② this.$forceUpdate
v-if
当v-if的值发生变化时,组件都会被重新渲染一遍。因此,利用v-if指令的特性,可以达到强制
<comp v-if="update"></comp>
<button @click="reload()">刷新comp组件</button>
data() {
return {
update: true
}
},
methods: {
reload() {
// 移除组件
this.update = false
// 在组件移除后,重新渲染组件
// this.$nextTick可实现在DOM 状态更新后,执行传入的方法。
this.$nextTick(() => {
this.update = true
})
}
}
this.$forceUpdate
<button @click="reload()">刷新当前组件</button>
methods: {
reload() {
this.$forceUpdate()
}
}
22.对proxy的理解
Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。
23.缓存当前的组件?缓存后怎么更新?
<!-- 这里是需要keepalive的 -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 这里不会被keepalive -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
{
path: '',
component: ,
meta: {keepAlive: true} // 这个是需要keepalive的
},
{
path: '',
component: ,
meta: {keepAlive: false} // 这是不会被keepalive的
}
如果缓存的组件想要清空数据或者执行初始化方法,在加载组件的时候调用activated钩子函数,如下:
activated: function () {
//加入页面初始化逻辑
}
24.axios解决跨域的问题
使用axios直接进行跨域访问不可行,我们需要配置代理
代理可以解决的原因:
因为客户端请求服务端的数据是存在跨域问题的,而服务器和服务器之间可以相互请求数据,是没有跨域的概念(如果服务器没有设置禁止跨域的权限问题),也就是说,我们可以配置一个代理的服务器可以请求另一个服务器中的数据,然后把请求出来的数据返回到我们的代理服务器中,代理服务器再返回数据给我们的客户端,这样我们就可以实现跨域访问数据
- 配置BaseUrl
import axios from 'axios'
Vue.prototype.$axios = axios
axios.defaults.baseURL = '/api' //关键代码
-
配置代理
在config文件夹下的index.js文件中的proxyTable字段中,作如下处理
proxyTable: {
'/api': {
target:'http://api.douban.com/v2', // 你请求的第三方接口
changeOrigin:true,
// 在本地会创建一个虚拟服务端,然后发送请求的数据,并同时接收请求的数据,
//这样服务端和服务端进行数据的交互就不会有跨域问题
pathRewrite:{ // 路径重写,
'^/api': ''
// 替换target中的请求地址,也就是说以后你在请求http://api.douban.com/v2/XXXXX
//这个地址的时候直接写成/api即可。
}
}
- 在具体使用axios的地方,修改url如下即可
axios.get("/movie/top250").then((res) => {
res = res.data
if (res.errno === ERR_OK) {
this.themeList=res.data;
}
}).catch((error) => {
console.warn(error)
})
原理:
因为我们给url加上了前缀/api,我们访问/movie/top250就当于访问了:localhost:8080/api/movie/top250(其中localhost:8080是默认的IP和端口)。
在index.js中的proxyTable中拦截了/api,并把/api及其前面的所有替换成了target中的内容,因此实际访问Url是http://api.douban.com/v2/movie/top250。
至此,纯前端配置代理解决axios跨域得到解决。
25.v-for循环时为什么要加key
key的作用主要是为了高效的更新虚拟DOM,是因为Virtual DOM 使用Diff算法实现的原因。
当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则。
比如一下这个情况
我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的:
即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?
所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
26.常用的路由导航钩子
一、全局导航守卫
- 前置守卫
router.beforeEach((to, from, next) => {
// do someting
});
- 后置钩子(没有 next 参数)
router.afterEach((to, from) => {
// do someting
});
二、路由独享守卫
cont router = new VueRouter({
routes: [
{
path: '/file',
component: File,
beforeEnter: (to, from ,next) => {
// do someting
}
}
]
});
三、组件内的导航钩子
组件内的导航钩子主要有这三种:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。他们是直接在路由组件内部直接进行定义的
- beforeRouteEnter
data(){
return{
pro:'产品'
}
},
beforeRouteEnter:(to,from,next)=>{
console.log(to)
next(vm => {
console.log(vm.pro)
})
}
注:beforeRouteEnter 不能获取组件实例 this,因为当守卫执行前,组件实例被没有被创建出来,我们可以通过给 next 传入一个回调来访问组件实例。在导航被确认时,会执行这个回调,这时就可以访问组件实例了
仅仅是 beforRouteEnter 支持给 next 传递回调,其他两个并不支持,因为剩下两个钩子可以正常获取组件实例 this
27.路由传参
params 和 query
- params
//传参
this.$router.push({
name:"detail",
params:{
name:'xiaoming',
}
});
//接受
this.$route.params.name
- query
//传参
this.$router.push({
path:'/detail',
query:{
name:"xiaoming"
}
})
//接收参数是this.$route
this.$route.query.id
- query 和 params 区别
1、params 只能用 name 来引入路由,query 既可以用 name 又可以用 path(通常用 path)
2、params 类似于 post 方法,参数不会再地址栏中显示