本文已参与「新人创作礼」活动,一起开启掘金创作之路。
引言
本文对 Vue 框架中一些重要的特性、框架的原理以问题的形式进行整理汇总。本文章节结构以从易到难进行组织,建议读者按章节顺序进行阅读。希望读者读完本文,有一定的启发思考,也能对自己的 Vue 掌握程度有一定的认识,对缺漏之处进行弥补,对 Vue 有更好的掌握。
笔者也会站在面试者的角度对下述问题进行回答,并加以适当的分析。
1.为什么 data 是一个函数
组件中的 data 写成一个函数,主要是为了数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
如果单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个全局变量的污染。
2.怎样理解 Vue 的单向数据流?
所有的 prop 都使得其父子 prop 之间形成了一个单向数据流:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样主要是为了防止从子组件意外改变父级组件的状态,从而导致应用的数据流向难以理解。
当每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,再由父组件修改。
3. v-if和v-for哪个优先级更高?
在vue2中,通过源码可以看出v-for的优先级是高于v-if,因此如果v-for和v-if放置同个标签内,便会在每次玄幻的过程中进行判断,增加内存开销,因此一般的做法是使用计算属性computed进行数据筛选,之后将筛选后的数据再进行绑定。
vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,便会导致异常。
实际开发中永远不要把 v-if
和 v-for
同时用在同一个元素上
4.computed 和 watch 的区别和运用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值,不然会获取缓存值,节约性能;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
- 当多个值的改变可能会影响到一个值得改变的场合,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算; 例如:当前人员数据库中有出生日期和入职日期,但是因为年龄和工龄是动态的,所以不适合做数据库存储,这个时候就可以用到 computed 进行缓存。
- 当一个值得改变可能会影响到多个值的变化的场合,应该使用 watch, 例如:子组件需要监听父组件传递过来的某个值,做子组件自身数据的改变,就可以用到 watch 进行监听。
5.谈谈你对生命周期的理解
2.X版本的生命周期中总共有四个阶段细分为八个钩子函数
其中四个阶段分别是创建阶段,挂载阶段,更新阶段和销毁阶段
八个钩子函数分别是beforeCreated() ,Created(),beforeMount(),mounted(),beforeUpdate(),update(),beforeDestroy(),destroyed()
按照官网生命周期得图示来分析的话,VUE执行上到下的执行顺序依次为
-
首先会创建一个Vue的实例对象
-
当执行beforeCreated钩子函数,在这个生命周期被执行的时候,只可以调用this上面的一些原生属性与默认事件,其他东西都还未创建。
-
当执行created钩子函数,实例完成了创建,可以调用data中的数据了也可以调用methods中的方法,在此阶段一般会配合axios进行后端接口数据的访问,进行数据的加工。
-
Vue开始编译模板,会通过h函数将模板中的标签解析为内存模板字符串,之后把这个模板字符串通过patch方法渲染为虚拟DOM,此时只是在内存中渲染好了模板,并没有吧模板挂载到真正的页面中去
-
当执行beforeMount()钩子函数,调用此函数的时候模板已经在内存之中编译好了,但是尚未挂载到页面中去,因此页面还是旧的。
-
将虚拟DOM更新到浏览器的页面当中。
-
当执行mounted()钩子函数,此阶段一般会进行DOM节点得操作。
-
进行更新阶段的话,会通过模板中存在的data的改变而进行触发
-
当执行beforeUpdate()钩子函数,页面中显示的数据还是旧的,但是data中的数据是最新的,页面尚未与data中的数据保持同步,可以进一步更改状态,不会触发重复渲染问题
-
先根据data中最新的数据,渲染出一份最新的内存DOM,当最新的内存DOM被更新了之后,会把最新的内存DOM重新渲染到最新的页面中去。
-
当执行update()钩子函数,页面和data数据已经保持同步了,全都是最新的,避免更改状态,不然会重复触发渲染问题。
-
当执行beforeDestroy()钩子函数,我们可以在这时进行善后收尾工作,比如清除计时器
-
当执行destroyed()的时候,实例已经完全销毁了,此时所有的data和所有的methods,过滤器,指令等都不可使用了,所有的子实例也均会被销毁。
6.谈谈你对Vuex的理解
Vuex是实现组件全局状态管理的一种机制,可以方便的实现组件之间数据的共享。
其中共有五大属性分别是,actions,state,getters,mutations,modules
state: 相当于是存储库,里面会对数据进行缓存
getters: 其实就是相当于vue里面的计算属性,可以通过getters访问store中的内存数据,通过this.$store.getters访问
actions: 是进行异步操作的,通过this.$store.dispatch访问
mutations: 是进行同步操作的,通过this.$store.commit访问
modules: 就是进行模块化,这样会使代码更加整洁
在项目中通常通过axios返回的后端数据使this.$store.commit 往state里面存值,之后通过
this.$store.getters 取值
弊端:因为vuex相当于是本地缓存,因此当页面强制F5刷新的时候会有数据丢失的现象,因此一般配合localStorage做成持久化存储或者使用sessionStorage来进行浏览器存储解决,但是localStorage存储过多数据会导致页面变卡,因为localStorage的本质是对字符串的读取,具体使用方案还是需要根据具体项目情况具体分析。
7.谈谈你对虚拟DOM的理解
由于在浏览器中操作 DOM 是很昂贵的,如果直接使用真实DOM的话,会对性能造成比较大的浪费,因此引入了虚拟DOM这个概念,使用虚拟DOM的话,可以对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的。
VUE底层对虚拟DOM的处理其实是借鉴了开源库 snabbdom 进行实现的,具体的就是使用一个函数将template模板里面的一些标签转化为js模板字符串形式的对象,之后再将新旧虚拟dom进行比对,从而达到优化性能的目的。
优点:
- 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,vue会根据虚拟 DOM 和 数据双向绑定原理,帮我们以可预期的方式更新视图,极大提高我们的开发效率。
- 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作。
缺点:
- 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
- 页面初始化 DOM 的时候,由于多了一层虚拟 DOM 的计算,所以初始显示会慢一些。
如果对虚拟DOM想要深入理解的话,可以查看本文作者写的另一篇详解虚拟 DOM 的文章【Vue深入】之虚拟DOM - 掘金 (juejin.cn)
8.谈谈你对Diff算法的理解
vue底层是采用diff算法来进行新旧虚拟dom对比的,diff算法当中最主要的就是一个patch方法。
- sameVnode方法来判断是否为同一类型节点,如果两个节点都是一样的,那么就深入检查他们的子节点,如果两个节点不一样那就说明新节点完全被改变了,就可以直接替换新节点。
- patchVnode方法来判断如果是同一类型节点,则进行深层次比较,具體做了以下事情。
- 找到对应的真实dom,称为
el
- 判断
Vnode
和oldVnode
是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将
el
的文本节点设置为Vnode
的文本节点。 - 如果
oldVnode
有子节点而Vnode
没有,则删除el
的子节点 - 如果
oldVnode
没有子节点而Vnode
有,则将Vnode
的子节点真实化之后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点,这一步很重要
- updateChildren方法用于新旧虚拟节点的子节点对比,主要采用首尾指针法进行对比,命中顺序为新前对比旧前,新后对比旧后,新后对比旧前与新前对比旧后,入后以上均不满足则采用key做映射。
如果对DIFF算法想要深入理解的话,可以查看本文作者写的另一篇详解DIFF算法的文章【Vue深入】之DIFF算法 - 掘金 (juejin.cn)
9.谈谈你对Vue 中的 key 的理解
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速,key的作用主要是为了更高效的更新虚拟DOM。
因为从源码中可以知道,vue在patch过程中判断两个节点是否是相同节点时key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕,它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。
开发过程使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,因为这可能导致一些隐蔽的bug;
如果对key的作用想要深入理解的话,可以查看本文作者写的另一篇详解DIFF算法的文章【Vue深入】之DIFF算法 - 掘金 (juejin.cn)里面有vue源码对于key使用的判断。
10.谈谈你对Vue SSR 的理解
SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端,使用SSR主要是为了解决首屏加载速度比较慢的问题,但是他也有一定的弊端,它也会使我们的开发条件受到限制,项目中是否需要使用还是需要根据项目的具体需求进行分析判断。
SSR优点:
-
更好的 SEO: 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
-
更快的内容到达时间(首屏加载更快): SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
SSR缺点:
- 更多的开发条件限制: 例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
11.谈谈你对Vue双向绑定的理解
vue的双向绑定是使用v-model
标签进行处理的,v-model
是语法糖,默认情况下相当于:value
和@input
,一个简单的v-model
是将值绑定到:value
之后通过@input
方法再将值绑定为$event.target.value
加以实现的。
原理:vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,原理是观察者observer通过Object.defineProperty()来劫持到各个属性的getter setter,在数据变动的时候,会被observer观察到,会通过Dep通知数据的订阅者watcher,之后进行相应的视图上面的变化。
数据劫持:
第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果
12.谈谈你对Vue响应式原理的理解
数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。
MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
以vue为例说明,通过数据响应式加上虚拟DOM和patch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。
vue2.X版本的响应式主要依赖于底层得Object.defineProperty()与Object.defineReactive()组成得一个闭包环境,通过访问其中的getter和setter方法进行响应式
其中分为对象响应式和数组响应式两部分
对象响应式: 对象响应式中会将data通过observer进行代理或者拦截,获得getter和setter的能力,data中每一个属性都有一个依赖dep,每一个依赖都有若干个wachter进行监视,当data中数据改变时,通知给dep,dep再通知给wachter,wachter就从对应的data中拿到值后渲染到页面。
数组响应式: 数组响应式中会以Array.prototype为原型创建一个arrayMthods,之后使用Object.setOrototypeOf使我们自身的数组强制指向arrayMthods,其中会重写七大数组的原生方法(push,sort,shfit,unshfit,pop,splice,reverse)使这些方法可以额外的做更新通知,从而作出响应。
但vue2.x版本的响应式也具有一定的弊端,例如无法直接更改数组中对应索引得值和无法直接删除数组中的数据,因此引入了this.delete()进行处理,并且由于初期化的时候会对整个data对象进行遍历以便对data的每个属性添加getter和settse,因此data数据的层级不能过深,否则会有一定的内存损耗。
为了解决这些问题,vue3重新编写了响应式的实现:利用ES6的Proxy代理需要响应化的数据,Proxy有着高达13中的代理方式并且是对整个对象进行代理,因此初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了。
13.谈谈你对Vue依赖收集的理解
通过遍历所有data中的属性,使用Object.defineProperty为其添加getter和setter,在getter中每个属性会new Dep来被记录为一个依赖,每一个依赖都有若干个wachter进行监视,当data中数据改变时,通知给相应的dep,dep再通知给wachter,wachter就从对应的data中拿到值后渲染到页面。
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
14.谈谈你对this.$nextTick的理解
因为vue存在异步更新策略,如果修改了data的某一个值,并不会立即反应到页面中,而是会开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,只有在当前任务空闲时才会去执行队列任务,这就有一个延迟时间了。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。
在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值。
this.$nextTick()
方法主要是用在随数据改变而改变的dom应用场景中,vue中数据和dom渲染由于是异步的,所以,要让dom结构随数据改变这样的操作都应该放this.$nextTick()
的回调函数中
15.Vue要做权限管理该怎么做?
权限管理一般需求是两个:页面权限和按钮权限。
-
具体实现的时候分后端和前端两种方案:
前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个
asyncRoutes
数组,需要认证的页面在其路由的meta
中添加一个roles
字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)
方式动态添加路由即可。后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过
addRoutes
动态添加路由信息按钮权限的控制通常会实现一个指令,例如
v-permission
,将按钮要求角色通过值传给v-permission指令,在指令的moutned
钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。 -
纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;后端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
16.谈谈你是如何解决跨域问题的
发生原因是因为前台和后端得服务器网址不一致而产生的。
通俗来讲如果前端服务器为3000,后端服务器为5000,就会发生跨域错误,前端会将请求头数据发送给后端,后端也会将响应数据返回给前端,但是响应数据会被ajax引擎拦截,因此发生跨域错误。
通常我们使用代理来解决跨域的问题。
使用代理进行跨域的原理是:将域名发送给本地的服务器(启动vue项目的服务,loclahost:8080),再由本地的服务器去请求真正的服务器。
在vue中主要采用两种方式配置代理
1.在package.json文件中配置proxy只想后端服务器(很少使用)
2.在vue根目录下得vue.config.js文件中进行配置(常用)
主要就是配置target,changOrigin和pathRewrite这三个参数
target参数指向后台得真实接口
changOrigin参数配置为true允许跨域
pathRewrite参数会重写路径
17.谈谈你有对 Vue 项目进行过哪些方面的优化
-
对象层级不要过深,否则性能就会差(object.defineproperty会进行遍历监听)
-
不需要响应式的数据不要放到 data 中(object.defineproperty会对对象的每个属性开启监听)
-
v-if 和 v-show 区分使用场景
-
computed 和 watch 区分使用场景
-
v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
-
大数据列表和表格性能优化-虚拟列表
-
大数据下拉组件性能优化-数据懒加载
-
防止内部泄漏,组件销毁后把全局变量和事件销毁
-
路由懒加载
-
第三方插件的按需引入(不要全部引入,不然打包体积过大)
-
适当采用 keep-alive 缓存组件
-
防抖、节流运用
-
服务端渲染 SSR
推荐
如果有想要继续学习浏览器篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之浏览器篇 - 掘金 (juejin.cn)
如果有想要继续学习CSS原理篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之CSS篇 - 掘金 (juejin.cn)
如果有想要继续学习JS原理篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之JavaScript原理篇 - 掘金 (juejin.cn)
如果有想要继续学习手写JS常用方法的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之手写常用JS方法 - 掘金 (juejin.cn)
如果有想要了解vue3.0对比vue2.x优势的读者,可以观看笔者的另一篇文章【面试宝典】之vue3.0对比vue2.x优势 - 掘金 (juejin.cn)
结语
本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力。