「2022」寒冬下我的面试知识点复盘【Vue3、Vue2、Vite】篇

28,070 阅读35分钟

前言

笔者今年2022寒冬下成功跳槽了阿里,这篇文章就是将自己面试的一些准备知识总结分享出来~

如果这篇文章对你有用,请一键三连(点赞评论+收藏)让更多的同学看到

如果需要转载,请评论区留言,未经允许请不要私自转载

防杠声明

这篇文章不是纯堆砌面试题,而是以知识总结为主,主观观点和主观总结居多里面总结的知识点在我这次的面试中也不全都有用到~如果有写错的地方欢迎评论区提出,如果只是要那请右上角X掉慢走;

传送门

这个专栏预计要做以下这些内容,可以根据自己的需要跳转查看

面经「2022面经」:2年前端拿下字节阿里offer总结

专栏2022寒冬下我的面试知识点复盘:

「2022」寒冬下我的面试知识点复盘【浏览器原理】篇

「2022」寒冬下我的面试知识点复盘【计算机网络】篇

「2022」寒冬下我的面试知识点复盘【JS】篇(加紧编写中)

「2022」寒冬下我的面试知识点复盘【CSS】篇

「2022」寒冬下我的面试知识点复盘【Vue3、Vue2、Vite】篇

「2022」寒冬下我的面试知识点复盘【工程化】篇(加紧编写中)

「2022」寒冬下我的面试知识点复盘【Nodejs】篇(加紧编写中)

「2022」寒冬下我的面试知识点复盘【TypeScript】篇(加紧编写中)

本文标题思维导图

Vue.png

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 列表不会变化的就缓存,简单说就是用内存换时间
在 框架 设计层面:
  • 代码打包体积更小:许多VueAPI可以被Tree-Shaking,因为使用了es6moduletree-shaking 依赖于 es6模块的静态结构特性;
  • 响应式 的优化:用 Proxy 代替 Object.defineProperty,可以监听到数组下标变化,及对象新增属性,因为监听的不是对象属性,而是对象本身,还可拦截 applyhas 等方法;
  • 虚拟DOM的优化:保存静态节点直接复用(静态提升)、以及添加更新类型标记patchflag)(动态绑定的元素)
    • 静态提升:静态提升就是不参与更新的静态节点,只会创建一次,在之后每次渲染的时候会不停的被复用;
    • 更新类型标记:在对比VNode的时候,只对比带有更新类型标记的节点,大大减少了对比Vnode时需要遍历的节点数量;还可以通过 flag 的信息得知当前节点需要对比的内容类型;
    • 优化的效果Vue3的渲染效率不再和模板大小成正比,而是与模板中的动态节点数量成正比;
  • Diff算法 的优化:Diff算法 使用 最长递增子序列 优化了对比流程,使得 虚拟DOM 生成速度提升 200%
在 兼容性 方面:
  • Vue3 不兼容 IE11,因为IE11不兼容Proxy
其余 特点
  • v-if的优先级高于v-for,不会再出现vue2v-forv-if混用问题;
  • vue3v-model可以以v-model:xxx的形式使用多次,而vue2中只能使用一次;多次绑定需要使用sync
  • Vue3TS 编写,使得对外暴露的 api 更容易结合 TypeScript

2.Vue3 响应式

Vue3 响应式的特点
  • 众所周知 Vue2 数据响应式是通过 Object.defineProperty() 劫持各个属性 getset,在数据变化时发布消息给订阅者,触发相应的监听回调,而这个API存在很多问题;
  • Vue3 中为了解决这些问题,使用 Proxy结合Reflect代替Object.defineProperty
    • 支持监听对象数组的变化,
    • 对象嵌套属性只代理第一层,运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能取得很大进步;
    • 并且能拦截对象13种方法,动态属性增删都可以拦截,新增数据结构全部支持,
  • Vue3 提供了 refreactive 两个API来实现响应式;
什么是Proxy

ProxyES6中的方法,Proxy用于创建一个目标对象的代理,在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象;

