VUE 2 面试题

229 阅读8分钟

1.说一下 Vue 的生命周期

  • beforeCreate 是 new Vue()之后触发的第一个钩子,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。

  • created 在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 Dom 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 Dom。

  • beforeMount 发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 Dom 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated。

  • mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点,使用$refs 属性对 Dom 进行操作。

  • beforeUpdate 发生在更新之前,也就是响应式数据发生更新,虚拟 dom 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

  • updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

  • beforeDestroy 发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

  • destroyed 发生在实例销毁之后,这个时候只剩下了 dom 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

2.那你能讲一讲 MVVM 吗?

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

3.简单说一下 Vue2 响应式数据原理

Vue 在初始化数据时,会使用 Object.defineProperty 重新定义 data 中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的 watcher)如果属性发生变化会通知相关依赖进行更新操作(发布订阅)

  • 实现一个监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

  • 实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

  • 实现一个解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

  • MVVM 作为一个入口,整合三者,搭起一个桥梁,从而达到了数据更新,视图改变

4.再说一下 Computed 和 Watch

  • Computed 本质是一个具备缓存的 watcher,依赖的属性发生变化就会更新视图。适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。

  • Watch 没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用 unWatch 手动注销哦。

5.说一下 v-if 和 v-show 的区别

  • 当条件不成立时,v-if 不会渲染 DOM 元素,v-show 操作的是样式(display),切换当前 DOM 的显示和隐藏。

  • 注意:v-if 会触发生命周期

6.说一下 v-model 的原理

v-model 本质就是一个语法糖,可以看成是 value + input 方法的语法糖。可以通过 model 属性的 prop 和 event 属性来进行自定义。原生的 v-model,会根据标签的不同生成不同的事件和属性。

7.组件中 data 为什么是函数

  • 如果组件里 data 直接写了一个对象的话,那么如果你在模板中多次声明这个组件,组件中的 data 会指向同一个引用。

  • 此时如果在某个组件中对 data 进行修改,会导致其他组件里的 data 也被污染。 而如果使用函数的话,每个组件里的 data 会有单独的引用,这个问题就可以避免了。

8.组件间的通信

  • 父 -> 子 props children ref provide/inject

  • 子 -> 父 $emit

  • 兄弟 bus

  • 多页面 vuex

9.nextTick()

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后,立即使用这个回调函数,获取更新后的 DOM。

10.vue.js 的两个核心是什么?

数据驱动和组件化。

11.vue 常用的修饰符

系统修饰符

  • .ctrl

  • .alt

  • .shift

  • .meta

鼠标按钮修饰符

  • .left

  • .right

  • .middle

其他修饰符

  • .lazy

  • .number

  • .trim

12.v-on 可以监听多个方法吗?

可以

13.vue 中 key 值的作用

  • 使用 key 来给每个节点做一个唯一标识

  • key 的作用主要是为了高效的更新虚拟 DOM。另外 vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,

  • 否则 vue 只会替换其内部属性而不会触发过渡效果。

14.说出至少 4 种 vue 当中的指令和它的用法

  • v-if(判断是否隐藏)

  • v-show(控制展示隐藏)

  • v-for(把数据遍历出来)

  • v-bind(绑定属性)

  • v-model(实现双向绑定)

  • v-text(输出一段文本)

  • v-html(输出一段结构)

15.怎么定义 vue-router 的动态路由?怎么获取传过来的值?

动态路由的创建,主要是使用 path 属性过程中,使用动态路径参数,以冒号开头,如下:

{
  path: '/details/:id'

  name: 'Details'

  components: Details
}

访问 details 目录下的所有文件,如果 details/a,details/b 等,都会映射到 Details 组件上。

当匹配到/details 下的路由时,参数值会被设置到 this.route.params下,所以通过这个属性可以获取动态参数this.route.params下,所以通过这个属性可以获取动态参数this.route.params.id

16.vue-router 有哪几种路由守卫?

  • 路由守卫为:

  • 全局守卫:beforeEach

  • 后置守卫:afterEach

  • 全局解析守卫:beforeResolve

  • 路由独享守卫:beforeEnter

17.routeroute和 router 的区别是什么?

  • $router 为 VueRouter 的实例,是一个全局路由对象,包含了路由跳转的方法、钩子函数等。

  • $route 是路由信息对象||跳转的路由对象,每一个路由都会有一个 route 对象,是一个局部对象,包含 path,params,hash,query,fullPath,matched,name 等路由信息参数。

18.不用 Vuex 会带来什么问题?

  • 可维护性会下降,你要想修改数据,你得维护三个地方

  • 可读性会下降,因为一个组件里的数据,你根本就看不出来是从哪来的

  • 增加耦合,大量的上传派发,会让耦合性大大的增加,本来 Vue 用 Component 就是为了减少耦合,现在这么用,和组件化的初衷相背。

19.vuex 有哪几种属性?

有五种,分别是 State、 Getter、Mutation 、Action、 Module。

20.vuex 的使用

  • 在 state 中定义数据

  • Getter 相当于 vue 中的 computed 计算属性,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算,Getters 可以用于监听、state 中的值的变化,返回计算后的结果

  • 给 action 注册事件处理函数,当这个函数被触发时候,将状态提交到 mutations 中处理。

  • mutations 是一个对象里。面的方法 都是同步事务,是更改 state 初始状态的唯一合法方法,具体的用法就是给里面的方法传入参数 state 或额外的参数

21.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

22.你的接口请求一般放在哪个生命周期中?

接口请求一般放在 mounted 中,但需要注意的是服务端渲染时不支持 mounted,需要放到 created 中。

23.为什么要虚拟 DOM?

  • 虚拟 DOM 就是为了解决这个浏览器性能问题而被设计出来的,如果一次操作中有 10 次更新 DOM 的操作,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存在本地的一个 js 对象中,最终将这个 js 对象一次性 attach 到 DOM 树上,通知浏览器去执行绘制工作,这样可以避免大量的无谓的计算量。

  • 在实际代码中,会对新旧两棵树进行一个深度的遍历,每个节点都会有一个标记。每遍历到一个节点,就把该节点和新的树进行对比,如果有差异就记录到一个对象中。然后映射到真是 DOM。

24.虚拟 DOM 与真实 DOM 的区别?

  • 虚拟 DOM 不会进行重排与重绘操作;

  • 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要修改的部分,最后进行重排和重绘,减少过多 DOM 节点重排和重绘损耗。

  • 虚拟 DOM 有效降低大面积(真实 DOM 节点)的重排和重绘,因为最终与真实 DOM 比较差异,可以局部渲染。

25.Vue 中的 diff 算法

概括起来就是对操作前后的 dom 树同一层的节点进行对比,一层一层对比,然后再插入真实的 dom 中,重新渲染,注意 diff 算法是需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点。

26.v-text 和{{}}的区别

  • {{ text }} // 将数据解析成为纯文本,不能显示输出 HTML

  • v-text="text":将数据解析为纯文本,不能输出真正的 html,与花括号的区别是在页面加载时不显示{{}}

27.hash 和 history 的区别

hash ——即地址栏 URL 中的#符号(此 hsah 不是密码学里的散列运算)。          比如这个 URL:www.abc.com/#/hello, hash 的值为#/hello。它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。

history ——利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法

  • 使用 pushState()跳转时会在历史记录里面添加一条历史记录,即 go(-1)返回的时候是返回到上一条记录

  • 使用 replaceState()跳转时不会往历史记录里面添加记录,即 go(-1)返回的时候是返回到上上一条记录

28.那个生命周期可以获取到真实DOM?

mounted钩子函数,此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了,可以在这里操作真实dom等事情

29.$nextTick 作用?实现原理?

  • 作用:

在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,此时DOM结构还没有更新出来,所以要使用Vue.nextTick()这个函数来获取,DOM生成后的真实结构

  • 原理:

当调用nextTick方法时会传入两个参数,回调函数和执行回调函数的上下文环境,如果没有提供回调函数,那么将返回promise对象。首先将拿到的回调函数存放到数组中,判断是否正在执行回调函数,如果当前没有在pending的时候,就会执行timeFunc,多次执行nextTick只会执行一次timerFunc,timeFunc其实就是执行异步的方法,在timeFunc方法中选择一个异步方法(首先判断是否支持promise,如果支持就将flushCallbacks放在promise中异步执行,并且标记使用微任务。如果不支持promise就看是否支持MutationObserver方法,如果支持就new了一个MutationObserver类,创建一个文本节点进行监听,当数据发生变化了就会异步执行flushCallbacks方法。如果以上两个都不支持就看是否支持setImmediate方法,如果支持setImmediate 就去异步执行flushCallbacks方法。如果以上三种方法都不支持,就使用setTimeout),然后异步去执行flushCallbacks方法,flushCallbacks中就是将传递的函数依次执行。

30.vue scoped属性作用?实现原理?

  • 作用:

在Vue文件中的style标签上有一个特殊的属性,scoped。当一个style标签拥有scoped属性时候,它的css样式只能用于当前的Vue组件,可以使组件的样式不相互污染。如果一个项目的所有style标签都加上了scoped属性,相当于实现了样式的模块化。

  • 原理:

Vue中的scoped属性的效果主要是通过PostCss实现的,PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom,这种做法使得样式只作用于含有该属性的dom元素(组件内部的dom)

31.vue 2.x defineProperty缺陷?

  • 虽然Object.defineProperty通过为属性设置getter/setter能够完成数据的响应式,但是它并不算是实现数据的响应式的完美方案,某些情况下需要对其进行修补或者hack,这也是它的缺陷,主要表现在两个方面:

1.无法检测到对象属性的新增或删除

2.由于js的动态性,可以为对象追加新的属性或者删除其中某个属性,这点对经过Object.defineProperty方法建立的响应式对象来说,只能追踪对象已有数据是否被修改,无法追踪新增属性和删除属性,这就需要另外处理。

32.$set原理?

在vue源码中 set 函数第一个入参是 target, set函数里面会对 target 进行判断,如果 target 是数组,使用 vue 实现的变异方法 splice 实现响应式,如果 target 是对象,判断属性存在,即为响应式,直接赋值,如果 target 本身就不是响应式,直接赋值,如果属性不是响应式,则调用 defineReactive 方法进行响应式处理,

33.vue是怎么重写数组方法的?

  •  vue在数据初始化时调用initData方法,然后通过new Observer对数据进行监测,然后对数据进行判断,如果是数组并且支持原型链就会执行protoAugment让目标原型链指向arrayMethods,arrayMethods用来改写数组的原型方法。内部会采用函数劫持的方式,当用户调用这些方法(push,pop,shift,unshift,sort,splice,reverse)之后,还会调用原数组的方法进行更新数组。拿到原数组的方法,然后重新定义这些方法。

  • 用户调方法时走的就是这个重写的mutator函数,这个函数还是会调用数组原有的方法,重写的mutator函数中会调用原生的方法,对新增数组的方法push,unshift,splice可以帮我们更新数组中的新增一项,对插入的数据使用observeArray再次进行监测,最后通过dep.notify通知视图更新。

34.Vue事件绑定原理说一下

  • 每一个Vue实例都是一个Event Bus,当子组件被创建的时候,父组件将事件传递给子组件,子组件初始化的时候是有$on方法将事件注册到内部,在需要的时候使用emit触发函数,而对于原生native事件,使用addEventListener绑定到真实的DOM元素上。

35.Vue模板渲染的原理是什么?

  • vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。

  • 模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。

parse阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。
optimize阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。
generate阶段:将最终的AST转化为render函数字符串。

36.template预编译是什么?

  • 对于 Vue 组件来说,模板编译只会在组件实例化的时候编译一次,生成渲染函数之后在也不会进行编译。因此,编译对组件的 runtime 是一种性能损耗。

  • 而模板编译的目的仅仅是将template转化为render function,这个过程,正好可以在项目构建的过程中完成,这样可以让实际组件在 runtime 时直接跳过模板渲染,进而提升性能,这个在项目构建的编译template的过程,就是预编译。