1. 聊聊为什么会出现 React、vue 这样的框架,他们的出现解决了什么问题
用户界面越来越复杂,框架采用声明式的写法,将界面的构建和数据的管理分离出来,大大提升开发效率和维护效率。
(1)原生JS实现不太方便。如果需要获取元素实例,需要直接操作dom元素的属性,如果元素被卸载,很有可能由于保留了引用触发内存泄漏
(2)事件绑定可能会出现不同浏览器的兼容问题
(3)代码不好维护,很容易出现一些全局变量
(4)不好做组件化,逻辑和视图的复用,都变得不太容易
框架的事件绑定无需额外操作,代码好维护,且组件化操作使复用很方便。
2. 知道声明式和命令式吗?他们有什么区别
命令式写法:面向过程,关注“如何做”,需要详细描述每一步的实现方式。命令式编程的典型语言包括 C、Java 和 Python 等。
声明式写法:面向结果,关注“做什么”,只需要描述需要完成的任务,而不需要考虑具体的步骤和实现方式。声明式编程的典型语言包括 SQL、CSS 和 XSLT 等。
声明式编程通常更加易于维护,因为它只需要关注需求的变化,而命令式编程需要关注具体的实现方式,因此可能需要更多的维护工作。
声明式:UI = f(data)
用户界面是由数据通过一个函数计算得到的。
它将用户界面的构建和数据的管理分离了开来,使得开发者可以更加专注于数据的处理和业务逻辑的实现,而不需要关心具体的用户界面更新操作。
参照老师做的大模型机器人,自己选一门框架,react or vue,进行实现。
一方面熟悉框架语法,另一方面巩固 css 和 js 的知识,同时思考为什么要有 nextTick,useEffect。
3. vue 的原理是怎么样的?vue2 和 vue3 在底层实现上的区别是什么?
vue 是多个模块组成的有机整体:
通过编译器,将模版编译为具有 render 函数的一个对象,其返回值是组件对应的虚拟 DOM 节点;(声明式原理)
接下来通过渲染器将虚拟 DOM 节点渲染为真实的 DOM 节点;(声明式原理)
通过响应式系统,实现数据变更的检测,通过 diff 算法实现 DOM 节点的更新。(响应式原理)
vue采用响应式原理,本质是在属性被获取时,将副作用函数进行收集。在属性被修改时,依次执行副作用函数。
具体步骤:
首先进行数据劫持,proxy(vue3) 或 defineProperty(vue2) 实现对数据的劫持;
副作用函数执行前,通过设置当前副作用函数,建立数据和副作用函数的依赖关系;
数据被修改时,重新执行对应的副作用函数,便是所谓的响应式
区别: vue2使用 defineProperty 进行数据劫持, vue3使用 proxy 进行数据劫持。
4. 什么是虚拟 DOM,虚拟 DOM 的 diff 算法了解多少?
用 javascript 对象来描述 UI 其实就是所谓的虚拟 DOM.
只做同层级比较,类型不一样直接替换,类型一样进行列表渲染,通过key进行优化。
diff 算法的原理:
(1)简单 diff 算法:算法最为简单易懂,react 使用,但是移动次数上可能是最多
遍历新子节点,找到旧子节点中,key 与其相同的节点,并记录下当前旧子节点中最大的索引,如果后续新子节点在旧子节点中的索引小于该值,则该子节点对应的真实 dom 需要移动。移动的目的地便是新子节点中,上一个节点的后面。
本质就在于,如果新子节点在旧子节点中的索引是递增的,那么证明其顺序并未发生改变,故并不需要移动,而一旦非递增遇到局部最高点,则后面的元素,都需要进行移动。由于移动的位置,始终是前一个元素的后方,所以对于后面的顺序,非递增也好,递增也罢,都能够保证顺序。除非遇到更高点,此时,后面的元素又会依此移动
(2)双端 diff:算法相对复杂,vue2 使用,移动次数上相对可控
每次对列表进行 首首,首尾,尾首,首尾 比较,寻找可复用的节点,从而获得更少的移动次数。
当然,实际的场景中,还存在四次比较都没有找到可复用节点的情况,此时,可以直接在旧子节点中,查找 key 与当前新子节点的 key 相同的节点,找到后将其移动到旧子节点的 oldStartIdx 对应的 dom 节点前,此时更新 newStartIdx + 1,继续此过程即可完成整个diff。
优点就在于,由于进行了首尾比较,所以能更好的处理简单 diff 算法中,最后一个元素在最前导致后面的节点都需要移动,从而移动次数过多的问题。
(3)快速 diff:算法相对复杂,vue3 使用,移动次数最优
首先进行预处理,将队首相同,队尾相同的元素,直接将其分别移动到队首队尾,后续无需移动。
找到新子节点在旧子节点中的位置索引组成一个数组,找到其中的最长递增子序列,这一部分节点,就是不需要移动的节点。
对于剩余节点依此遍历,将其对应的 dom 元素移动到新节点数组中,下一个节点前即可
5. 用数组 index作为key有作用吗?
首先key值是在新旧列表比对时发挥作用的,为了增加元素的复用。
但是一般不建议使用index作为key,没有作用。因为新列表可能不在整个列表最后新增元素,可能在最开始新增,那么index无法辨别旧元素,会全部新建。index永远都是0,1,2,3,……
6. 计算属性和监听器的区别是什么?
计算属性computed(): 主要用于根据已有的响应式数据计算得出一个新的值。只有依赖的数据发生变化,才会重新计算。通常用来处理复杂或者多次被使用的计算逻辑。一般认为是只读的,可以修改,但是非常不推荐。初始化时会计算一次。
监听器watch: 用于监听某个特定数据的变化,并执行相应的回调函数。只要被监听的数据发生变化,回调函数就会执行。适用于执行异步操作或开销较大的操作。可以在回调函数中修改数据。但是返回值没有用初始化的时候不会执行,但是可以通过设置 immediate 为 true 立即执行一次。
如果是需要一个值,那么使用计算属性;如果是要做一件事,那么使用监听器
7. 介绍一下vue的生命周期
初始化选项式 api 或执行 组合式 api: beforeCreated, created
生成虚拟 dom
挂载真实 dom:beforeMount, mounted
更新 dom: beforeUpdate, updated
组件卸载: beforeUnmount, unmounted
Vue实例是通过Vue构造函数创建的对象,包含了数据、方法、计算属性、生命周期钩子等属性和方法。
Vue 挂载是指将 Vue 组件实例附加到 DOM 元素的过程。
8. keepLive是什么?
是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。(切换时保留状态)
<component :is="activeComponent" />
默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
当一个组件实例从 DOM 上移除但因为被 <KeepAlive>
缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
所以就多了两个声明周期:activated
和 deactived
生命周期
组件的实现原理,其实就是内部对动态组件的实例进行了缓存,如果缓存存在,则直接使用上次的组件实例而不是重新创建。
9. 什么是双向绑定?
指数据和视图的双向绑定。一般用在 input 上,v-model来 绑定后,视图改变,数据就会修改,数据修改,视图就会更新。更泛一点来说,就是数据修改,触发视图更新,用户交互,触发事件回调修改数据。
10. MVVM 怎么理解?
Model(模型):
应用的数据和业务逻辑
View(视图):
用户界面
ViewModel(视图模型):
连接视图和模型; 模型数据改变,自动更新视图; 发生交互(click)或视图改变(input)时,触发业务逻辑(事件绑定)或修改应用数据(v-model,背后其实也是事件触发)
背后的原理: 就是前面讲述的响应式的原理,通过数据劫持监听数据的获取和修改,获取时对副作用函数收集,修改时触发副作用函数,从而完成数据修改来自动更新视图。而视图的交互和修改,则通过事件回调来完成数据的修改。
**优点:**视图和数据层分离。极大提升开发效率,代码可维护性
11. 选项式的 api 中,data 为什么是一个函数?直接在 data 上新增属性,会是响应式吗?
组件被多次使用时,通过执行 data 函数,可以保证每次被使用时,返回的永远是 新的引用 ,如果返回一个对象,那么 多次使用共享同一个引用,会导致相互干扰 ,从而带来不可预期的结果。
响应式:数据的变化能够自动更新视图,从而保持数据与视图的一致性。
Vue 2 和 Vue 3 都支持选项式 API,但vue2不支持组合式API。
vue3 中,data 最终会被转换为 proxy 对象,它本身是可以监测到属性的新增和删除的。所以直接新增和删除,还是会是响应式的。
vue2 中,data 是通过 defineProperty 建立响应式的,本身监听不到属性的新增和删除,所以直接新增属性,是不会触发组件渲染,也不是响应式的。可以通过 Vue.set 来设置,将会是响应式的。
12. v-if、v-show有什么区别?
v-if 按条件渲染,如果在初次渲染时条件值为false,则不会做任何事,只有当条件首次变为true时才渲染;而 v-show 无论出事条件如何,始终被渲染,只有CSS display属性会被切换。
所以,v-if 有更高的切换开销,适用于运行时绑定条件很少改变的情况;而v-show有更高的初始渲染开销,适用于频繁切换的情况。
13. v-if、v-for有什么区别?
vue3 中,v-if 优先级更高,所以 v-if 中不能访问 v-for 中定义的变量名。意味着, v-if 只会执行一次。将 v-for 放在外层,v-if 放在内层,可以达到和 vue2 一样的效果。
vue2 中, v-for 优先级更高,所以 v-if 中能够访问到 v-for 中定义的变量名。也意味着对每一项,v-if 都会执行。将 v-if 放在外层,v-for 放在内层,可以达到和 vue3 一样的效果。
但是,vue3 和 vue2 都不建议同时使用,更推荐的做法是,使用计算属性对列表过滤后,v-for 进行循环。
14. nextTick 有什么用?
为了防止多次数据变化引发多次触发组件更新。( nextTick 是等待下一次 DOM 更新刷新的工具方法。)
vue 在数据变化时,会批量处理更新合并他们来提高性能。
具体内部实现是将这些处理合并到微任务队列 中,根据浏览器的兼容性,依此使用 promise.then, mutationObserver, setImmediate, setTimeout 来实现异步批处理。
所以数据修改后, DOM 不会立即更新。需要等待 nextTick 执行后,再进行 DOM 相关的操作
15. slot 的作用是什么?
为了更灵活的再组合组件。
通过子组件接收模版来更灵活的实现组件组合。
子组件中定义插槽位置,父组件提供插槽内容.
同时还提供了 v-slot 来使得在父组件接收子组件的参数
16. 组件间如何通信,都有哪些方案?
(1)父子组件之间通信
props: 父组件向子组件传递数据
emit: 子组件向父组件传递数据
组件ref: 父组件主动获取子组件数据
(2)兄弟组件之间通信
公共数据提取到父组件中,可以避免两者通信
除此之外可以借助发布-订阅者模式实现
(3)祖孙组件之间通信
(4)无直接关系组件之间通信
- props, emit, ref 可以完成基本通信
- 将状态提取到公共父节点,可以完成兄弟组件间的通信
- 使用 eventBus,可以完成任意组件间的通信
- 使用 provide/inject,可以完成组件和子节点的通信(祖孙之间,相隔比较远)
- 使用全局状态管理库,可以解决任意组件间的通信(缺陷:组件不好复用,因为状态依赖全局,其他组件想要使用,还需要拷贝相应的全局状态)
17. 什么是发布订阅,可以自己实现一下吗?
发布订阅模式包括两种类型的对象:发布者和订阅者。发布者是事件的发出者,它维护一个事件列表,并向列表中添加或删除事件。当某个事件发生时,发布者会将这个事件通知给所有订阅者。订阅者则是事件的接收者,它们订阅感兴趣的事件,并在事件发生时接收通知。这种模式有助于实现松耦合的设计,使得对象之间的依赖关系更加灵活,提高了代码的可扩展性和可维护性。
class EventBus {
events = {};
// 订阅
$on(name, fn) {
if (!this.events[name]) {
this.events[name] = new Set();
}
this.events[name].add(fn);
}
// 发布
$emit(name, params) {
if (this.events[name]) {
this.events[name].forEach((event) => {
event(params);
});
}
}
// 取消订阅
$off(name, fn) {
if (this.events[name]) {
this.events[name].delete(fn);
}
}
}
// 应用
const eventBus = new EventBus();
function fn1(params) {
console.log("fn1", params);
}
function fn2(params) {
console.log("fn2", params);
}
eventBus.$on("event1", fn1);
eventBus.$on("event2", fn2);
eventBus.$emit("event1", { a: 1 });
eventBus.$off("event1", fn1);
eventBus.$emit("event1", { a: 1 });
18. vue中逻辑复用的方案有哪些?
(1) mixins (vue3 已经不推荐使用)
提供基础的 data, methods, 生命周期函数等选项,与组件以恰当方式混合后,组件就具备了基础的 data, methods, 生命周期函数。
data, methods 如果出现名称冲突,保留组件的
生命周期钩子全部保留,同时 mixins 的执行顺序靠前
全局混入会影响每一个组件,而局部混入只会影响当前组件。
问题:
多个 mixins 将会难以追踪具体是哪一个在生效;
命名空间冲突;
多个 mixins 可能会相互依赖同一个属性名,存在隐形的耦合.
(2) 组合式函数 (具有状态的逻辑复用)
vue 的自定义 hooks。主要用于逻辑复用。
灵感来自 react 的 hooks,但是得益与 vue 的响应式系统,形式和 react 相似,却比 react 少了很多限制和容易犯错的坑点
(3) 自定义指令 (和 dom 操作相关的逻辑复用)
(4) vue 插件 (对应用实例进行全局增强)