「2022」寒冬下我的面试知识点复盘【Vue3、Vue2、Vite】篇
前言
笔者今年2022寒冬下成功跳槽了阿里,这篇文章就是将自己面试的一些准备、知识总结分享出来~
如果这篇文章对你有用,
请一键三连(点赞评论+收藏)让更多的同学看到
如果需要
转载,请评论区留言,未经允许请不要私自转载;
防杠声明
这篇文章不是纯堆砌面试题,而是以知识总结为主,主观观点和主观总结居多,里面总结的知识点在我这次的面试中也不全都有用到~如果有写错的地方欢迎评论区提出,如果只是要杠那请右上角X掉慢走;
传送门
这个专栏预计要做以下这些内容,可以根据自己的需要跳转查看
「2022」寒冬下我的面试知识点复盘【JS】篇(加紧编写中)
「2022」寒冬下我的面试知识点复盘【Vue3、Vue2、Vite】篇
「2022」寒冬下我的面试知识点复盘【工程化】篇(加紧编写中)
「2022」寒冬下我的面试知识点复盘【Nodejs】篇(加紧编写中)
「2022」寒冬下我的面试知识点复盘【TypeScript】篇(加紧编写中)
本文标题思维导图

Vue3 篇
1.Vue3 带来的新变化 & 新特性总览
在 API 特性方面:
Composition API:可以更好的逻辑复用和代码组织,同一功能的代码不至于像以前一样太分散,虽然Vue2中可以用minxin来实现复用代码,但也存在问题,比如:方法或属性名会冲突、代码来源也不清楚等SFC Composition API语法糖:Teleport传送门:可以让子组件能够在视觉上跳出父组件(如父组件overflow:hidden)Fragments:支持多个根节点,Vue2中,编写每个组件都需要一个父级标签进行包裹,而Vue3不需要,内部会默认添加Fragments;SFC CSS变量:支持在<style></style>里使用v-bind,给CSS绑定JS变量(color: v-bind(str)),且支持JS表达式 (需要用引号包裹起来);Suspense:可以在组件渲染之前的等待时间显示指定内容,比如loading;v-memo:新增指令可以缓存html模板,比如v-for列表不会变化的就缓存,简单说就是用内存换时间
在 框架 设计层面:
代码打包体积更小:许多Vue的API可以被Tree-Shaking,因为使用了es6module,tree-shaking依赖于es6模块的静态结构特性;响应式 的优化:用Proxy代替Object.defineProperty,可以监听到数组下标变化,及对象新增属性,因为监听的不是对象属性,而是对象本身,还可拦截apply、has等方法;虚拟DOM的优化:保存静态节点直接复用(静态提升)、以及添加更新类型标记(patchflag)(动态绑定的元素)静态提升:静态提升就是不参与更新的静态节点,只会创建一次,在之后每次渲染的时候会不停的被复用;更新类型标记:在对比VNode的时候,只对比带有更新类型标记的节点,大大减少了对比Vnode时需要遍历的节点数量;还可以通过flag的信息得知当前节点需要对比的内容类型;优化的效果:Vue3的渲染效率不再和模板大小成正比,而是与模板中的动态节点数量成正比;
Diff算法 的优化:Diff算法使用最长递增子序列优化了对比流程,使得虚拟DOM生成速度提升200%
在 兼容性 方面:
Vue3不兼容IE11,因为IE11不兼容Proxy
其余 特点
v-if的优先级高于v-for,不会再出现vue2的v-for,v-if混用问题;vue3中v-model可以以v-model:xxx的形式使用多次,而vue2中只能使用一次;多次绑定需要使用syncVue3用TS编写,使得对外暴露的api更容易结合TypeScript。
2.Vue3 响应式
Vue3 响应式的特点
- 众所周知
Vue2数据响应式是通过Object.defineProperty()劫持各个属性get和set,在数据变化时发布消息给订阅者,触发相应的监听回调,而这个API存在很多问题; Vue3中为了解决这些问题,使用Proxy结合Reflect代替Object.defineProperty,- 支持监听
对象和数组的变化, - 对象嵌套属性只代理第一层,运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能取得很大进步;
- 并且能拦截对象
13种方法,动态属性增删都可以拦截,新增数据结构全部支持,
- 支持监听
Vue3提供了ref和reactive两个API来实现响应式;
什么是Proxy
Proxy是ES6中的方法,Proxy用于创建一个目标对象的代理,在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象;
defineProperty 和 Proxy 的区别
Object.defineProperty是Es5的方法,Proxy是Es6的方法defineProperty是劫持对象属性,Proxy是代理整个对象;defineProperty监听对象和数组时,需要迭代对象的每个属性;defineProperty不能监听到对象新增属性,Proxy可以defineProperty不兼容IE8,Proxy不兼容IE11defineProperty不支持Map、Set等数据结构defineProperty只能监听get、set,而Proxy可以拦截多达13种方法;Proxy兼容性相对较差,且无法通过pollyfill解决;所以Vue3不支持IE;
为什么需要 Reflect
- 使用
Reflect可以修正Proxy的this指向问题; Proxy的一些方法要求返回true/false来表示操作是否成功,比如set方法,这也和Reflect相对应;- 之前的诸多接口都定义在
Object上,历史问题导致这些接口越来越多越杂,所以干脆都挪到Reflect新接口上,目前是13种标准行为,可以预期后续新增的接口也会放在这里;
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
const user = new User();
const userProxy = new Proxy(user, {});
// 此时,`getName` 的 this 指向代理对象 userProxy
// 但 userProxy 对象并没有 #name 私有属性,导致报错
alert(userProxy.getName()); // Error
// 解决方案:使用 Reflect
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
Vue3 响应式对数组的处理
Vue2对数组的监听做了特殊的处理,在Vue3中也需要对数组做特殊的处理;Vue3对数组实现代理时,也对数组原型上的一些方法进行了重写;
原因:
- 比如使用
push、pop、shift、unshift、splice这些方法操作响应式数组对象时,会隐式地访问和修改数组的length属性,所以我们需要让这些方法间接读取length属性时禁止进行依赖追踪; - 还比如使用
includes、indexOf等对数组元素进行查找时,可能是使用代理对象进查找,也可能使用原始值进行查找,所以就需要重写查找方法,让查找时先去响应式对象中查找,没找到再去原始值中查找;
Vue3 惰性响应式
Vue2对于一个深层属性嵌套的对象做响应式,就需要递归遍历这个对象,将每一层数据都变成响应式的;- 而在
Vue3中使用Proxy并不能监听到对象内部深层次的属性变化,因此它的处理方式是在getter中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,减少性能消耗
Proxy 只会代理对象的第一层,Vue3 如何处理
- 判断当前
Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理,这样就实现了深度观测 - 检测数组的时候可能触发了多个
get/set,那么如何防止触发多次呢?我们可以判断key是否是当前被代理的target自身属性;
Vue3 解构丢失响应式
- 对
Vue3响应式数据使用ES6解构出来的是一个引用对象类型时,它还是响应式的,但是结构出的是基本数据类型时,响应式会丢失。 - 因为
Proxy只能监听对象的第一层,深层对象的监听Vue是通过reactive方法再次代理,所以返回的引用仍然是一个Proxy对象;而基本数据类型就是值;
Vue3 响应式 对 Set、Map 做的处理
Vue3对Map、Set做了很多特殊处理,这是因为Proxy无法直接拦截Set、Map,因为Set、Map的方法必须得在它们自己身上调用;Proxy返回的是代理对象;- 所以
Vue3在这里的处理是,封装了toRaw()方法返回原对象,通过Proxy的拦截,在调用诸如set、add方法时,在原对象身上调用方法;
其实还有一个方法是,用
Class搞一个子类去继承Set、Map,然后用子类new的对象就可以通过proxy来代理,而Vue没有采用此方法的原因,猜测是:calss只兼容到Edge13
3.Ref 和 Reactive 定义响应式数据
- 在
vue2中, 定义数据都是在data中, 而vue3中对响应式数据的声明,可以使用ref和reactive,reactive的参数必须是对象,而ref可以处理基本数据类型和对象 ref在JS中读值要加.value,可以用isRef判断是否ref对象,reactive不能改变本身,但可以改变内部的值- 在
模板中访问从setup返回的ref时,会自动解包;因此无须再在模板中为它写.value; Vue3区分ref和reactive的原因就是Proxy无法对原始值进行代理,所以需要一层对象作为包裹;
Ref 原理
ref内部封装一个RefImpl类,并设置get/set,当通过.value调用时就会触发劫持,从而实现响应式。- 当接收的是对象或者数组时,内部仍然是
reactive去实现一个响应式;
Reactive 原理
reactive内部使用Proxy代理传入的对象,从而实现响应式。- 使用
Proxy拦截数据的更新和获取操作,再使用Reflect完成原本的操作(get、set)
使用注意点
reactive内部如果接收Ref对象会自动解包(脱ref);Ref赋值给reactive属性 时,也会自动解包;- 值得注意的是,当访问到某个
响应式数组或Map这样的原生集合类型中的ref元素时,不会执行ref的解包。 - 响应式转换是深层的,会影响到所有的嵌套属性,如果只想要浅层的话,只要在前面加
shallow即可(shallowRef、shallowReactive)
4.Composition API
Options API 的问题
- 难以维护:
Vue2中只能固定用data、computed、methods等选项来组织代码,在组件越来越复杂的时候,一个功能相关的属性和方法就会在文件上中下到处都有,很分散,变越来越难维护 - 不清晰的数据来源、命名冲突:
Vue2中虽然可以用minxins来做逻辑的提取复用,但是minxins里的属性和方法名会和组件内部的命名冲突,还有当引入多个minxins的时候,我们使用的属性或方法是来于哪个minxins也不清楚
和 Options API 区别和作用
- 更灵活的代码组织:
Composition API是基于逻辑相关性组织代码的,将零散分布的逻辑组合在一起进行维护,也可以将单独的功能逻辑拆分成单独的文件;提高可读性和可维护性。 - 更好的逻辑复用:解决了过去
Options API中mixins的各种缺点; - 同时兼容
Options API; - 更好的类型推导:
组合式 API主要利用基本的变量和函数,它们本身就是类型友好的。用组合式 API重写的代码可以享受到完整的类型推导
Composition API 命名冲突
在使用组合式API时,可以通过在解构变量时对变量进行重命名来避免相同的键名
5.SFC Composition API语法糖(script setup)
是在单文件组件中使用组合式 API 的编译时语法糖。
- 有了它,我们可以编写更简洁的代码;
- 在添加了
setup的script标签中,定义的变量、函数,均会自动暴露给模板(template)使用,不需要通过return返回 - 引入的组件可以自动注册,不需要通过
components进行注册
setup 生命周期
setup是vue3.x新增的,它是组件内使用Composition API的入口,在组件创建挂载之前就执行;- 由于在执行
setup时尚未创建组件实例,所以在setup选型中没有this,要获取组件实例要用getCurrentInstance() setup中接受的props是响应式的, 当传入新的props时,会及时被更新。
6.Teleport传送门
Teleport是vue3推出的新功能,也就是传送的意思,可以更改dom渲染的位置。
比如日常开发中很多子组件会用到dialog,此时dialog就会被嵌到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。Dialog从用户感知的层面,应该是一个独立的组件,我们可以用<Teleport>包裹Dialog, 此时就建立了一个传送门,传送到任何地方:<teleport to="#footer">
7.Fragments
Fragments 的出现,让 Vue3 一个组件可以有多个根节点(Vue2 一个组件只允许有一个根节点)
- 因为虚拟
DOM是单根树形结构的,patch方法在遍历的时候从根节点开始遍历,这就要求了只有一个根节点; - 而
Vue3允许多个根节点,就是因为引入了Fragment,这是一个抽象的节点,如果发现组件是多根的,就会创建一个Fragment节点,将多根节点作为它的children;
8.watch 与 watchEffect
watch作用是对传入的某个或多个值的变化进行监听;触发时会返回新值和老值;也就是说第一次不会执行,只有变化时才会重新执行watchEffect是传入一个立即执行函数,所以默认第一次也会执行一次;不需要传入监听内容,会自动收集函数内的数据源作为依赖,在依赖变化的时候又会重新执行该函数,如果没有依赖就不会执行;而且不会返回变化前后的新值和老值
watch加Immediate也可以立即执行
9.Vue3 生命周期
- 基本上就是在
Vue2生命周期钩子函数名基础上加了on; beforeDestory和destoryed更名为onBeforeUnmount和onUnmounted;- 然后用
setup代替了两个钩子函数beforeCreate和created; - 新增了两个开发环境用于调试的
钩子,在组件更新时onRenderTracked会跟踪组件里所有变量和方法的变化、每次触发渲染时onRenderTriggered会返回发生变化的新旧值,可以让我们进行有针对性调试;

Vue2 篇
1.Vue2.0 的响应式
原理
简单来说就一句话:
Vue是采用数据劫持结合观察者(发布者-订阅者)模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者(watcher),触发相应的监听回调来更新DOM;
Vue 响应式的创建、更新流程
- 当一个
Vue实例创建时,vue会遍历data选项的属性,用Object.defineProperty为它们设置getter/setter并且在内部追踪相关依赖,在属性被访问和修改时分别调用getter和setter。 - 每个组件实例都有相应的
watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,观察者Wacher自动触发重新render当前组件,生成新的虚拟DOM树 Vue框架会遍历并对比新旧虚拟DOM树中每个节点的差别,并记录下来,最后将所有记录的不同点,局部修改到真实DOM树上。(判断新旧节点的过程在vue2和vue3也有不同)
Vue2 响应式的缺点
Object.defineProperty是可以监听通过数组下标修改数组的操作,通过遍历每个数组元素的方式- 但是
Vue2无法监听,原因是性能代码和用户体验不成正比,其次即使监听了,也监听不了数组的原生方法进行操作; - 出于性能考虑,
Vue2放弃了对数组元素的监听,改为对数组原型上的7种方法进行劫持;
- 但是
Object.defineProperty无法检测直接通过.length改变数组长度的操作;Object.defineProperty只能监听属性,所以需要对对象的每个属性进行遍历,因为如果对象的属性值还是对象,还需要深度遍历。因为这个api并不是劫持对象本身。- 也正是因为
Object.defineProperty只能监听属性而不是对象本身,所以对象新增的属性没有响应式;因此新增响应式对象的属性时,需要使用Set进行新增; - 不支持
Map、Set等数据结构
Vue2 如何解决数组响应式问题
push、pop、shift、unshift、splice、sort、reverse这七个数组方法,在Vue2内部重写了所以可以监听到,除此之外可以使用 set()方法,Vue.set()对于数组的处理其实就是调用了splice方法
v-model 双向绑定原理
v-model本质上是语法糖,v-model 默认会解析成名为 value的 prop 和名为 input 的事件。这种语法糖的方式是典型的双向绑定;
2.Vue 渲染过程
模版 编译原理 & 流程
- 解析
template模板,生成ast语法树,再使用ast语法树生成render函数字符串,编译流程如下:- 解析阶段:使用大量的
正则表达式对template字符串进行解析,转化为抽象语法树AST。 - 优化阶段:遍历
AST,找到其中的一些静态节点并进行标记,方便在进行diff比较时,直接跳过这一些静态节点,优化性能 - 生成阶段: 将最终的
AST转化为render函数
- 解析阶段:使用大量的
视图 渲染更新流程
- 监听数据的变化,当数据发生变化时,
Render函数执行生成vnode对象 - 对比新旧
VNode对象,通过Diff算法(双端比较)生成真实DOM;
Vue runtime-compiler 与 runtime-only
(1) runtime-compiler的步骤
template--> ast--> render函数 --> VDom--> 真实DOM
(2) runtime-only的步骤
render函数 --> VDom--> 真实DOM
不过 runtime-only 版本的体积较小。但是无法使用 template 选项
渲染流程图

