30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)
vue2.x高阶问题,你能答多少
就是将template(模板)
转化为render(渲染函数)
的过程。会经历以下阶段:
1.解析器:
将模板解析成AST(abstract syntax tree 抽象语法树)
2.优化器:
遍历AST标记静态节点
(生成渲染函数之前这个阶段,需要做一个优化操作:遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现这个节点有这个标记,就不会重新渲染它。)
3.代码生成器:
将优化后的AST树转换为可执行的代码(渲染函数)
简单来说,diff算法有以下过程
1.同级比较,再比较子节点
2.先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
3.比较都有子节点的情况(核心diff)
4.递归比较子节点
正常Diff两个树的时间复杂度是O(n^3)
, 但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n)
, 只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
Vue2
的核心Diff算法采用了双端比较
的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
Vue3.x
使用的快速diff算法
借鉴了ivi算法
和 inferno算法
在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。)
该算法中还运用了动态规划
的思想求解最长递增子序列。
快速diff算法在实测中性能最优,它借鉴了文本Diff中的预处理思路,先处理新旧两组子节点中相同的前置节点和相同的后置节点。当前置节点和后置节点全部处理完毕后,如果无法简单的通过挂载新节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。
SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求。
hash路由的本质是浏览器location对象中的hash属性,它会记录链接地址中‘#’后面的内容(#part1)。因此,我们可以通过监听window.onhashchange
事件获取到跳转前后访问的地址,从而实现地址切换的目的。
history路由:实际采用了HTML5中提供的API来实现,主要有history.pushState()和history.replaceState()。通过这两个新增的API,就可以实现无刷新的更改地址栏链接,配合AJAX就可以做到整个页面的无刷新跳转。
(在之前的HTML版本中,我们可以通过history.back()、history.forward()和history.go()方法来完成用户历史记录中向后和向前的跳转。)
编码阶段
1.尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
2.v-if和v-for不能连用
3.如果需要使用v-for给每项元素绑定事件时使用事件代理
4.SPA 页面采用keep-alive缓存组件
5.在更多的情况下,使用v-if替代v-show
5.key保证唯一
6.使用路由懒加载、异步组件
7.防抖、节流
8.第三方模块按需导入
9.长列表滚动到可视区域动态
10.加载图片懒加载
SEO优化
1.预渲染
2.服务端渲染SSR
打包优化
1.压缩代码
2.Tree Shaking / Scope Hoisting
3.使用cdn加载第三方模块
4.splitChunks抽离公共文件
用户体验
1.骨架屏
2.PWA
一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不相互影响,data必须是一个函数。
vue通过原型拦截
的方式重写了数组的7个方法,并对push、unshift、splice三个能增加数组长度的方法做了判断,获取到新插入的值
,把它变成一个响应式对象
,再调用notify
通知依赖更新。
用 js 按照DOM结构来实现的树形结构对象
1.用js对象模拟DOM(虚拟DOM)
利用 createElement 方法创建 VNode,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree
2.把此虚拟DOM转成真实DOM并插入页面中(render)
3.如果有事件发生修改了虚拟DOM,比较两颗虚拟DOM树的差异,得到差异对象(diff)
4.把差异对象应用到真正的DOM树上,视图就更新了(patch)
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。
其实这道题目面试官更想听到的答案不是上来就说「直接操作/频繁操作 DOM 的性能差」,如果 DOM 操作的性能如此不堪,那么 jQuery 也不至于活到今天。所以面试官更想听到 VDOM 想解决的问题以及为什么频繁的 DOM 操作会性能差。
首先我们需要知道:
DOM 引擎、JS 引擎 相互独立,但又工作在同一线程(主线程)JS 代码调用 DOM API 必须 挂起 JS 引擎、转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行。若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化,引擎间切换的单位代价将迅速积累。若其中有强制重排的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。
其次是 VDOM 和真实 DOM 的区别和优化:
1.虚拟 DOM 不会立马进行排版与重绘操作
2.虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗
3.虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部
1.Object.defineProperty:
2.Proxy
$emit / $on 这种方法通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案vuex。
子组件:
我是子组件
父组件
单页面应用的精髓,就是点击任何链接,都不会引起页面的整体刷新。只会通过javascript,来替换页面的局部内容。
1.优点
2. 缺点:
当他们处于同一节点时,v-if比y-for有更高的优先级,所以v-if将没有权限访问v-for里面的变量
1.事件修饰符
.stop
.prevent
.capture
.self
.once
.passive
2.按键修饰符
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right
3.系统修饰符
Do something
.ctrl
.alt
.shift
.meta
4.表单修饰符
.lazy
.number
.trim
5. sync修饰符
在某些情况下,我们可能需要对一个 prop 进行“双向绑定”。
简单的方法是 子组件向父组件发送一个事件,父组件监听该事件,然后更新prop <=> 可以用 .sync
代替
sync的用法
computed: 是计算属性,主要用于同步
数据的处理。基于他们的响应式依赖进行缓存,只在相关响应式依赖发生变化时,才会重新求值
watch: 是侦听属性,允许执行异步
操作,观测某个值的变化去完成一段开销较大的复杂业务逻辑
deep: 是否深入观察。值为true时:开启深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上监听器(在页面初始化时不会触发,只有当值改变时,才会触发)
immediate: 是否立即触发。值为true时:会立即执行里面的 handler 方法。
1. 生命周期是什么?
Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,我们称这是Vue的生命周期。通俗说:就是Vue实例从创建到销毁的过程,就是生命周期。
2. 各个生命周期的作用:
beforeCreate: 组件实例被创建之初,数据还没有挂载,只是一个空壳,无法访问到数据和真实的dom(一般不做操作)
created: 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成, $el 还不可用(一般可以在这里做初始数据的获取)
beforeMount: 接下来开始找实例或者组件对应的模板,编译模板为虚拟dom放入到render函数中准备渲染,然后执行beforeMount钩子函数,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated,在这里可以在渲染前最后一次更改数据的机会,不会触发其他的钩子函数(一般可以在这里做初始数据的获取)
mountd: 接下来开始render,渲染出真实dom,然后执行mounted钩子函数,此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了(可以在这里操作真实dom等事情…)
beforeUpdate: 当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建虚拟dom,与上一次的虚拟dom树利用diff算法进行对比之后重新渲染(一般不做什么事)
updated: 当更新完成后,执行updated,数据已经更改完成,dom也重新render完成(可以操作更新后的虚拟dom)
beforeDestroy: 当经过某种途径调用$destroy方法后,立即执行beforeDestroy,一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等
destroyed: 组件的数据绑定、监听…去掉后只剩下dom空壳,这个时候,执行destroyed,在这里做善后工作也可以
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程
父 beforeUpdate -> 父 updated
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
// Child.vue
mounted() {
this.$emit("mounted");
}
以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。
// Parent.vue
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
一:全局钩子:无论访问哪一个路径,都会触发全局的钩子函数,位置是调用router的方法
二:单个路由里面的钩子:写在路由配置中,只有访问到这个路径,才能触发钩子函数(单个路由独享的)
三:组件路由:写在组件中,访问路径,即将渲染组件的时候触发的
Vue-Router路由钩子函数(导航守卫)
1.data 数据冲突
会使用组件中的data,覆盖mixin
2.methods 方法冲突
会使用组件中的方法,覆盖mixin
3.computed 计算属性冲突
会使用组件中的方法,覆盖mixin
4.生命周期函数冲突
先执行mixin中生命周期函数中的代码,然后再执行组件内部的代码
v-if :1. 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建; 2. 它也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show: 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
keep-alive实例会缓存对应组件的VNode,如果命中缓存,直接从缓存对象返回对应VNode
keep-alive中运用了LRU(Least Recently Used)算法:【LRU算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)】
给我五分钟,我把 keep-alive 的用法和原理跟你说明白
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
(3)主要包括以下几个模块: