前端基础之vue篇

1,831 阅读9分钟

01.如何理解MVVM原理?

MVVMModel-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModelModel层代表数据模型,View代表UI组件,ViewModelViewModel层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

  • 传统的MVC指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数据。将结果返回给前端,页面重新渲染
  • MVVM:传统的前端会将数据手动渲染到页面上,MVVM模式不需要用户收到操作dom元素,将数据绑定到viewModel层上,会自动将数据渲染到页面中,视图变化会通知viewModel层更新数据。ViewModel就是我们MVVM模式中的桥梁。

02.响应式数据的原理是什么?

核心是利用Object.defineProperty方法

  • 组件在初始化的时候会调用Object.defineProperty方法(不兼容IE8)将data中的所有属性定义成访问器属性,也就是为他们定义getter/setter方法。
  • render function被渲染的时候,因为会读取所需对象的值,所以会触发getter函数进行「依赖收集」,代码层面就是调用DepaddSubs方法将观察者的Watcher对象添加进subs数组中,每个组件都有一个自己的Watcher,而data中的每个属性会维护一个这样的依赖数组。
  • 当数据发生变化或者视图导致的数据发生了变化时,会触发数据劫持的setter函数,setter方法会调用Depnotify方法通知目前Dep对象的subs中的所有Watcher对象触发更新操作,Wather就会再次通过update方法来更新视图。

03.Vue中是如何检测数组和对象变化?

首先对于data中的属性是对象时候,vue会遍历对象的所有属性进行依赖收集,如果对象属性还是对象,那么会继续递归遍历这个对象的所有属性进行依赖收集。

对于数组,会遍历数组的每一项进行依赖收集,如果数组的单项是数组或者对象,会继续进行递归依赖收集。所以我们修改数组的任何一项或者任何一项的任意属性,都会触发对应的setter方法,触发更新操作。

同时,对于数组,vue重写了数组原型链的方法,在调用原型链方法的时候,会自动触发更新操作。其中,如果调用的是会增加数组项的pushunshiftsplice这三个方法,那么在触发更新前,会对新增的项进行依赖收集。

03.直接给一个数组项赋值,Vue 能检测到变化吗?

vue的监听机制都是通过defineproperty来实现的,它是不能检测对象新添加的属性,对象可以初始化的时候就设置好属性,不加新属性,数组也是对象,然而如果要求数组初始化的时候就设置好所有属性(索引),不能新增索性(改变长度),显然是不可能的,那么要数组也就没用了。

由于JavaScript的限制,Vue不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如: vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如: vm.items.length = newLength

为了解决第一个问题,Vue提供了以下操作方法:通过索引来修改数组,使其能成为响应式,解决直接使用赋值不能响应的问题

Vue.set(vm.data,2,'huanpu','name') 对数组

Vue.$set(vm.data,'K','V')  对对象

为了解决第二个问题,Vue提供了以下操作方法:

vm.items.splice(newLength) // newLength 就是指的你更新的长度

04.为何Vue采用异步渲染?

当然也可以采用同步渲染,只是同步渲染的话,对数据的每次修改,都会立刻引发dom的修改,而对dom的操作是比较费时的,从性能的角度考虑,vue选择了异步更新。

实际上,Vue在默认情况下,每次触发某个数据的setter方法后,对应的Watcher对象其实会被push进一个队列queue 中,在下一个tick的时候将这个队列queue全部拿出来runWatcher对象的一个方法,用来触发patch操作) 一遍。

05.nextTick实现原理?

目前浏览器平台并没有实现nextTick方法,所以Vue源码中会依次检测PromiseMutationObserversetTimeoutsetImmediate等方式浏览器是否支持,如果支持就用它来创建一个微任务或宏任务,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。

nextTick内部维护一个了全局的回调函数队列,每次调用nextTick时,其回调函数会被push到这个全局的函数队列中,我们刚才说的创建的微任务或者宏任务被执行的时候,把这个队列里的函数取出依次执行。

06.Vue组件的生命周期?

  • beforeCreatenew Vue()之后触发的第一个钩子,在当前阶段datamethodscomputed以及watch上的数据和方法都不能被访问。
  • created在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,但这时真实dom还没有被创建。
  • beforeMount发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated
  • mounted在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。
  • beforeUpdate发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重新渲染。
  • updated发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
  • beforeDestroy发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
  • destroyed发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