defineProperty 和 Proxy 的区别
  • Object.definePropertyEs5 的方法,ProxyEs6 的方法
  • defineProperty 是劫持对象属性,Proxy 是代理整个对象;
  • defineProperty 监听对象和数组时,需要迭代对象的每个属性;
  • defineProperty 不能监听到对象新增属性,Proxy 可以
  • defineProperty 不兼容 IE8Proxy 不兼容 IE11
  • defineProperty 不支持 MapSet 等数据结构
  • defineProperty 只能监听 getset,而 Proxy 可以拦截多达13种方法;
  • Proxy 兼容性相对较差,且无法通过 pollyfill 解决;所以Vue3不支持IE
为什么需要 Reflect
  • 使用 Reflect 可以修正 Proxythis指向问题;
  • 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 对数组实现代理时,也对数组原型上的一些方法进行了重写;


原因:

  • 比如使用 pushpopshiftunshiftsplice这些方法操作响应式数组对象时,会隐式地访问和修改数组的length属性,所以我们需要让这些方法间接读取length属性时禁止进行依赖追踪;

  • 还比如使用 includesindexOf 等对数组元素进行查找时,可能是使用代理对象进查找,也可能使用原始值进行查找,所以就需要重写查找方法,让查找时先去响应式对象中查找,没找到再去原始值中查找;
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 做的处理
  • Vue3Map、Set做了很多特殊处理,这是因为Proxy无法直接拦截 Set、Map,因为 Set、Map的方法必须得在它们自己身上调用;Proxy 返回的是代理对象;
  • 所以 Vue3 在这里的处理是,封装了 toRaw() 方法返回原对象,通过Proxy的拦截,在调用诸如 setadd方法时,在原对象身上调用方法;

其实还有一个方法是,用Class搞一个子类去继承 SetMap,然后用子类new的对象就可以通过proxy来代理,而Vue没有采用此方法的原因,猜测是:calss只兼容到 Edge13

3.Ref 和 Reactive 定义响应式数据

  • vue2 中, 定义数据都是在data中, 而vue3中对响应式数据的声明,可以使用 refreactivereactive的参数必须是对象,而ref可以处理基本数据类型对象
  • refJS中读值要加.value,可以用isRef判断是否ref对象,reactive不能改变本身,但可以改变内部的值
  • 模板中访问从 setup 返回的 ref 时,会自动解包;因此无须再在模板中为它写 .value
  • Vue3区分 refreactive 的原因就是 Proxy 无法对原始值进行代理,所以需要一层对象作为包裹;
Ref 原理
  • ref内部封装一个RefImpl类,并设置get/set,当通过.value调用时就会触发劫持,从而实现响应式。
  • 当接收的是对象或者数组时,内部仍然是 reactive 去实现一个响应式;
