知识梳理——vue

30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)
vue2.x高阶问题,你能答多少

1.Vue模板编译的原理

就是将template(模板)转化为render(渲染函数)的过程。会经历以下阶段:

1.解析器: 将模板解析成AST(abstract syntax tree 抽象语法树)
2.优化器: 遍历AST标记静态节点
(生成渲染函数之前这个阶段,需要做一个优化操作:遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现这个节点有这个标记,就不会重新渲染它。)
3.代码生成器: 将优化后的AST树转换为可执行的代码(渲染函数)

2.Vue2.x和Vue3.x渲染器的diff算法分别说一下

简单来说,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中的预处理思路,先处理新旧两组子节点中相同的前置节点和相同的后置节点。当前置节点和后置节点全部处理完毕后,如果无法简单的通过挂载新节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。

3.SSR了解吗?

SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。

SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求。

4.hash路由和history路由实现原理说一下

hash路由的本质是浏览器location对象中的hash属性,它会记录链接地址中‘#’后面的内容(#part1)。因此,我们可以通过监听window.onhashchange事件获取到跳转前后访问的地址,从而实现地址切换的目的。

history路由:实际采用了HTML5中提供的API来实现,主要有history.pushState()和history.replaceState()。通过这两个新增的API,就可以实现无刷新的更改地址栏链接,配合AJAX就可以做到整个页面的无刷新跳转。
(在之前的HTML版本中,我们可以通过history.back()、history.forward()和history.go()方法来完成用户历史记录中向后和向前的跳转。)

5.你都做过哪些Vue的性能优化?

编码阶段
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

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

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不相互影响,data必须是一个函数。

7.再说一下vue2.x中如何监测数组变化

vue通过原型拦截的方式重写了数组的7个方法,并对push、unshift、splice三个能增加数组长度的方法做了判断,获取到新插入的值,把它变成一个响应式对象,再调用notify通知依赖更新。

8.说一下什么是virtual dom

用 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 之间做了一个缓存。

Virtual 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 比较差异,可以只渲染局部

9.对vue3的了解

  1. 独立于vue的库(如reactivity响应式库),减少引包的体积
  2. vue2:flow vue3:ts
  3. 源码体积的优化
    a. 移除冷门feature(如filter、inline-template……)
    b. 引入tree-shaking(依赖es6的import和export),编译时标记未引入的模块、组件,减少打包体积
  4. 数据劫持的优化:Object.defineProperty -> proxy
  5. 编译优化:动态节点

10. Object.defineProperty 与 Proxy

1.Object.defineProperty:

  • 只能对属性进行劫持,需要递归遍历对象的每个属性,执行Object.defineProperty把每一层对象数据都变成响应式的(如果定义的响应式数据过于复杂,会有很大的性能负担)
  • 不能检测对象属性的添加和删除(需要重新遍历)

2.Proxy

  • 在getter中去递归响应式,真正访问到的内部对象才会变成响应式,而不是无脑递归,提升了性能
  • 劫持的是整个对象,能检测到对象属性的添加和删除

11. vue组件间通信

  1. 父子通信: 父向子传递数据是通过 props,子向父是通过 events( $emit);
    通过父链 / 子链也可以通信( $parent / $children);
    ref 也可以访问组件实例;
    provide / inject API;
    a t t r s / attrs/attrs/listeners
  2. 兄弟通信: Bus($emit / $on);Vuex
    $emit / $on 这种方法通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案vuex。
  3. 跨级通信: Bus;Vuex;provide / inject API、 a t t r s / attrs/attrs/listeners
    Vue 组件间通信六种方式(完整版)

12. slot的用法

子组件:

我是子组件

父组件


	
	

13.单页面

单页面应用的精髓,就是点击任何链接,都不会引起页面的整体刷新。只会通过javascript,来替换页面的局部内容。

1.优点

  1. 有良好的交互体验
    能提升页面切换体验,用户在访问应用页面是不会频繁的去切换浏览页面,从而避免了页面的重新加载;
  2. 前后端分离开发
    单页Web应用可以和 RESTful 规约一起使用,通过 REST API 提供接口数据,并使用 Ajax 异步获取,这样有助于分离客户端和服务器端工作。更进一步,可以在客户端也可以分解为静态页面和页面交互两个部分;
  3. 减轻服务器压力
    服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力会提高几倍;
  4. 共用一套后端程序代码
    不用修改后端程序代码就可以同时用于 Web 界面、手机、平板等多种客户端;