除此之外,如果组件被keep-alive修饰,则会有另外另个生命周期

  • activited: 组件被激活时调用
  • deactivated: 组件被隐藏时调用

07.Ajax请求放在哪个生命周期中?

官方实例的异步请求是在mounted生命周期中调用的,而实际上也可以在created生命周期中调用。

08.何时需要beforeDestory?

当需要在组件销毁前做一些善后收尾工作的时候,比如清除计时器,可以在这个回调里写。

09.Vue父子组件生命周期调用顺序?

  • 组件的调用顺序都是先父后子,渲染完成的顺序是先子后父;
  • 组件的销毁操作是先父后子,销毁完成的顺序是先子后父。

加载渲染过程

父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted

子组件更新过程

父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程

父beforeUpdate -> 父updated

销毁过程

父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

10.Vue中Computed的特点?

computed会拥有自己的watcher,它内部有个属性dirty开关来决定computed的值是需要重新计算还是直接复用之前的值。,假设计算属性sum依赖响应式属性count

  1. sum第一次进行求值的时候会读取响应式属性count,收集到这个响应式数据作为依赖。并且计算出一个值来保存在自身的 value上,把dirty设为false,接下来在模板里再访问sum就直接返回这个求好的值value,并不进行重新的求值。
  2. count发生变化了以后会通知sum所对应的watcher把自身的dirty属性设置成 true,这也就相当于把重新求值的开关打开来了。这个很好理解,只有count变化了,sum才需要重新去求值。
  3. 那么下次模板中再访问到this.sum的时候,才会真正的去重新调用sum函数求值,并且再次把dirty设置为 false

11.Watch中的deep:true是如何实现的?

不光是数组类型,对象类型也会对深层属性进行依赖收集,比如deep watchobj,那么对obj.a.b.c = 5 这样深层次的修改也一样会触发watch的回调函数。本质上是因为Vue内部对需要deep watch 的属性会进行递归的访问,而在此过程中也会不断发生依赖收集。(只要此属性也是响应式属性)

这种方式会有性能损耗

12.Vue中事件绑定的原理?

原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的。

13.Vue中v-html会导致哪些问题?

v-html指令最终调用的是innerHTML方法将指令的value插入到对应的元素里,这很容易导致XSS跨站脚本攻击,一般情况下我们只对可信内容使用HTML插值,绝不要对用户提供的内容插值。

如果一定要用可以用<pre>标签代替<div>之类,主要是利用<pre>的一个属性:被包围在<pre> 元素中的文本通常会保留空格和换行符,并且文本也会呈现为等宽字体。

站在项目全局的角度,如果不放心可以使用xssnpm包,在webpack里配置对所有innerHTML方法进行覆盖,对innerHTML方法的值外面包上一层xss方法。

14.Vue中v-if和v-show的区别?

  • v-if是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
  • v-show就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSSdisplay属性进行切换。

所以,v-if适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show则适用于需要非常频繁切换条件的场景。

15.为什么v-for和v-if不能连用?

v-forv-if优先级高,所以嵌套使用的的话,每次v-for都会执行v-if,造成不必要的计算,影响性能,尤其是当之需要渲染很小一部分的时候。必要时候可以使用computed,或者在v-for的数组先filter筛选代替v-if

16.v-model中的实现原理及如何自定义v-model?

v-model本质上不过是语法糖,可以用v-model指令在表单<input><textarea><select> 元素上创建双向数据绑定,会根据控件类型自动选取正确的方法来更新元素,同时负责监听用户的输入事件以更新数据。

17.组件中的data为什么是一个函数?

同一个组件被复用多次,会创建多个实例。这些实例用的是同一个构造函数,如果data是一个对象的话。那么所有组件都共享了同一个对象。为了保证组件的数据独立性要求每个组件必须通过data函数返回一个对象作为组件的状态。

18.Vue组件如何通信?

  • 父子组件通信:props + $emit,或者$parent、$children获取父子实例,此外获取实例也可以用ref
  • 兄弟,跨级通信可以用Event Bus或者vuex

provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础

19.什么是作用域插槽?

插槽用于决定将所携带的内容,插入到子组件指定的某个位置,但内容必须在父组件中子组件的标签内定义,在子组件中用<slot></slot>标签接收。slot可以说是组件内部的占位符。

