Vue面试题整理
- 1. v-if与v-show的区别以及使用场景
- 2. 说说你对 SPA 单页面的理解,它的优缺点分别是什么?
- 3. 详细阐述一下导航守卫
- 4. 怎样理解 Vue 的单向数据流
- 5. computed 和 watch 的区别和运用的场景?
- 6.Vue 生命周期
- 7. Vue 的父组件和子组件生命周期钩子函数执行顺序
- 8. 父组件监听子组件的生命周期
- 9. keep-alive
- 10. 组件中 data 为什么是一个函数?
- 11. Vue 是否能检测到直接给一个数组项赋值的变化
- 12. v-model的理解
- 13. Vue 组件间通信方式
- 14. Vue 如何实现MVVM数据双向绑定
- 15. Proxy 与 Object.defineProperty 优劣对比(vue2和vue3重大区别?)
- 16.Vue 用 vm.$set() 解决对象新增属性不能响应的问题
- 17. 虚拟 DOM 实现原理
- 18. Vue 中的 key 的作用
- 19. Vue 项目优化
- 20. $nextTick原理与使用场景
- 21. Vuex
1. v-if与v-show的区别以及使用场景
v-if
是惰性的,通过控制dom节点的存在与否来控制元素的显隐, 是真正的条件渲染。因为它会确保切换过程中合适地销毁和重建内部的事件监听和子组件。如果初始条件为假,则什么也不做;只有在条件第一次变为真时才会开始渲染条件块。v-show
是在任何条件下(首次条件是否为真)都被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。- v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
- 如果需要非常频繁地切换,使用 v-show 较好;如果在运行时条件很少改变,使用 v-if 较好。
2. 说说你对 SPA 单页面的理解,它的优缺点分别是什么?
SPA( single-page application )仅在 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点
- 用户体验友好好、速度快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理,SPA的出现促进了前后端的分离;
缺点
首屏渲染时间较长
,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;前进后退路由管理
:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;不利于SEO
,由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
3. 详细阐述一下导航守卫
vue-router
提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的
, 单个路由独享的
, 或者组件级
的。
①全局守卫
router.beforeEach
: 全局前置守卫router.beforeResolve
: 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用router.afterEach
全局后置钩子 进入路由之后
示例:
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子...');
});
to,from,next 三个参数:
- to: Route: 即将要进入的目标 路由对象
- from: Route: 当前导航正要离开的路由
- next: Function: 一定要调用该方法来 resolve 这个钩子,否则不能进入路由(页面空白)。
②路由独享守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
// 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
③路由组件内的守卫
- beforeRouteEnter:进入路由前
- beforeRouteUpdate (2.2 新增):路由复用同一个组件时
- beforeRouteLeave:离开当前路由时
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
beforeRouteEnter
守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
beforeRouteEnter 是支持给 next 传递回调的唯一守卫
。
④完整的路由导航解析流程(不包括其他生命周期):
- 触发进入其他路由。
- 调用要离开路由的组件守卫
beforeRouteLeave
- 调用局前置守卫:
beforeEach
- 在重用的组件里调用
beforeRouteUpdate
- 在路由配置里调用路由独享守卫
beforeEnter
。 - 解析异步路由组件。
- 在将要进入的路由组件中调用
beforeRouteEnter
- 调用全局解析守卫
beforeResolve
- 导航被确认。
- 调用全局后置钩子的
afterEach
钩子。 - 触发DOM更新(mounted)。
- 执行
beforeRouteEnter
守卫中传给next
的回调函数
⑤触发钩子的完整顺序:
将路由导航
、keep-alive
、和组件生命周期钩子
结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件:
beforeRouteLeave
:路由组件的组件离开路由前钩子,可取消路由离开。beforeEach
: 路由全局前置守卫,可用于登录验证、全局路由loading等。beforeEnter
: 在路由配置里调用路由独享守卫beforeRouteEnter
: 路由组件的组件进入路由前调动用。beforeResolve
:路由全局解析守卫afterEach
:路由全局后置钩子- beforeCreate:组件生命周期,不能访问this。
- created:组件生命周期,可以访问this,不能访问dom。
- beforeMount:组件生命周期
- deactivated: 离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
- mounted:访问/操作dom。
- activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
- 执行
beforeRouteEnter
回调函数next
。
⑥$route和 $router的区别
- router为VueRouter的实例,是一个全局路由对象,包含了路由跳转的方法、钩子函数等。
this.$router.push()
:跳转到不同的url,但这个方法会向history栈添加一个记录,点击后退会返回到上一个页面。this.$router.replace()
:同样是跳转到指定的url,但是这个方法不会向history里面添加新的记录,点击返回,会跳转到上上一个页面。上一个记录是不存在的。this.$router.go(n)
:相对于当前页面向前或向后跳转多少个页面,类似 window.history.go(n)。n可为正数可为负数。正数返回上一个页面
- route 是路由信息对象||跳转的路由对象,每一个路由都会有一个route对象,是一个局部对象,包含path,params,hash,query,fullPath,matched,name,meta等路由信息参数。
4. 怎样理解 Vue 的单向数据流
父级 prop 的更新会向下流动到子组件中,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值,但是子组件无权修改父组件传递给它的数据,当开发者尝试这样做的时候,vue 将会报错。这样做是为了组件间更好的解耦。这样的设计也是为了方便调试代码。
子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
所以,当你想要在子组件去修改 props 时,把这个子组件当成父组件那样用,所以就有了
1、定义一个局部变量,并用 prop 的值初始化它。
props: ['name'],
data: function () {
return {
counter: this.name
}
}
2、定义一个计算属性,处理 prop 的值并返回。
props: ['sex'],
computed: {
normalizedSize: function () {
return this.sex.trim().toLowerCase()
}
}
5. computed 和 watch 的区别和运用的场景?
区别:
computed
: 是计算属性,依赖其它属性值,并且 computed 的值有缓存
,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;watch
: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
computed:
一个数据受多个数据影响
,当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;watch
:一个数据影响多个数据
,当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
6.Vue 生命周期
Vue 实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
updated | 组件数据更新之后 |
activated | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestory | 组件销毁前调用 |
destoryed | 组件销毁后调用 |
经典图解如下: | |
7. Vue 的父组件和子组件生命周期钩子函数执行顺序
- 加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted - 子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated - 父组件更新过程
父 beforeUpdate -> 父 updated - 销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
8. 父组件监听子组件的生命周期
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 created就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
<Child @created="doSomething"/>
// Child.vue
created() {
this.$emit("created");
}
还有一种特别简单的方式,子组件不需要任何处理,只需要在父组件引用的时候通过@hook
来监听即可:
// Parent.vue
<Child @hook:created="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 created钩子函数 ...');
},
// Child.vue
created(){
console.log('子组件触发 created 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 created钩子函数 ...
// 父组件监听到 created钩子函数 ...
当然 @hook 方法不仅仅是可以监听 created,其它的生命周期事件,例如:mounted,updated 等都可以监听。
9. keep-alive
keep-alive是Vue的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
特点
- 提供
include
和exclude
属性,两者都支持字符串
或正则表达式
(需要动态绑定), include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高; - 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
10. 组件中 data 为什么是一个函数?
因为组件是复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
11. Vue 是否能检测到直接给一个数组项赋值的变化
由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
- 当你直接修改数组的长度时,例如:
vm.items.length = newLength
// Array.prototype.splice
vm.items.splice(newLength)
12. v-model的理解
在官网我们可以得知,v-model只是语法糖而已
v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性 和 input 事件;
- checkbox 和 radio 元素使用 checked 属性 和 change 事件;
- select 元素使用value 属性 和 change 事件;
如 input 表单元素:
<input v-model='name'>
等价于
<input v-bind:value="name" v-on:input="name= $event.target.value">
13. Vue 组件间通信方式
①props/$emit
适用 父子组件通信
父组件A通过props的方式向子组件B传递,子组件B to 父组件A 通过在 B 组件中 $emit发送自定义事件, A 组件中 v-on 的方式动态绑定这个自定义事件实现。
② 事件总线 $ emit/$on
适用于 父子、隔代、兄弟组件通信
EventBus这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
③vuex
适用于 父子、隔代、兄弟组件通信
④. $ attrs/$listeners
适用于 隔代组件通信
$ attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=“$attrs” 传入内部组件。通常配合 inheritAttrs 选项一起使用。
⑤provide/inject
适用于 隔代组件通信
祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
⑥ $parent / $children与 ref
适用 父子组件通信
14. Vue 如何实现MVVM数据双向绑定
- 实现一个监听器
Observer
:利用Object.defineProperty()
对属性都加上setter
和getter
用来劫持并监听所有属性,如果属性发生变化,就通知订阅者。 - 实现一个解析器
Compile
:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,一旦数据有变动,收到通知,调用更新函数进行数据更新
。 - 实现一个订阅者
Watcher
:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是接收 Observer 中的属性值变化的消息,触发解析器 Compile 中对应的更新函数进行视图渲染。 - 实现一个订阅器
Dep
:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
15. Proxy 与 Object.defineProperty 优劣对比(vue2和vue3重大区别?)
说说Vue2.0和Vue3.0有什么区别
1.重构响应式系统,使用Proxy替换Object.defineProperty, 使用Proxy优势:
- Proxy 可以直接监听对象而非属性;
- Proxy 可以直接监听数组的变化;
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Object.defineProperty 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
defineProperty缺点:
- Object.defineProperty 只能劫持对象的属性(key值.Object.key()),因此我们需要对每个对象的每个属性进行遍历。
- Object.defineProperty不能监听数组。是通过重写数据的那7个可以改变数据的方法来对数组进行监听的。
- Object.defineProperty 也不能对 es6 新产生的 Map,Set 这些数据结构做出监听。
- Object.defineProperty也不能监听新增和删除操作,通过 Vue.set()和 Vue.delete来实现响应式的
2.新增Composition API,更好的逻辑复用和代码组织
3.重构Virtual DOM
- 模板编译时的优化,将一些静态节点编译成常量
- slot优化, 将slot编译为lazy函数, 将slot的渲染的决定权交给子组件
- 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)
4.代码结构调整,更便于Tree shaking,使得体积更小
5.使用Typescript替换Flow
16.Vue 用 vm.$set() 解决对象新增属性不能响应的问题
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性。
vm. $set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发响应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
17. 虚拟 DOM 实现原理
- 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
- diff 算法 — 比较两棵虚拟 DOM 树的差异;
- pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
18. Vue 中的 key 的作用
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
。
Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,新旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。
因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
不建议使用index作为key?
比如说插入了一条数据
之前的数据 之后的数据
key: 0 index: 0 name: test1 key: 0 index: 0 name: test1
key: 1 index: 1 name: test2 key: 1 index: 1 name: 我是插队的那条数据
key: 2 index: 2 name: test3 key: 2 index: 2 name: test2
key: 3 index: 3 name: test3
对比发现除了第一个数据可以复用之前的之外,另外三条数据都需要重新渲染,而我想要的只是新增的那一条数据新渲染出来就行了,最好的办法是使用数组中不会变化的那一项作为key值
,即使用id作为key值。此时再来看其中发生的变化。
之前的数据 之后的数据
key: 1 id: 1 index: 0 name: test1 key: 1 id: 1 index: 0 name: test1
key: 2 id: 2 index: 1 name: test2 key: 4 id: 4 index: 1 name: 我是插队的那条数据
key: 3 id: 3 index: 2 name: test3 key: 2 id: 2 index: 2 name: test2
key: 3 id: 3 index: 3 name: test3
19. Vue 项目优化
(1)代码层面的优化
-
v-if 和 v-show 区分使用场景
-
computed 和 watch 区分使用场景
-
v-for 遍历必须为 item 添加 key,且避免同时使用 v-if。因为v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。
-
事件的销毁
-
图片、路由懒加载
-
第三方插件的按需引入
-
服务端渲染 SSR or 预渲染
(2)Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
(3)基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
20. $nextTick原理与使用场景
可能你还没有注意到,Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际 (已去重的) 工作。
- 在$nextTick 当中的操作不会立即执行,而是等数据更新、DOM更新完成之后再执行,这样我们拿到的肯定就是最新的了。
- $nextTick方法将回调延迟到下次DOM更新循环之后执行。
即$nextTick将回调函数放到微任务或者宏任务当中以延迟它的执行顺序