@[toc]
前端面试之Vue篇
前沿知识:
关于MVVM,MVVM之间的通信都是双向的,主要是负责数据模型的操作,不用过多的操作dom,它是种架构模式的概念,按照这种模式设计的框架都叫MVVM框架, 它主要有三个部分,M:数据模型、V:视图模型、VM:视图模型而
vue就是个mvvm架构模式的渐进式框架 最核心的有两个点: 1.数据驱动,也叫双向数据绑定,主要是负责数据模型的操作,不用过多地操作dom,减少dom的操作也意味着更高的性能。 2.组件系统提高开发效率、方便重复使用、简化调试步骤、提升整个项目的可维护性
vue组件的核心选项
1.模板(template):模板声明了数据和最终展现给用户的dom之间的映射关系。
2.初始数据(data):一个组件的初始数据状态,对于可复用的组件来说,这通常是私有的状态
3.接受的外部参数(props):组件之间通过参数来进行数据的传递和共享
4.方法(methods):对数据的改动操作一般都在组件的方法进行
5.生命周期钩子函数(lifecycle hooks),一个组件会触发多个生命周期钩子函数
6.私有资源(assets),vue.js当中将用户自定义的指令、过滤器、组件等统称为资源。一个组件可以声明自己的私有资源,私有资源只有该组件和他的子组件可以调用。
对于vue是一套构建用户界面的渐进式框架的理解 渐进式代表的含义是:没有多做职责之外的事 vue.js只提供了vue-cli生态中最核心的组件系统和双向的数据绑定 像vuex、vuer-outer都属于围绕vue,js开发的库
vue组件的通信方式有哪几种?
1.父传子:props 父组件通过props向下传递数据给子组件
props: {
desc: {
type: String,
default: ""
}
},
2.子传父:$emit 子组件通过$emit发送消息,父组件通过v-on绑定函数,接收数据
this.$emit('getData',1)
<detail @getData='getDataList'></detail>
getDataList(ms){
console.log(ms)
}
vue如何操作dom节点?
vue是鼓励开发人员按照“数据驱动”的思路去开发项目,但是有时候也不可避免会遇到操作dom的特殊情况,以下就是vue操作dom节点的方式
- ref和$refs
ref是用来给子组件和元素注册引用信息的,引用信息会注册到父对象的refs持有所有注册过ref所有子组件的对象
vue当数据更新是如何更新dom节点的?
真实dom的渲染会引起整个dom树的重排重绘,会造成非常大的开销,因此vue采用virtual dom(即虚拟dom)进行局部更新。
- 虚拟dom是将真实dom数据抽取出来,以对象的形式模拟树形的结构
- 在更新节点的过程中采用
diff算法,diff的过程就是调用patch函数,比较新旧节点,一边对比一边给真实dom打补丁
how to进行diff比较?
- 采用diff算法比较新旧节点时,比较只会在同层级进行,不会跨层级比较
- diff算法在执行时有三个维度:Tree Diff 、Component Diff、Element Diff
详解vue的diff算法:juejin.cn/post/684490… 老哥的这篇文章写的很详细,推荐大家看
Vue组件中的data为什么必须是函数?
<script>
export default {
data() {
//返回一个唯一的对象,不要和其他的组件共有一个对象进行返回
return {
msg:'hello'
}}}
因为一个组件是可以共享的,一个改变,其他的也改变,假如我们data是一个对象的话,很容易会污染变量,造成数据的紊乱(因为是全局对象),但假如是函数的形式返回一个对象,那么我们每次使用该组件的时候,返回的对象地址的指向都是不一样的,(函数=块级作用域),这样就能让各个组件数据独立。
Vue等单页面应用及其优缺点
优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。 缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。 //// 优点
- 良好的交互体验
- 良好的前后端工作分离模式
- 减轻服务端的压力 缺点
- seo难度较高 - 前进、后退管理
- 初次加载耗时多
说一下 Vue 的双向绑定数据的原理?
vue实现双向数据绑定的原理是:采用数据劫持“发布者-订阅者”的模式,这种模式是通过Object.defineProperty()来劫持各个属性的setter、getter,然后在数据变动的时候发布给订阅者,触发相应的监听回调。 简单地理解:当data有变化的时候它通过Object.defineProperty()方法中的set方法进行监控,并且调用在此之前已经定义好data和view的关系的回调函数,来通知view进行数据的改变,而view发生改变则是通过底层的input事件来进行data的响应更改。
vue是通过Object.defineProperty()来实现数据劫持的。实现方式(发布者-订阅者模式):Observer(Object.defineProperty中的set)监听data的变化,当data有变化的时候通知观察者列表Dep(里面有与data变化对应的update函数),watcher负责向观察者列表添加(订阅)对应的更新函数,Dep里面的更新函数执行完了之后再将最新的值更新到view上面。 具体步骤: (1)需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter。 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。 (2)compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。 (3)Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。 (4)MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
vue的底层用object.definedPropety( )来实现它有什么弊端或局限性?
弊端: 1.不能监听对象属性的新增和删除,只对获取和更改进行了处理 2.通过索引更改数组对象不能被监听,如arr[1]=9 3.Object.defineProperty不能再get或set中使用该对象,因为使用该对象,就会触发get方法,会进入死循环,需要对被订阅的对象进行拷贝,使用拷贝的对象 4.需要遍历对象当中的各个属性,对其使用Object.defineProperty进行set/get的改写
目前,解决第一个和第二个问题的方式是通过
vue.set(target,key,value)来实现 在使用proxy时,创建代理对象。使用target和key,传入需要实现响应式的对象和属性,通过代理实现了目标对象的响应式。
解释单向数据流和双向数据绑定?
单向数据流:顾名思义,数据流是单向的,数据的流动方向可以跟踪,流动单一,追查问题的时候可以更加快捷。
缺点就是写起来不太方便。要使UI发生变更就必须创建各种action来维护相对应的state。 双向数据绑定:数据之间是相同的,将数据变更的操作隐藏在框架的内部。优点是在表单交互较多的场景下,会简化大量与业务无关的代码,缺点就是无法追踪局部状态的变化,增加了出错时debug的难度。
Vue 如何去除 URL 中的#?
vue-router默认使用
hash模式,所以在路由加载的时候,项目中的url会自带“#”。如果不想使用“#”,可以使用vue-router的另一种模式 history:
newRouter({
mode:'history',
routes:[]
})
需要注意的是,当我们启用
history模式的时候,由于我们的项目是一个单页面的应用,所以在路由跳转的时候,就会出现访问不到静态资源出现“404”的情况,这时候就需要服务端增加一个覆盖所有情况的候选资源:如果url匹配不到任何静态资源,则应该返回同一个“index.html”页面。(由后端处理)
对 MVC、MVVM 的理解?
MVC:
特点:
- View传送指令到Controller
- Controller完成业务逻辑后,要求Model改变状态
- Model将新的数据发送到View,用户得到反馈
所有的通信都是单向的
MVVM:
MVVM分为Model、View、ViewModel三者 Model:代表数据模型,数据和业务逻辑都要在Model中定义 View:代表UI视图,负责数据的展示 ViewModel:就是界面View对应的Model,因为数据库结构往往是不能直接跟界面控件一一对应上,所以,需要再定义一个数据对象专门对应view上的控件,而ViewModel的职责就是把model对象封装成可以显示和接受输入的界面数据对象
特点: 1.各部分之间的通信,都是双向的 2.采用双向绑定:View的变动,自然都反应在ViewModel,反之也一样
延伸问题:ul中有一个li列表,它是怎么和我们的数据一一对应上的呢?(此题考查ViewModel)Model是通过ViewModel来联系的,Model和ViewModel之间是有着双向数据绑定的联系,View和ViewModel也是,因此当Model中的数据改变时会触发View层的数据更新,View中由于用户的交互操作而改变的数据也会在Model数据模型当中同步。
Vue 生命周期的理解?
共有八个,创建前,创建后,挂载前,挂载后,更新前,更新后,销毁前,销毁后。
beforeCreate():在实例创建之前执行,数据是未加载的状态created():在实例创建,数据加载后,能初始化数据,在DOM渲染之前执行beforeMounte():虚拟DOM已创建完成,在数据渲染前最后一次更新数据mounted():页面、数据渲染完成,真实的DOM挂载完成beforeUpdate():重新渲染之前触发Updated():数据已经更改完成,DOM也重新render完成,更改数据会陷入一个死循环。beforeDestory():销毁前执行(这时实例仍然可以使用)destoryed():销毁后执行
什么是虚拟的dom?
Vue通过建立一个虚拟的dom数对真实的dom发生的变化保持追踪
一棵真实的dom树的渲染需要先解析css样式和dom树,然后将其整合成一棵渲染树,再通过布局算法去计算每个节点在浏览器中的位置,最终输出到显示器上面。
而我们所提及的虚拟的dom 则可以理解为
保存了一棵dom树被渲染之前所包含的所有信息,而这些信息可以通过对象的形式一直保存在内存当中,并通过js的操作来进行维护
请解释下虚拟dom的原理?
背景:在网页浏览器资源开销最大的便是dom节点了,dom很慢而且也非常地庞大,
网页的性能问题也大多数是由于js修改dom所引起的,我们使用js来操作dom,操作的效率往往是非常低的由于dom被表示为树的结构,每次dom中的某些内容都会发生变化,引起对dom的更改非常的快,但是由于更改后的元素,他的子项,必须经过reflow/layout阶段,然后浏览器必须重新绘制更改,这很慢的。因此,回流/重绘的次数越多,应用程序就会越卡顿。但是js运行的速度很快, 虚拟dom是放在js和html中间的一个层,他可以通过新旧dom的对比,来获取对比后的差异对象,然后有针对性地把差异部分真正地渲染到页面上,从而减少实际dom的操作,最终达到了性能优化的目的。虚拟的dom是根据dom树映射出来的,里面加了diff算法,通过对比新旧dom,可以计算dom的最少改变值,最后通过虚拟的dom来修改真实的dom简单地概括有以下三点: 1、用js模拟dom树,并渲染这个dom树 2、比较新老的dom树,饿到比较的差异对象 3、把差异对象应用到渲染的dom树里面
v-show和v-if的区别?
v-show的值无论是true还是false,他的元素都是存在于html当中,只是设置了style中的display的值,为none 或者block 而v-if只有为true的时候,元素才会显示在html当中
如何让css只在当前组件中起作用?
只需要在当前组件的tyle当中写入 scoped
css <style scoped> </style>
router 的区别?路由跳转有哪些形式?
router是vueRouter的实例,相当于一个全局的路由对象,包括路由的跳转对象和钩子函数。 route是“路由的信息对象”,包括path,params,name,query等 路由跳转的形式有哪些? 1.router-link(声明式路由)
不带参数:<router-link :to=''{path:'/home'}'> //name path都行,建议用name带参数:<router-link :to='{name:'home',params:{id:1}}'> 2.router.push(编程式路由)字符串:router.push('home')对象:router.push({path:'home'})命名的路由:router.push({name:"user',params:{id:0}})带查询参数 变成/register?paln=priverouter.push({path:'register',query:{plan:'prive'}}) query和params区别? 比较直观的区别: 1.query类似于get,跳转后页面url后面会拼接参数,类似?id=1,非重要的可以这样子传,密码之类的还是用params刷新页面id还在 2.params类似post跳转之后页面url后面不会拼接参数,但是刷新页面id会消失 其他区别: 3.query传参配置的是path,而params传参配置的是name,在params配置path无效 4.query传递的参数会显示在地址栏 5.params传参刷新会无效,但是query会保存传递过来的值,刷新不变
讲下vue-router 路由你的了解有多少?
路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。 前端路由的
核心就在于改变视图的同时不会向后端发出请求。hash和historyhash模式: 这里的hash就是指url尾巴后的#号以及后面的字符。这里的#和css里的#是一个意思。hash也称为锚点,本身是用来做页面定位的,她可以使对应的id的元素显示在可视区域内。由于hash值变化不会导致浏览器向服务器发出请求,而且hash改变会触发hashchange事件,浏览器的进后退也能对齐进行控制,所以人们在html5的history出现前,基本都是使用hash来实现前端路由的。 history模式 已经有hash模式了,而且hash能兼容到ie8,history只能兼容到IE10,为什么还要搞个history呢? 首先,hash本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。其次,hash的传参是基于url,如果要传递复杂的数据,会有体积的限制,而history模式不仅可以在url里面放参数,还可以将数据存放在特定的对象里面。 路由配置主要有四步: 1.引入路由插件 2.创建路由配置 3.使用路由配置创建路由对象实例 4.挂载到根实例
<script src='./js/vue.min.js'></script>
//需要在引入vue之后再去引入路由库
//1.引入库
<script src='./js/vue-router.min.js'></script>
<script>
//2.创建路由配置
//里面的应该是路由对象,每个对象都是一个路由
//路由最重要的是这个配置
var routes=[]
//3.使用上面的配置创建路由实例对象
var router=new VueRouter({routes})
//4.挂载到根实例
var app=new Vue({el:'#app',router})
</script>
//其他引入方式
import Vue from 'vue'
import VueRouter from 'vue-router'
不用路由也可以实现组件的切换 需求:点击按钮,切换页面
- 编写组件:
- data数据配合v-if判断数据决定组件是否显示
- 创建改变data的方法 - 使用按钮控制data数据 缺点:
- 繁琐,每多一个组件就多一个v-if
- 不友好,没有状态保存,刷新后会被重置
讲下路由守卫是怎样的?
一、全局守卫 1.router.beforeEach((to,from,next)=>{}) 回调函数里面的参数,to:进入到哪个路由去,from:从哪个路由离开,next:函数,决定是否展示你要看到的路由页面 业务场景:检验是否登录了,没登录就跳转到登录页面不然他在其他页面逗留,现在一般都是用请求的全局拦截来进行实现
router.beforeEach((to,from,next)=>{
if(to.path == '/login' || to.path == '/register'){
next();
}else{
alert('请先登录,憨憨儿');
next('/login');
}
})
2.router.afterEach((to,from)=>{})(少用) 这个钩子不会接受next函数,也不会改变导航本身 二、路由独享的守卫 1.beforeEnter、beforeLeave 用法和全局守卫是类似的,但是只能作用于单个路由里面
//登录模块
path:'login',
component:()=>import('/views/login'),
beforeEnter:(to,from,next)=>{
if(to.meta.needLogin $$ !$store.state.isLogin)
next({path:'/login'}
)}}
}
else{
next()
}
三、组件内的守卫 可以在路由组件内直接定义一下的路由导航守卫 1.beforeRouteEnter
- 在渲染该组件的对应路由被confirm前调用
- 不能!获取组件实例this,因为当守卫执行之前,组件实例还没被创建
- 可以通过next获取data中的数据
data(){
return {
name:"jack"}
}
beforeRouteEnter:(to,from,next)=>{
next(vm=>{
alert("hi"+vm.name)})
}
2.beforeRouteUpdate 在原先的版本中,如果一个在两个子路由之间跳转,是不触发beforeRouteLeave的,这回导致某些重置的操作,没地方触发,在这之前,我们都是用watch,但是通过这个钩子,我们有了更好的方式
beforeRouteUpdate(to,from,next){
//在当前路由改变,但是该组件被复用时被调用
//举例来说,对于一个带有动态参数的路径 /doo/:id,在/doo/1和/doo/4之间跳转的时候
//由于会渲染同样的doo组件,因此组件实例会被复用,而这个钩子就会在这个情况下被调用
//可以访问组件实例‘this’
}
3.beforeRouteLeave 这个离开的守卫通常用来禁止用户在还未保存修改前突然离开,该导航可以通过next(false)来取消
- 可以访问组件实例this
beforeRouteLeave: (to, from, next) => {
if (confirm("你确定要离开吗") == true) {
next();
} else {
next(false);
}
},
返回上一级路由 1.history.back() 2.this.$router.go(-1) 关于to,from,next,next(false)
vue-router响应路由参数的变化
问题:当使用路由参数时,例如从/page?id=1到/page?id=2,此时原来的组件实例会被复用,意味着组件的生命周期钩子不会再被调用,此时vue应该如何响应路由的参数变化? 答案:使用watch(监听变化)$route对象
Vue中key的值
key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。 可参考官方文档:cn.vuejs.org/v2/api/#key 简单来讲: vue中的key作用是
高效地更新虚拟的dom,就是管理可复用的元素。当vue.js用v-for遍历已渲染过的元素列表时,它默认用“就地复用的策略”,会复用已有的元素而不是从头开始渲染,这样会使vue变得非常快
组件间的参数传递
父组件与子组件的传值父组件传给子组件:子组件通过props方法进行接受数据 子组件传给父组件:子组件通过$emit的方式进行传递兄弟组件之间的传值一般会创建一个中间站,就是兄弟子组件,都把数据传递给父组件,即先是子组件传递给父组件,再通过父组件传递给另一个子组件的方式- 非兄弟、非父子之间的组件传值如下:
/*新建一个Vue实例作为中央事件总嫌*/
let event = new Vue();
/*监听事件*/
event.$on('eventName', (val) => {
//......do something
});
/*触发事件*/
vent.$emit('eventName', 'this is a message.')
NextTick 是做什么的?
$nextTick是在下次dom更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick,则可以在回调当中获取更新后的DOM。 举个栗子: 比如需要在一个dom上面挂载或者监听一个内容时候,往往会出错。比如使用v-if来进行dom渲染,由于dom还未渲染,会直接导致挂载失败。这也是Vue不建议直接操作dom的原因。 或者是通过data里的数据来重新刷新一些插件的值,由于数据是异步刷新的,可能会导致传输到插件的值还是原值,导致插件刷新失败。 这里Vue提供一个Vue.nextTick(callback)的方法,这样可以在dom都渲染完毕后再执行我们相关的业务代码。 什么时候需要用Vue.nextTick():
- 你在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。原因是什么呢,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。
- 在数据变化后要执行的某个操作,当你设置 vm.someData = 'new value',DOM并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。
Vuex是什么?怎么使用?哪种功能场景使用它?
vuex是vue框架中的状态管理。 只用来读取的状态集中放在store中; 改变状态的方式是提交mutations,这是个同步的事物; 异步逻辑应该封装在action中。 在main.js引入store,注入。新建了一个目录store,….. export 。 场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车
state Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。 mutations mutations定义的方法动态修改Vuex 的 store 中的状态或数据,view层通过store.commit来动态改变数据。 getters 类似vue的计算属性,主要用来过滤一些数据。 action actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。
对keep-alive 的了解?
keep-alive是vue内置的一个组件,主要用于
保留组件状态或避免重新渲染。 应用场景: 有一个列表页面和一个详情页面,那么用户就会经常执行这样的操作详情=>返回列表=>打开详情这样的操作的话,列表和详情都是一个频率很高的页面,那么就可以对列表组件使用进行缓存,这样用户每次返回列表的时候,都能`从缓存中快速渲染,而不是重新渲染
<!-- 基本 -->
<keep-alive>
<component :is="view"></component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
具体参考官方文档API:cn.vuejs.org/v2/api/#kee…
v-for与v-if的优先级
当他们处于同一个节点时,v-for的优先级比v-if的更高,这意味着v-if将分别重复运行与每个v-for的循环当中。当你想仅为一些项渲染节点时,这种优先级的机制会十分地有用。 ⽽如果你的⽬的是有条件地跳过循环的执⾏,那么可以将 v-if 置于外层元 素 (或