Reactive 原理
  • reactive内部使用Proxy代理传入的对象,从而实现响应式。
  • 使用 Proxy 拦截数据的更新和获取操作,再使用 Reflect 完成原本的操作(getset
使用注意点
  • reactive内部如果接收Ref对象会自动解包脱ref);
  • Ref 赋值给 reactive 属性 时,也会自动解包;
  • 值得注意的是,当访问到某个响应式数组Map这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。
  • 响应式转换是深层的,会影响到所有的嵌套属性,如果只想要浅层的话,只要在前面加shallow即可(shallowRefshallowReactive

4.Composition API

Options API 的问题
  • 难以维护Vue2 中只能固定用 datacomputedmethods 等选项来组织代码,在组件越来越复杂的时候,一个功能相关的属性方法就会在文件上中下到处都有,很分散,变越来越难维护
  • 不清晰的数据来源、命名冲突Vue2 中虽然可以用 minxins 来做逻辑的提取复用,但是 minxins里的属性和方法名会和组件内部的命名冲突,还有当引入多个 minxins 的时候,我们使用的属性或方法是来于哪个 minxins 也不清楚
和 Options API 区别和作用
  • 更灵活的代码组织Composition API 是基于逻辑相关性组织代码的,将零散分布的逻辑组合在一起进行维护,也可以将单独的功能逻辑拆分成单独的文件;提高可读性和可维护性。
  • 更好的逻辑复用:解决了过去 Options APImixins 的各种缺点;
  • 同时兼容Options API
  • 更好的类型推导组合式 API主要利用基本的变量和函数,它们本身就是类型友好的。用组合式 API重写的代码可以享受到完整的类型推导
Composition API 命名冲突

在使用组合式API时,可以通过在解构变量时对变量进行重命名来避免相同的键名

5.SFC Composition API语法糖(script setup)

是在单文件组件中使用组合式 API 的编译时语法糖。

  • 有了它,我们可以编写更简洁的代码;
  • 在添加了setupscript标签中,定义的变量、函数,均会自动暴露给模板(template)使用,不需要通过return返回
  • 引入的组件可以自动注册,不需要通过components 进行注册
setup 生命周期
  • setupvue3.x新增的,它是组件内使用Composition API的入口,在组件创建挂载之前就执行;
  • 由于在执行setup时尚未创建组件实例,所以在setup选型中没有this,要获取组件实例要用getCurrentInstance()
  • setup中接受的props是响应式的, 当传入新的props时,会及时被更新。

6.Teleport传送门

Teleportvue3推出的新功能,也就是传送的意思,可以更改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 是传入一个立即执行函数,所以默认第一次也会执行一次;不需要传入监听内容,会自动收集函数内的数据源作为依赖,在依赖变化的时候又会重新执行该函数,如果没有依赖就不会执行;而且不会返回变化前后的新值和老值

watchImmediate也可以立即执行

9.Vue3 生命周期

  • 基本上就是在 Vue2 生命周期钩子函数名基础上加了 on
  • beforeDestorydestoryed 更名为 onBeforeUnmountonUnmounted
  • 然后用 setup 代替了两个钩子函数 beforeCreatecreated
  • 新增了两个开发环境用于调试的钩子,在组件更新时 onRenderTracked 会跟踪组件里所有变量和方法的变化、每次触发渲染时 onRenderTriggered 会返回发生变化的新旧值,可以让我们进行有针对性调试;

image.png

Vue2 篇

1.Vue2.0 的响应式

原理

简单来说就一句话:

  • Vue 是采用数据劫持结合观察者发布者-订阅者)模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者(watcher),触发相应的监听回调来更新DOM
Vue 响应式的创建、更新流程
  • 当一个 Vue 实例创建时,vue 会遍历 data 选项的属性,用 Object.defineProperty 为它们设置 getter/setter 并且在内部追踪相关依赖,在属性被访问和修改时分别调用 gettersetter
  • 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,观察者 Wacher 自动触发重新 render 当前组件,生成新的虚拟 DOM
  • Vue 框架会遍历并对比新旧虚拟 DOM 树中每个节点的差别,并记录下来,最后将所有记录的不同点,局部修改到真实 DOM 树上。(判断新旧节点的过程在vue2vue3也有不同)
Vue2 响应式的缺点
  • Object.defineProperty 是可以监听通过数组下标修改数组的操作,通过遍历每个数组元素的方式
    • 但是 Vue2 无法监听,原因是性能代码和用户体验不成正比,其次即使监听了,也监听不了数组的原生方法进行操作;
    • 出于性能考虑,Vue2 放弃了对数组元素的监听,改为对数组原型上的 7 种方法进行劫持;
  • Object.defineProperty 无法检测直接通过 .length 改变数组长度的操作;
  • Object.defineProperty 只能监听属性,所以需要对对象的每个属性进行遍历,因为如果对象的属性值还是对象,还需要深度遍历。因为这个api并不是劫持对象本身。
  • 也正是因为 Object.defineProperty 只能监听属性而不是对象本身,所以对象新增的属性没有响应式;因此新增响应式对象的属性时,需要使用 Set 进行新增;
  • 不支持 MapSet 等数据结构
Vue2 如何解决数组响应式问题

pushpopshiftunshiftsplicesortreverse这七个数组方法,在Vue2内部重写了所以可以监听到,除此之外可以使用 set()方法,Vue.set()对于数组的处理其实就是调用了splice方法

v-model 双向绑定原理

v-model本质上是语法糖,v-model 默认会解析成名为 valueprop 和名为 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要经历VNodecreatediffpatch等阶段
虚拟DOM 的作用
  • 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
  • 方便实现跨平台:可以使用虚拟DOM去针对不同平台进行渲染;
Diff算法 实现原理
  • 首先,对比新旧节点(VNode)本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换;
  • 如果为相同节点,就要判断如何对该节点的子节点进行处理,这里有四种情况:
    • 旧节点子节点新节点没有子节点,就直接删除旧节点子节点
    • 旧节点没有子节点新节点子节点,就将新节点子节点添加到旧节点上;
    • 新旧节点都没有子节点,就判断是否有文本节点进行对比;
    • 新旧节点都有子节点,就进行双端比较;(值得一提的是)
Diff算法 的执行时机

VueDiff算法 执行的时刻是组件更新的时候,更新函数会再次执行 render 函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。

DIFF算法为什么是 O(n) 复杂度而不是 O(n^3)
  • 正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以VueDiff进行了优化,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从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
什么是就地复用 & 就地更新

Vue2-就地更新

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值,删除第二项,发现在视图上显示被删除的实际上是第三项,因为原本的key1,2,3,删除后key1,2,所以3被认为删除了

5.Vue2 生命周期

总共分为 8 个阶段:创建前/后,载入前/后,更新前/后,销毁前/后。

各阶段的使用场景
  • beforeCreate:执行一些初始化任务,此时获取不到 props 或者 data 中的数据
  • created:组件初始化完毕,可以访问各种数据,获取接口数据等
  • beforeMount:此时开始创建 VDOM
  • mounteddom已创建渲染,可用于获取访问数据和dom元素;访问子组件等。
  • beforeUpdate:此时view层还未更新,可用于获取更新前各种状态
  • updated:完成view层的更新,更新后,所有状态已是最新
  • beforeDestroy:实例被销毁前调用,可用于一些定时器或订阅的取消
  • destroyed:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
  • keep-alive 独有的生命周期,分别为 activateddeactivated。用 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 设置成了响应式;
  • watchkey 会在 Watcher 里进行值的读取,也就是立即执行 get 获取 value,此时如果有 immediate属性就立马执行 watch 对应的回调函数;
  • data 对应的 key 发生变化时,触发回调函数的执行;

7.Vue 组件

组件通信方式
  • props:常用于父组件向子组件传送数据,子组件不能直接修改父组件传递的 props
  • props.async:实现父组件子组件传递的数据双向绑定,子组件可以直接修改父组件传递的 props
  • v-model:本质上是语法糖,v-model 默认会解析成名为 valueprop 和名为 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 给新增的属性去添加 gettersetter
  • 目标是数组,就直接调用数组本身的 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 把全局的 stategetters 映射到当前组件的 computed 计算属性
  • getter:可以对 state 进行计算操作;
  • mutation:用来更改 Vuexstore 的 状态
  • action:类似于 mutation ,但不同于 action 提交的是 mutation,而不是直接变更 state,且 action 可以包含异步操作;
  • module: 面对复杂的应用程序,当管理的状态比较多时;我们需要将vuexstore对象分割成模块(modules)。
mutation 和 action 的区别
  • 修改state顺序,先触发ActionAction再触发Mutation
  • mutation 专注于修改 state,理论上要是修改 state 的唯一途径,而action可以处理业务代码和异步请求等
  • mutation 必须同步执行,而 action 可以异步;
mutation 同步的意义

同步的意义在于每一个 mutaion 执行完成后都可以对应到一个新的状态,这样 devtools 就可以打一个快照下来;

模块 和 命名空间 的作用

模块化:

  • 如果使用单一状态树,应用的所有状态会集中到一个比较大的对象。所以 Vuex 允许我们将 store 分割成模块(module)。
  • 每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块。

命名空间:

  • 默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的,这样使得多个模块能够对同一 mutationaction 作出响应。
  • 如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。
  • 当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名。

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算法 就是维护一个队列;
  • 当新数据来时,将新数据插入到尾部;
  • 当缓存命中时,也将数据移动到尾部;
  • 当队列满了,就把头部的数据丢弃;

image.png

12. NextTick

  • $nextTick 可以让我们在下次 DOM 更新结束之后执行回调,用于获得更新后的 DOM
  • 使用场景在于响应式数据变化后想获取DOM更新后的情况;
