目录
4.Vue-Router的Hash模式和History模式区别
前言
Vue篇
1.Vue3中的Diff
1.1Diff是什么
Diff是一个对比函数,它是一个用来比较两个JavaScript对象(虚拟DOM)差异的函数。
具体来说吗,它比较的是"旧的虚拟DOM树 "和"新的虚拟DOM树 ",它对比结果不是简单的"true / false ",而是一系列的"操作指令 ",它的目标是:"尽可能复用节点,尽可能少移动DOM"
具体算法过程如下:
假设场景:
- 旧列表(Old):(A,B),C,D,E,(F,G)
- 新列表(New):(A,B),E,C,D,H,(F,G)
首先,Vue会执行一个"掐头去尾"的操作,系统生成两个指针(头指针 和尾指针),分别从列表头尾向中间遍历
从头开始比:
- 指针i从0开始比
- Old[0]是A,New[0]是A,相同不操作,i++
- Old[1]是B,New[1]是B,相同不操作,i++
- Old[2]是C,New[2]是E,不相同,指针停止
从尾开始比:
- Old[-1]是G,New[-1]是G,相同不操作,i++
- Old[-2]是F,New[-2]是F,相同不操作,i++
- Old[-3]是E,New[-3]是H,相同不操作,指针停止
此时,头部(A,B)和尾部(F,G)已经处理完毕,直接去掉,剩下的乱序部分是:
- Old剩余:[C,D,E]
- New剩余:[E,C,D,H]
其次,掐头去尾后,发现一种特殊情况,就直接处理:
- 新列表比旧列表长:如果旧的遍历完了,新的还有剩余的,说明剩下的都是新增的,直接插入
- 旧列表比新列表长:如果新的遍历完了,旧的还有剩,说明剩下的都是不需要的,直接删入
(此时我们的列表既有新增又有移动,所以)
最后,Diff需要处理将[C,D,E]变成[E,C,D,H]的过程:
- 遍历旧列表[C,D,E]:C、D、E在Map中都找得到,记下位置,同时生成一个新数组用来记录"新列表中的节点在旧列表中的位置",newArray[5,3,4,0]
- 计算最长递增子序列:现在我们有一个位置数组[5,3,4,0],Vue需要找出哪一串节点的位置顺序是没变的,这样这串节点就不用移动,最后找到[3,4]的最长递增子序列是[3,4],这样它们两个不需要移动
- 移动节点:Vue倒序遍历新列表的乱序不分,H是0表示新增,创建并插入H,E是5,将E移动到C前面
总结:
Diff算法就是在内存中,用最快的速度找出新旧两个虚拟DOM树之间有什么不同
1.2为什么要用Diff
使用Diff算法的主要原因只有一个:"性能好"
假设没有Diff时,你修改了列表中的一个商品价格,浏览器会执行下面三个步骤:
- 操作:浏览器清空当前的整个列表DOM
- 重建:浏览器根据新数据,重新创建所有商品的DOM元素
- 插入:把所有新元素塞回页面
这样做性能很差,会让页面卡顿、闪烁。
使用Diff算法后,浏览器会执行下面几个步骤:
- 生成:Vue在内存中生成一个新的虚拟DOM
- 对比:算法发现某个商品的价格改变
- 更新:Vue只去操作真实DOM中的那一个文本节点,其它部分完全不动
这样做性能最好
1.3Diff用在哪里
Diff算法并不是时时刻刻都在运行的,它只发生在"组件更新"时,当响应式数据(ref、reactive)发生变化,出发了组件的重新渲染时,Diff算法才会使用
Diff算法在Vue的patch()函数中,这个函数属于Vue的核心渲染源码
Vue的更新粒度是组件级别的,数据变化只会触发该数据所属组件的Diff
2.为什么v-for中的key不建议使用index
v-for 根据key来判断节点是否是同一个,如果使用index作为key会产生如下后果:
- 当在数组中插入或删除数据时,所有的节点index都会改变
- Vue会以为这些节点全部被改变了,所以会导致整个节点列表重新渲染,造成性能浪费
- 除此之外,Vue还可能让节点错误的原地复用,比如输入框中的残留内容被错误的渲染到其它节点中
所以在实践中,推荐使用数据库里的唯一ID作为key值
3.computed和watch的区别
| 特性 | computed | watch |
|---|---|---|
| 侧重点 | 属性是什么 | 要做什么事情 |
| 缓存 | 有缓存,依赖不变时,直接返回缓存值,不执行函数 | 无缓存,每次变化都会执行回调 |
| 执行时机 | 同步,在渲染或读取时计算 | 异步 / 同步,当数据变化时触发 |
| 适用场景 | 生成一个新数据 | 执行一些操作 |
| 返回值 | 必须有返回值 | 不需要返回值 |
总结:
- computed是为了得到一个新值
- wacth是为了数据变化时处理一些附加操作
4.nextTick是什么
nextTick是Vue的一个方法,它接受一个回调函数,将这个回调函数插入到微任务队列中
Vue的DOM更新是异步的,当有多个响应式数据修改后,Vue只记录最后一次修改,当没有数据修改后,DOM更新在微任务中完成,此时nextTick的回调在DOM更新的后面
5.Vue3的编译优化有哪些
Vue3的性能提升表现在编译器在编译阶段就可以分析将文件分类成:"静态文件 "和"动态文件"
随后编译器对动态文件执行以下操作:
- 对动态文件打上数字标记
- 标记分为三类:"只有文字会变的文件"、"只有类名会变的文件"、"只有样式会变的文件"
编译器对静态文件执行以下操作:
- 对文件静态提升,编译器将它们的创建代码提升到渲染函数之外
- Vue3只全局创建一次,渲染时直接复用同一个变量
编译器对事件侦听创建缓存,比如@click="handleClick"这个事件侦听,Vue3会生成一个内联函数包裹它并将它缓存,避免了不必要的组件更新
6.v-if和v-for的优先级
在Vue3中v-if大于v-for,在Vue2中v-for大于v-if
禁止v-if和v-for写到一个节点上,因为if先执行,此时for还没有遍历,所以在v-if里访问不到v-for的变量,会报错
如果v-if的变量确实依赖于v-for里面的变量,那么有两种解决方案:
第一种,使用computed计算变量,在JS层筛选数据:
javascript
<script setup>
import { ref, computed } from 'vue'
const list = ref([
{ id: 1, name: 'A', isShow: true },
{ id: 2, name: 'B', isShow: false },
{ id: 3, name: 'C', isShow: true }
])
// 1. JS 层过滤,生成一个新的数组
const visibleList = computed(() => {
return list.value.filter(item => item.isShow)
})
</script>
<template>
<ul>
<li v-for="item in visibleList" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
第二种,使用<template>标签将v-for提到上层:
javascript
<template>
<ul>
<template v-for="item in list" :key="item.id">
<li v-if="item.isShow">
{{ item.name }}
</li>
</template>
</ul>
</template>
Vue生态篇
1.父子组件的生命周期执行顺序
一个原则:"由外向内初始化,由内向外挂载"
挂载阶段的执行顺序:
- 父组件setup
- 父组件onBeforeMount
- 子组件setup
- 子组件onBeforeMount
- 子组件onMounted
- 父组件onMounted
更新阶段(父组件传递给子组件的props更新或父组件重新渲染时)的执行顺序:
- 父组件onBeforeUpdate
- 子组件onObeforeUpdate
- 子组件onUpdated
- 父组件onUpdated
卸载 / 销毁阶段的执行顺序:
- 父组件onBeforeUnmount
- 子组件onBeforeUnmount
- 子组件onBeforeUnmounted
- 父组件onUnmounted
总结:"总是以父组件开始,再以父组件结束"
2.组件通信有哪些方式
- Props:父传子
- $attrs:父传子
- Emits:子传父
- Ref / Expose:子传父
- v-mode:双向绑定
- Provide / Inject:祖先传子孙
- Pinia、Vuex库:全局组件
- EventBus / mitt:全局组件
3.keep-alive是什么
keep-alive是Vue的一个内置组件:
- 作用:缓存组件实例,避免组件在切换时被销毁和重建
- 场景:Tab切换、多页面跳转
- 生命周期:被缓存的组件可以使用两个生命周期钩子:"onActivated "(激活时)和"onDeactivated"(停用时)
原理是LRU缓存算法:
- keep-alive有一个max属性,限制缓存数量
- 当设置缓存max为2,并且缓存了[A,B]两个组件时
- 访问A命中,A变成"最近使用的",顺序变为[B,A]
- 访问新组件C时,缓存已满,算法会淘汰最久没用的B,存入C,序列变成[A,C]
4.Vue-Router的Hash模式和History模式区别
| 特点 | Hash模式 | History模式 |
|---|---|---|
| URL格式 | 带#号 | 不带#号,干净好看 |
| 底层原理 | 基于window.onhashchange事件 | 基于HTML5 History API |
| 请求行为 | #后面的内容不会发送给服务器 | 完整的URL会发送给服务器 |
| 兼容性 | 极好 | 需要IE10+ |
| 部署配置 | 不需要服务器特殊配置 | 必须配置服务器(Nginx / Apache) |
History模式不管什么URL都会向服务器发送请求,例如当访问一个"site.com/home"URL,Vue会向服务器请求/home这个文件,如果服务器没有这个文件,就会报404。此时就需要服务器配置try_files,不管请求什么路径,如果找不到这个文件,统统返回index.html
5.MVVM和MVC是什么
MVVM:
- M(Model):数据模型(后端传来的JSON数据)
- V(View):视图(HTML页面)
- VM (ViewModel):视图模型(Vue实例),负责协调M和V,VM可以自动同步M和V的状态
MVC:
- M(Model):数据模型
- V(View):视图(HTML页面)
- C(Controller):负责协调M和V,Controller需要手动修改M或者V,不能自动更新、同步M和V的状态
6.Pinia和Vuex的区别
Pinia是Vue官方推荐的"下一代Vuex",它有如下优点:
- 更简单的API(去掉了Mutation):Pinia只有state、Getter、Action,直接在Action里修改State即可
- TypeScript支持更好
- PInia可以创建多个独立的Store,它们之间是独立的,按需引入
- Pinia体积比Vuex更小
代码手写篇
1.简单复现Vue里的reactive
javascript
const handler = {
get(target, key, receiver) {
console.log("正在读取" , key);
const result = Reflect.get(target, key, receiver);
return typeof result === 'object' && result !== null ? reactive(result) : result;
},
set(target, key, value, receiver) {
console.log("正在设置" , key);
const result = Reflect.set(target, key, value, receiver);
return result;
}
}
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
return new Proxy(target, handler);
}
const user = reactive({
name: "张三",
age: 18
});
const company = reactive({
name: "Tech Corp",
info: {
address: {
city: "Beijing",
street: "Road 1"
},
employees: 100
}
});
console.log("--- 深度测试 ---");
const myCity = company.info.address.city;
console.log("--- 修改深度属性 ---");
company.info.employees = 200;
结果:
