前言
笔者今年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
中只能使用一次;多次绑定需要使用sync
Vue3
用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
不兼容IE11
defineProperty
不支持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
:此时开始创建VDOM
mounted
: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
:实现父组件子组件传递的数据双向绑定,子组件可以直接修改父组件传递的props
v-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
>setTimeout
2.5
的整体优先级:Promise
>setImmediate
>MessageChannel
>setTimeout
2.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-loader
vue-loader
被执行时,它会对SFC
中的每个语言块用单独的loader
处理。最后将这些单独的块装配成最终的组件模块。
Vue2.7 向后兼容的内容
composition API
SFC <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
资源;