3.VirtualDOM & Diff算法
虚拟DOM 的产生和本质
- 由于在浏览器中操作
DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。使用虚拟DOM可以减少直接操作DOM的次数,减少浏览器的重绘及回流 Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点。是对真实DOM的一层抽象Virtual DOM映射到真实DOM要经历VNode的create、diff、patch等阶段
虚拟DOM 的作用
- 将真实元素节点抽象成
VNode,有效减少直接操作dom次数,从而提高程序性能 - 方便实现跨平台:可以使用虚拟
DOM去针对不同平台进行渲染;
Diff算法 实现原理
- 首先,对比新旧节点(
VNode)本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换; - 如果为相同节点,就要判断如何对该节点的子节点进行处理,这里有四种情况:
旧节点有子节点,新节点没有子节点,就直接删除旧节点的子节点;旧节点没有子节点,新节点有子节点,就将新节点的子节点添加到旧节点上;新旧节点都没有子节点,就判断是否有文本节点进行对比;新旧节点都有子节点,就进行双端比较;(值得一提的是)
Diff算法 的执行时机
Vue 中 Diff算法 执行的时刻是组件更新的时候,更新函数会再次执行 render 函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。
DIFF算法为什么是 O(n) 复杂度而不是 O(n^3)
- 正常
Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n^3)降低至O(n)。
Vue2 Diff算法 双端比较 原理
使用了四个指针,分别指向新旧两个 VNode 的头尾,它们不断的往中间移动,当处理完所有 VNode 时停止,每次移动都要比较 头头、头尾 排列组合共4次对比,来去寻找 key 相同的可复用的节点来进行移动复用;
Vue3 Diff算法 最长递增子序列
vue3 为了尽可能的减少移动,采用 贪心 + 二分查找 去找最长递增子序列;
4.Vue 中的 Key
Key 的作用
key主要是为了更高效的更新虚拟DOM:它会告诉diff算法,在更改前后它们是同一个DOM节点,这样在diff新旧vnodes时更高效。- 如果不使用
key,它默认使用“就地复用”的策略。而使用key时,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素。
- 如果不使用
- 它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:
- 完整地触发组件的生命周期钩子
- 触发过渡(给
transition内的元素加上key,通过改变key来触发过度)
- 在
Vue源码的判断中,Diff时去判断两个节点是否相同时主要判断两者的key和元素类型(tag),因此如果不设置key,它的值就是undefined;
什么是就地复用 & 就地更新
当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
使用 key 的注意点
- 有相同父元素的子元素必须有
独特的key。重复的key会造成渲染错误。 v-for循环中尽量不要使用index作为key值
为什么不建议使用 index 作为 key 值
因为在数组中key的值会跟随数组发生改变(比如在数组中添加或删除元素、排序),而key值改变,diff算法就无法得知在更改前后它们是同一个DOM节点。会出现渲染问题。
用 index 作为 key 值带来问题的例子
v-for渲染三个输入框,用index作为key值,删除第二项,发现在视图上显示被删除的实际上是第三项,因为原本的key是1,2,3,删除后key为1,2,所以3被认为删除了
5.Vue2 生命周期
总共分为 8 个阶段:创建前/后,载入前/后,更新前/后,销毁前/后。
各阶段的使用场景
beforeCreate:执行一些初始化任务,此时获取不到props或者data中的数据created:组件初始化完毕,可以访问各种数据,获取接口数据等beforeMount:此时开始创建VDOMmounted:dom已创建渲染,可用于获取访问数据和dom元素;访问子组件等。beforeUpdate:此时view层还未更新,可用于获取更新前各种状态updated:完成view层的更新,更新后,所有状态已是最新beforeDestroy:实例被销毁前调用,可用于一些定时器或订阅的取消destroyed:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器keep-alive独有的生命周期,分别为activated和deactivated。用keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated钩子函数,命中缓存渲染后会执行actived钩子函数。
DOM 渲染在哪个周期中就已经完成
mounted
注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
父子组件 生命周期 顺序
创建过程自上而下,挂载过程自下而上
- 加载渲染过程
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
- 子组件更新过程
父 beforeUpdate-> 子 beforeUpdate-> 子updated -> 父 updated
- 父组件更新过程
父 beforeUpdate-> 父 updated
- 销毁过程
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
生命周期钩子是如何实现的
Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会依次执行对应的钩子方法(发布)
6.Computed 和 Watch
两者的区别
computed是计算一个新的属性,并将该属性挂载到Vue实例上,而watch是监听已经存在且已挂载到Vue示例上的数据,调用对应的方法。computed计算属性的本质是一个惰性求值的观察者computed watcher,具有缓存性,只有当依赖变化后,第一次访问computed属性,才会计算新的值- 从使用场景上说,
computed适用一个数据被多个数据影响,而watch适用一个数据影响多个数据;
数据放在 computed 和 methods 的区别
computed内定义的视为一个变量;而methods内定义的是函数,必须加括号();- 在依赖数据不变的情况下,
computed内的值只在初始化的时候计算一次,之后就直接返回结果;而methods内调用的每次都会重写计算。
Computed 的实现原理
computed 本质是一个惰性求值的观察者computed watcher。其内部通过this.dirty 属性标记计算属性是否需要重新求值。
- 当
computed的依赖状态发生改变时,就会通知这个惰性的watcher,computed watcher通过this.dep.subs.length判断有没有订阅者, - 有订阅者就是重新计算结果判断是否有变化,变化则重新渲染。
- 没有的话,仅仅把
this.dirty = true(当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备lazy(懒计算)特性。)
Watch的 实现原理
Watch 的本质也是一个观察者 watcher,监听到值的变化就执行回调;
watch的初始化在data初始化之后,此时的data已经通过Object.defineProperty设置成了响应式;watch的key会在Watcher里进行值的读取,也就是立即执行get获取value,此时如果有immediate属性就立马执行watch对应的回调函数;- 当
data对应的key发生变化时,触发回调函数的执行;
7.Vue 组件
组件通信方式
props:常用于父组件向子组件传送数据,子组件不能直接修改父组件传递的props,props.async:实现父组件子组件传递的数据双向绑定,子组件可以直接修改父组件传递的propsv-model:本质上是语法糖,v-model默认会解析成名为value的prop和名为input的事件。这种语法糖的方式是典型的双向绑定ref:父组件可以通过ref获取子组件的属性以及调用子组件方法;$emit / $on:子组件通过派发事件的方式给父组件数据,父组件监听;EventBus:无论什么层级的组件都可以通过它进行通信;Vuex:$children / $parent:$attrs / $listeners:$attrs包含了父作用域中不作为prop的值;$listeners包含了负作用域中的v-on监听器;
父子组件通信方式
props、$emit、$parent 或者 $children、$refs调用子组件的方法传值。还可以使用语法糖 v-model 来直接实现;
兄弟组件通信 和 跨多层次组件通信
业务中直接使用 Vuex 或者 EventBus;
单向数据流 是什么意思
- 数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。
- 主要是避免子组件修改父组件的状态出现应用数据流混乱的状态,维持父子组件正常的数据依赖关系。
- 如果实在要改变父组件的
prop值 可以再data里面定义一个变量 并用prop的值初始化它 之后用$emit通知父组件去修改
vue.sync 的作用
vue.sync可以让父子组件的prop进行“双向绑定”,可允许子组件修改父组件传来的prop,父组件中的值也随着变化。
V-model 和 sync 的区别
- 在
Vue2中,v-model只能使用一次,而sync能使用多次; - 在
Vue3中,删除了sync,但是v-model可以以v-model:xxx的形式使用多次;
函数式组件
我们可以将组件标记为 functional,来表示这个组件不需要实例化,无状态,没有生命周期
优点
- 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
- 函数式组件结构比较简单,代码结构更清晰
8.Vue Set() 方法
什么情况要用 set()
在两种情况下修改数据 Vue 是不会触发视图更新的
- 在实例创建之后添加新的属性到实例上(给响应式对象新增属性)
- 直接更改数组下标来修改数组的值
set() 的原理
- 目标是对象,就用
defineReactive给新增的属性去添加getter和setter; - 目标是数组,就直接调用数组本身的
splice方法去触发响应式
9.Vue.use 插件机制
概述
Vue是支持插件的,可以使用Vue.use来安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。install方法调用时,会将Vue作为参数传入。- 该方法需要在调用
new Vue()之前被调用。 - 当
install方法被同一个插件多次调用,插件将只会被安装一次。
原理
Vue.use的原理其实不复杂,它的功能主要就是两点:安装Vue插件、已安装插件不会重复安装;
- 先声明一个数组,用来存放安装过的插件,如果已安装就不重复安装;
- 然后判断
plugin是不是对象,如果是对象就判断对象的install是不是一个方法,如果是就将参数传入并执行install方法,完成插件的安装; - 如果
plugin是一个方法,就直接执行; - 最后将
plugin推入上述声明的数组中,表示插件已经安装; - 最后返回
Vue实例
10.Vuex
Vuex是一个专为 Vue.js 应用程序开发的状态管理模式。但是无法持久化,页面刷新即消失;
Vuex 的核心概念
state:是vuex的数据存放地;state里面存放的数据是响应式的,vue组件从store读取数据,若是store中的数据发生改变,依赖这相数据的组件也会发生更新;它通过mapState把全局的state和getters映射到当前组件的computed计算属性getter:可以对 state 进行计算操作;mutation:用来更改Vuex中store的 状态action:类似于mutation,但不同于action提交的是mutation,而不是直接变更state,且action可以包含异步操作;module: 面对复杂的应用程序,当管理的状态比较多时;我们需要将vuex的store对象分割成模块(modules)。
mutation 和 action 的区别
- 修改
state顺序,先触发Action,Action再触发Mutation。 mutation专注于修改state,理论上要是修改state的唯一途径,而action可以处理业务代码和异步请求等mutation必须同步执行,而action可以异步;
mutation 同步的意义
同步的意义在于每一个 mutaion 执行完成后都可以对应到一个新的状态,这样 devtools 就可以打一个快照下来;
模块 和 命名空间 的作用
模块化:
- 如果使用单一状态树,应用的所有状态会集中到一个比较大的对象。所以
Vuex允许我们将store分割成模块(module)。 - 每个模块拥有自己的
state、mutation、action、getter、甚至是嵌套子模块。
命名空间:
- 默认情况下,模块内部的
action、mutation和getter是注册在全局命名空间的,这样使得多个模块能够对同一mutation或action作出响应。 - 如果希望你的模块具有更高的封装度和复用性,你可以通过添加
namespaced: true的方式使其成为带命名空间的模块。 - 当模块被注册后,它的所有
getter、action及mutation都会自动根据模块注册的路径调整命名。
11.keep-alive
- 用
keep-alive包裹动态组件时,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。 keep-alive的中还运用了LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰。
实现原理
- 在
vue的生命周期中,用keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated钩子函数,命中缓存渲染后会执行actived钩子函数。
两个属性 include / exclude
include- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
两个生命周期 activated / deactivated
用来得知当前组件是否处于活跃状态。
- 当
keep-alive中的组件被点击时,activated生命周期函数被激活执行一次,切换到其它组件时,deactivated被激活。 - 如果没有
keep-alive包裹,没有办法触发activated生命周期函数。
LRU 算法
LRU算法 就是维护一个队列;- 当新数据来时,将新数据插入到尾部;
- 当缓存命中时,也将数据移动到尾部;
- 当队列满了,就把头部的数据丢弃;

12. NextTick
$nextTick可以让我们在下次DOM更新结束之后执行回调,用于获得更新后的DOM;使用场景在于响应式数据变化后想获取DOM更新后的情况;
NextTick 的原理
$nextTick本质是对事件循环原理的一种应用,它主要使用了宏任务和微任务,采用微任务优先的方式去执行nextTick包装的方法;- 并且根据不同环境,为了
兼容性做了很多降级处理: 2.6版本中的降级处理:Promise>MutationObserver>setImmediate>setTimeout- 因为
Vue是异步更新的,NextTick就在更新DOM的微任务队列后追加了我们自己的回调函数
- 因为
Vue 的异步更新策略原理
Vue的DOM更新是异步的,当数据变化时,Vue就会开启一个队列,然后把在同一个事件循环中观察到数据变化的watcher推送进这个队列;- 同时如果这个
watcher被触发多次,只会被推送到队列一次; - 而在下一个
事件循环时,Vue会清空这个队列,并进行必要的DOM更新; - 这也就是响应式的数据
for循环改变了100次视图也只更新一次的原因;
为什么
Vue采用异步渲染呢:
Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染
Vue.nextTick 和 vm.$nextTick 区别
- 两者都是在
DOM更新之后执行回调; - 然而
vm.$nextTick回调的this自动绑定到调用它的实例上;
NextTick 的版本迭代变化
Vue在2.4版本、2.5版本和2.6版本中对于nextTick进行反复变动,原因是浏览器对于微任务的不兼容性影响、微任务和宏任务各自优缺点的权衡。
2.4的整体优先级:Promise>MutationObserver>setTimeout2.5的整体优先级:Promise>setImmediate>MessageChannel>setTimeout2.6的整体优先级:Promise>MutationObserver>setImmediate>setTimeout
2.4版本的 NextTick
在 Vue 2.4 和之前都优先使用 microtasks,但是 microtasks的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks又可能会出现渲染的性能问题。
2.4的$nexttick是采用微任务,兼容降级宏任务,但是由于微任务的优先级太高了,执行的比较快,会导致一个问题:在连续事件发生的期间(比如冒泡事件),微任务就已经执行了,所以会导致事件不断的被触发;但是如果全部都改成 macroTask,对一些有重绘和动画的场景也会有性能的影响。
2.5版本的 NextTick
2.5的$nexttick一样是采用微任务,兼容降级宏任务,然后暴露出了一个withMacroTask方法:用于处理一些 DOM 交互事件,如 v-on 绑定的事件回调函数的处理,会强制走 macrotask。
但是这样又引出了一些其余问题,在vue2.6里的注释是说:
- 在重绘之前状态发生改变会有轻微的问题;也就是
css定义@``media查询,window监听了resize事件,触发事件是需要的是state变化,样式也要变化,但是是宏任务,就产生了问题。 - 而且使用
macrotasks在任务队列中会有几个特别奇怪的行为没办法避免。有些时候由于使用macroTask处理DOM操作,会使得有些时候触发和执行之间间隔太大
2.6版本的 NextTick
2.6的&nexttick由于以上问题,又回到了在任何地方优先使用microtasks的方案。
13.v-for 与 v-if
两者优先级问题
- 在
Vue2中,v-for的优先级高于v-if,放在一起会先执行循环再判断条件;如果两者同时出现的话,会带来性能方面的浪费(每次都会先循环渲染再进行条件判断),所以编码的时候不应该将它俩放在一起; - 再
Vue3中,v-if的优先级高于v-for;因为v-if先执行,此时v-for未执行,所以如果使用v-for定义的变量就会报错;
解决同时使用的问题
- 如果条件出现在循环内部,我们可以提前过滤掉不需要
v-for循环的数据 - 条件在循环外部,
v-for的外面新增一个模板标签template,在template上使用v-if
14.Vue-router 路由
前端路由的本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新页面。
hash模式 和 history模式
hash模式:在浏览器中符号#以及#后面的字符称之为hash,用window.location.hash读取; 特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
$route 和 $router 的区别?
$route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。
$router是'路由实例'对象包括了路由的跳转方法,钩子函数等。
路由钩子函数
全局守卫:beforeEach进入路由之前、beforeResolve、afterEach进入路由之后路由独享守卫:beforeEnter路由组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
路由跳转
vue-router导航有两种方式:声明式导航和编程式导航声明式跳转就是使用router-link组件,添加:to=属性的方式编程式跳转就是router.push
路由传参
- 使用
query方法传入的参数使用this.$route.query接受 - 使用
params方式传入的参数使用this.$route.params接受 - 如果不仅仅考虑用路由的话,还可以用
vuex、localstorage等
动态路由
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: "/user/:id", component: User },
],
});
15.Vue.extend
有时候由于业务需要我们可能要去动态的生成一个组件并且要独立渲染到其它元素节点中,这时它们就派上了用场;
extend 原理
Vue.extend作用是扩展组件生成一个构造器,它接受一个组件对象,使用原型继承的方法返回了Vue的子类,并且把传入组件的options和父类的options进行了合并;通常会与$mount一起使用。- 所以,我们使用
extend可以将组件转为构造函数,在实例化这个这个构造函数后,就会得到组件的真实Dom,这个时候我们就可以使用$mount去挂载到DOM上;
使用示例:
<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
16.Vue.mixin
在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,可以通过 Vue 的 mixin 功能抽离公共的业务逻辑
mixin 和 mixins 区别
app.mixin用于全局混入,会影响到每个组件实例mixins用于多组件抽离。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过mixins混入代码。- 另外需要注意的是
mixins混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并,具体可以阅读 文档。
17.Vue style scoped
scoped可以让css的样式只在当前组件生效
scoped的原理?
vue-loader构建时会动态给 scoped css 块与相应的 template 标签加上随机哈希串 data-v-xxx
如何实现样式穿透
scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性
- 使用
/deep/ - 使用两个
style标签
18.Vue 自定义指令
概述
除了内置的 v-model、v-show指令之外,Vue还允许注册自定义指令;
可以用来做 权限控制、按钮防抖节流、图片按需加载等;
自定义指令的钩子函数(生命周期)
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。componentUpdated:被绑定元素所在模板完成一次更新周期时调用。unbind:只调用一次,指令与元素解绑时调用。
Vue3 指令的钩子函数
created: 已经创建出元素,但在绑定元素attributes之前触发beforeMount:元素被插入到页面上之前mounted:父元素以及父元素下的所有子元素都插入到页面之后beforeUpdate: 绑定元素的父组件更新前调用updated:在绑定元素的父组件及他自己的所有子节点都更新后调用beforeUnmount:绑定元素的父组件卸载前调用unmounted:绑定元素的父组件卸载后- 指令回调中传递四个参数:
- 绑定指令的节点元素
- 绑定值,里面包含表达式值、装饰符、参数等
- 当前
vnode值 - 变更前的
vnode值
V-once 的作用
v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。- 使用场景为我们已知一些组件不需要更新
原理:
- 编译器发现元素上面有
v-once时,会将首次计算结果存入缓存,组件再次渲染时就会从缓存获取,避免再次计算。
如何实现vue自定义指令?
在 bind 和 update 时触发相同行为,而不关心其它的钩子,就简写即可。
Vue.directive('permission', function (el, binding, vnode) {
//在这里检查页面的路由信息和权限表数组的信息是否匹配
const permissionKey = binding.arg;
Vue.nextTick(() => {
if (el.parentNode) el.parentNode.removeChild(el);
});
});
19.杂问题
为什么 vue 中 data 必须是一个函数?
- 对象是引用类型,如果
data是一个对象,当重用组件时,都指向同一个data,会互相影响; - 而使用返回对象的函数,由于每次返回的都是一个新对象,引用地址不同,则不会出现这个问题。
data 什么时候可以使用对象
当我们使用 new Vue() 的方式的时候,无论我们将 data 设置为对象还是函数都是可以的,因为 new Vue() 的方式是生成一个根组件,该组件不会复用,也就不存在共享 data 的情况了
vue-loader 是什么?使用它的用途有哪些?
- 是用于处理单文件组件的
webpack-loader,有了它之后,我们可以把代码分割为<template>、<script>和<style>,代码会异常清晰 webpack打包时,会以loader的方式调用vue-loadervue-loader被执行时,它会对SFC中的每个语言块用单独的loader处理。最后将这些单独的块装配成最终的组件模块。
Vue2.7 向后兼容的内容
composition APISFC <script setup>SFC CSS v-bind
Vue delete 原理
- 先判断是否为数组,如果是数组就调用
splice - 然后判断
target对象有这个属性的话,就delete删除这个属性; - 还要判断是否是响应式的,如果是就需要通知视图更新
vue中的 ref 是什么?
ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。
new Vue() 做了什么
- 合并配置
- 初始化生命周期
- 初始化事件
- 初始化
render函数 - 调用
beforecreate钩子函数 - 初始化
state,包括data、props、computed - 调用
created钩子函数 - 然后按照生命周期,调用
vm.$mount挂载渲染;
Vite 篇
什么是 Vite
Vite是新一代的前端构建工具
Vite 核心原理
Vite其核心原理是利用浏览器现在已经支持ES6的import,碰见import就会发送一个HTTP请求去加载文件。Vite启动一个koa服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回给浏览器。Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack开发编译速度快出许多!
它具有以下特点:
- 快速的冷启动:采用
No Bundle和esbuild预构建,速度远快于Webpack - 高效的热更新:基于
ESM实现,同时利用HTTP头来加速整个页面的重新加载,增加缓存策略:源码模块使用协商缓存,依赖模块使用强缓;因此一旦被缓存它们将不需要再次请求。 - 基于
Rollup打包:生产环境下由于esbuild对css和代码分割并使用Rollup进行打包;
基于 ESM 的 Dev server
- 在
Vite出来之前,传统的打包工具如Webpack是先解析依赖、打包构建再启动开发服务器,Dev Server必须等待所有模块构建完成后才能启动,当我们修改了bundle模块中的一个子模块, 整个bundle文件都会重新打包然后输出。项目应用越大,启动时间越长。
- 而
Vite利用浏览器对ESM的支持,当import模块时,浏览器就会下载被导入的模块。先启动开发服务器,当代码执行到模块加载时再请求对应模块的文件,本质上实现了动态加载。
基于 ESM 的 HMR 热更新
所有的 HMR 原理:
目前所有的打包工具实现热更新的思路都大同小异:主要是通过WebSocket创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。
Vite 的表现:
Vite监听文件系统的变更,只用对发生变更的模块重新加载,这样HMR更新速度就不会因为应用体积的增加而变慢
- 而
Webpack还要经历一次打包构建。
- 所以
HMR场景下,Vite表现也要好于Webpack。
基于 Esbuild 的依赖预编译优化
Vite预编译之后,将文件缓存在node_modules/.vite/文件夹下
为什么需要预编译 & 预构建
- 支持
非ESM格式的依赖包:Vite是基于浏览器原生支持ESM的能力实现的,因此必须将commonJs的文件提前处理,转化成ESM模块并缓存入node_modules/.vite - 减少模块和请求数量:
Vite将有许多内部模块的ESM依赖关系转换为单个模块,以提高后续页面加载性能。- 如果不使用
esbuild进行预构建,浏览器每检测到一个import语句就会向服务器发送一个请求,如果一个三方包被分割成很多的文件,这样就会发送很多请求,会触发浏览器并发请求限制;
- 如果不使用
为什么用 Esbuild
Esbuild 打包速度太快了,比类似的工具快10~100倍,
Esbuild 为什么这么快
Esbuild使用Go语言编写,可以直接被转化为机器语言,在启动时直接执行;
- 而其余大多数的打包工具基于
JS实现,是解释型语言,需要边运行边解释;
JS本质上是单线程语言,GO语言天生具有多线程的优势,充分利用CPU资源;
转载自:https://juejin.cn/post/7166446028266733581