20.用vnode来描述一下dom结构?

  • 属性:tag:当前节点标签名,data:当前节点的数据,children:当前节点的子节点,text:当前节点的文本,elm:当前节点对应的真是dom节点,context:当前节点上下文,isStatic:是否为静态节点,isComment:是否为注释节点。
  • 方法:createEmptyVNodecreateTextVNodecloneVNode

21.diff算法的时间复杂度?

两个树的完全的diff算法是一个时间复杂度为O(n3),Vue进行了优化O(n3)复杂度的问题转换成O(n) 复杂度的问题(只比较同级不考虑跨级问题) 在前端当中, 你很少会跨越层级地移动Dom元素。 所以Virtual Dom只会对同一个层级的元素进行对比。

22.简述vue中diff算法原理?

  1. 先同级比较,在比较子节点
  2. 先判断一方有儿子一方没儿子的情况
  3. 比较都有儿子的情况
  4. 递归比较子节点

vue的Diff算法其核心是基于两个简单的假设:

  1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
  2. 同一层级的一组节点,他们可以通过唯一的id进行区分。

23.v-for中为什么要用key?

要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点。作用主要是为了高效的更新虚拟DOM

24.描述组件渲染和更新过程?

渲染组件时,会通过Vue.extend方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()进行挂载。更新组件时会进行patchVnode流程.核心就是diff算法

25.vue中模板编译原理?

简单说,vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:

  • 解析(parse):解析模版,用正则等方式将template模板中进行字符串解析,得到指令classstyle等数据,形成 一棵AST树。
  • 优化(optimize):深度遍历AST树,按照相关条件对树节点进行静态节点标记,其实就是给每个节点设置isStatic值,被设置成true的节点在后面进行diff分析的时候可以直接跳过。
  • 生成(generate):将优化后的AST树转换为可执行的代码。

26.vue中常见性能优化?

1. 代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景 参考
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化:数组冻结Object.freeze
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

2. Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析
  • Vue 项目的编译优化

3. 基础的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

27.vue中相同逻辑如何抽离?

Vue.mixin用法 给组件每个生命周期,函数等都混入一些公共逻辑。

28.为什么要使用异步组件?

如果组件功能多打包出的结果会变大,我可以采用异步的方式来加载组件。主要依赖import()这个语法,可以实现文件的分割加载。

29.谈谈你对keep-alive的了解?

keep-alive可以实现组件的缓存,当组件切换时不会对当前组件进行卸载,常用的2个属性include/exclude,2个生命周期activated,deactivated

30.实现hash路由和history路由?

  • hash模式:url中会有#存在如www.baidu.com/#/a,看起来丑陋且不好做SEO,但是由于路由器向服务器发请求的时候会忽略#后面的值,所以在浏览器刷新的时候都是很正常的不会再次发请求。
  • history模式:利用了 HTML5 History Interface 中新增的 pushState()replaceState() 方法。这两个方法应用于浏览器的历史记录栈,在当前已有的 backforwardgo 的基础之上,它们提供了对历史记录进行修改的功能。但当没有后端进行相应配置时,url就是我们看的正常的路由样子,就是因为看起来很像真的,所以刷新浏览器的时候(浏览器向服务器请求)会发现服务器根本没有这个路径资源,所以返回404

31.vue-router中导航守卫有哪些?

32.action和mutation的区别?

  • mutation是同步更新数据(内部会进行是否为异步方式更新数据的检测)
  • action异步操作,可以获取数据后调佣mutation提交最终数据

33.简述vuex工作原理?

Vuex是专门为Vuejs应用程序设计的状态管理工具,它采用集中式存储管理应用的所有组件的状态。

vuex主要由四部分构成:

  1. state
  2. mutations
  3. actions
  4. getters

vuex的响应式原理包括两部分:

  1. vuex的store是如何挂载注入到组件中呢?

利用vue的插件机制,项目初始化调用Vue.use(vuex)时,会调用vuexinstall方法,装载vuex。具体来说就是vuex利用vuemixin混入机制,在beforeCreate钩子前混入vuexInit方法,vuexInit方法实现了store注入vue组件实例,并注册了vuex store的引用属性$store

  1. 响应式原理

Vuexstate状态是响应式,是借助vuedata是响应式,将state存入vue实例组件的data中;Vuexgetters则是借助vue的计算属性computed实现数据实时监听。

34.vue3.0你知道有哪些改进?

  • Vue3采用了TS来编写
  • 支持 Composition API
  • Vue3中响应式数据原理改成proxy
  • vdom的对比算法更新,只更新vdom的绑定了动态数据的部分