NextTick 的原理
  • $nextTick 本质是对事件循环原理的一种应用,它主要使用了宏任务微任务,采用微任务优先的方式去执行 nextTick 包装的方法;
  • 并且根据不同环境,为了兼容性做了很多降级处理
  • 2.6版本中的降级处理Promise > MutationObserver > setImmediate > setTimeout
    • 因为 Vue是异步更新的,NextTick 就在更新DOM微任务队列后追加了我们自己的回调函数
Vue 的异步更新策略原理
  • VueDOM 更新是异步的,当数据变化时,Vue 就会开启一个队列,然后把在同一个 事件循环 中观察到数据变化的 watcher 推送进这个队列;
  • 同时如果这个 watcher 被触发多次,只会被推送到队列一次;
  • 而在下一个 事件循环 时,Vue会清空这个队列,并进行必要的 DOM 更新;
  • 这也就是响应式的数据 for 循环改变了100次视图也只更新一次的原因;

为什么Vue采用异步渲染呢:

Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染

Vue.nextTick 和 vm.$nextTick 区别
  • 两者都是在 DOM 更新之后执行回调;
  • 然而 vm.$nextTick 回调的 this 自动绑定到调用它的实例上;
NextTick 的版本迭代变化

Vue2.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进入路由之前、beforeResolveafterEach进入路由之后
  • 路由独享守卫beforeEnter
  • 路由组件内的守卫beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave
路由跳转
  • vue-router导航有两种方式:声明式导航编程式导航
  • 声明式跳转就是使用 router-link 组件,添加 :to= 属性的方式
  • 编程式跳转就是 router.push
路由传参
  • 使用query方法传入的参数使用this.$route.query接受
  • 使用params方式传入的参数使用this.$route.params接受
  • 如果不仅仅考虑用路由的话,还可以用vuexlocalstorage
动态路由

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 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

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,可以通过 Vuemixin 功能抽离公共的业务逻辑

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-modelv-show指令之外,Vue还允许注册自定义指令

可以用来做 权限控制按钮防抖节流图片按需加载等

自定义指令的钩子函数(生命周期)
  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
  • componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
  • unbind:只调用一次,指令与元素解绑时调用。
Vue3 指令的钩子函数
  • created: 已经创建出元素,但在绑定元素 attributes之前触发
  • beforeMount:元素被插入到页面上之前
  • mounted:父元素以及父元素下的所有子元素都插入到页面之后
  • beforeUpdate: 绑定元素的父组件更新前调用
  • updated:在绑定元素的父组件及他自己的所有子节点都更新后调用
  • beforeUnmount:绑定元素的父组件卸载前调用
  • unmounted:绑定元素的父组件卸载后
  • 指令回调中传递四个参数:
    • 绑定指令的节点元素
    • 绑定值,里面包含表达式值、装饰符、参数等
    • 当前 vnode
    • 变更前的 vnode
V-once 的作用
  • v-oncevue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。
  • 使用场景为我们已知一些组件不需要更新

原理:

  • 编译器发现元素上面有v-once时,会将首次计算结果存入缓存,组件再次渲染时就会从缓存获取,避免再次计算。
如何实现vue自定义指令?

bindupdate 时触发相同行为,而不关心其它的钩子,就简写即可。

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,包括datapropscomputed
  • 调用 created 钩子函数
  • 然后按照生命周期,调用 vm.$mount 挂载渲染;

Vite 篇

什么是 Vite

Vite是新一代的前端构建工具

Vite 核心原理

  • Vite其核心原理是利用浏览器现在已经支持ES6import,碰见import就会发送一个HTTP请求去加载文件。
  • Vite启动一个 koa 服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回给浏览器。
  • Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack开发编译速度快出许多!

它具有以下特点:

  • 快速的冷启动:采用No Bundleesbuild预构建,速度远快于Webpack
  • 高效的热更新:基于ESM实现,同时利用HTTP头来加速整个页面的重新加载,增加缓存策略:源码模块使用协商缓存,依赖模块使用强缓;因此一旦被缓存它们将不需要再次请求。
  • 基于 Rollup 打包:生产环境下由于esbuildcss代码分割并使用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 资源;