2. 缺点:

  1. SEO难度较高
    由于所有的内容都在一个页面中动态替换显示,所以在SEO上其有着天然的弱势,所以如果你的站点对SEO很看重,且要用单页应用,那么就做些静态页面给搜索引擎用吧;
  2. 前进、后退管理
    由于单页Web应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理,当然此问题也有解决方案,比如利用URI中的散列+iframe实现;
  3. 初次加载耗时多
    为实现单页Web应用功能及显示效果,需要在加载页面的时候将JavaScript、CSS统一加载,部分页面可以在需要的时候加载。所以必须对JavaScript及CSS代码进行合并压缩处理;

14.为什么v-if和v-for不能一起使用

当他们处于同一节点时,v-if比y-for有更高的优先级,所以v-if将没有权限访问v-for里面的变量

15.vue有哪些修饰符

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的用法

16. computed 和 watch 的区别

computed: 是计算属性,主要用于同步数据的处理。基于他们的响应式依赖进行缓存,只在相关响应式依赖发生变化时,才会重新求值

watch: 是侦听属性,允许执行异步操作,观测某个值的变化去完成一段开销较大的复杂业务逻辑

17. watch属性deep和immediate区别

deep: 是否深入观察。值为true时:开启深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上监听器(在页面初始化时不会触发,只有当值改变时,才会触发)

immediate: 是否立即触发。值为true时:会立即执行里面的 handler 方法。

18. Vue 的生命周期

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,在这里做善后工作也可以

19. Vue 的父组件和子组件生命周期钩子函数执行顺序?

加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

父组件更新过程
父 beforeUpdate -> 父 updated

销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

20. 父组件可以监听到子组件的生命周期吗?

比如有父组件 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 钩子函数 ...     

21.vue-router中常用的钩子函数有哪些?

一:全局钩子:无论访问哪一个路径,都会触发全局的钩子函数,位置是调用router的方法

  • beforeEach、
  • afterEach、
  • beforeResolve

二:单个路由里面的钩子:写在路由配置中,只有访问到这个路径,才能触发钩子函数(单个路由独享的)

  • beforeEnter

三:组件路由:写在组件中,访问路径,即将渲染组件的时候触发的

  • beforeRouteEnter、
  • beforeRouteUpdate、
  • beforeRouteLeave

Vue-Router路由钩子函数(导航守卫)

22.mixin和组件产生冲突

1.data 数据冲突
会使用组件中的data,覆盖mixin
2.methods 方法冲突
会使用组件中的方法,覆盖mixin
3.computed 计算属性冲突
会使用组件中的方法,覆盖mixin
4.生命周期函数冲突
先执行mixin中生命周期函数中的代码,然后再执行组件内部的代码

23.watchEffect 与 watch 有什么不同?

  1. watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了响应式的属性, 那么当这些属性变更的时候,这个回调都会执行,而 watch 只能监听指定的属性而做出变更(v3开始可以同时指定多个)。
  2. watch 可以获取到新值与旧值(更新前的值),而 watchEffect 是拿不到的。
  3. watchEffect 如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而 watch 不需要,因为他一开始就指定了依赖。

24. v-show 与 v-if 有什么区别?

v-if :1. 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建; 2. 它也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show: 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。

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

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

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  1. 一般结合路由和动态组件一起使用,用于缓存组件;
  2. 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  3. 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

keep-alive 的实现原理:

keep-alive实例会缓存对应组件的VNode,如果命中缓存,直接从缓存对象返回对应VNode

keep-alive中运用了LRU(Least Recently Used)算法:【LRU算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)】

  1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名; 如果 keep-alive 存在多个子元素,keep-alive 要求同时只有一个子元素被渲染。所以在开头会获取插槽内的子元素,调用 getFirstComponentChild 获取到第一个子元素的 VNode。
  2. 根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则开启缓存策略。
  3. 根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键)。
  4. 如果不存在,则在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。最后将该组件实例的keepAlive属性值设置为true。

给我五分钟,我把 keep-alive 的用法和原理跟你说明白

26.你使用过 Vuex 吗?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
(3)主要包括以下几个模块:

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

你可能感兴趣的:(star,vue,面试,vue.js)