什么是 MVVM
MVVM 就特么是
你跟前端代码对喷的时候,把视图(View)跟数据(Model)这两个憨批强行分家 ,中间塞了个叫 ViewModel 的和事佬。
- Model(数据):就一闷骚仓库,存数据的,屁话不多,但谁都得找它要货。
- View(界面) :就你那花里胡哨的页面,整天叭叭地装逼,但自己不动脑子,数据全管 ViewModel 叫爹。
- ViewModel :究极打工人!把 Model 的数据拎过来,加工成 View 能直接啃的饲料,还特么自动同步------View 改个输入,Model 立马跟着变,省得你手动怼代码怼到脑淤血。
简单说:
以前写代码像谈恋爱,两头传话累成狗 ,现在 MVVM 像找了个中介,你只管吼"老子要改数据",剩下的它全给你摆平,界面跟数据自动 sync,你再手写 DOM 操作就特么是铁憨憨!
总结 :别瞎几把直接操作界面,让 ViewModel 当舔狗去同步,你躺平就完事了!
(附赠人话:MVVM 是一种前端架构模式,通过数据绑定实现视图与数据的自动同步,减少手动操作 DOM 的繁琐工作。)
什么是 VDOM 它和 DOM 有什么关系
VDOM 就是个"影分身舔狗"!
DOM 是浏览器里真身爷,动一下就要触发重排重绘,慢得像老牛拉破车。
VDOM 是 JS 里用对象模拟的假人替身, lightweight 到飞起,随便你怎么折腾都不卡。
它俩关系就像:
- 你写代码改数据 → 先拿 VDOM(替身) 疯狂乱改(反正不花钱)。
- 改完一通 → VDOM 和上次的替身搞"找不同"(Diff 算法),找出哪儿变了。
- 最后只把变的地方 去真实 DOM(真身) 上打个小补丁(Patch),完事!
总结:别动不动就怼真实 DOM!拿 VDOM 这个替身先演练,改完只同步必要的,省得浏览器累成狗你页面卡成屎!
(人话:VDOM 是 JS 对象模拟的 DOM 树,通过差异化比对最小化更新真实 DOM,提升渲染性能。)
Vue 组件初始化的各个阶段都做了什么?
阶段一:beforeCreate------ 毛都没有,穷得叮当响
这时候组件就是个光杆司令 ,data、method、props全特么是 undefined!
能干的只有注册点全局事件,但基本属于"要啥没啥,别来沾边"。
阶段二:created------ 数据有了,但还™是空气人
data、methods、props都初始化好了,数据能读写,能调方法!
但模板还没挂载到 DOM 上,页面连个影都没有,操作 DOM 就是做梦。
阶段三:beforeMount------ 准备"夺舍"真实DOM
模板编译好了,VDOM 也生成好了,但还没塞进页面。
这时候最后的机会改数据不会触发更新,一挂上去就开始打工了。
阶段四:mounted------ 老子正式上线了!
VDOM 被打包扔进真实 DOM,页面终于能看到组件了!
可以随便操作 DOM、调接口、玩第三方库,但别在这儿乱改数据,小心递归更新爆栈!
阶段五:beforeUpdate------ 数据变了,准备"重画脸"
数据更新触发重新渲染,VDOM 重新生成,但还没怼到页面上。
适合在更新前获取当前 DOM 状态(比如滚动条位置)。
阶段六:updated------ 脸画完了,但可能是个鬼脸
VDOM 对比完差异,真实 DOM 也更新了。
能操作更新后的 DOM ,但别在这儿改数据,不然容易进"更新死循环地狱"!
阶段七:beforeDestroy------ 准备卷铺盖滚蛋
组件马上要被卸磨杀驴了,但还™能正常用。
赶紧清定时器、解绑事件、取消请求,不然内存泄漏坑死你!
阶段八:destroyed------ 骨灰都扬了
组件实例被扬了,子组件也全没了,但事件监听还在冒泡(Vue 2 的坑)。
这时候只剩废墟,啥也干不了, farewell 吧您!
总结:
created拿数据,mounted怼 DOM,beforeUpdate改前刹一脚,beforeDestroy清垃圾别留屎! 其他生命周期?看情况加戏,别™瞎写!
(人话:Vue 组件初始化经历创建、挂载、更新、销毁等阶段,各阶段提供钩子函数以便开发者介入不同时机。)
Vue 如何实现双向数据绑定
Vue的双向绑定 = 数据劫持 + 发布订阅 + 指令装逼
核心就仨玩意儿:Observer、Dep、Watcher,一套组合拳打到DOM叫爸爸。
第一步:数据劫持 ------ 当个偷窥狂魔
Vue用 Object.defineProperty(Vue 2)或 Proxy(Vue 3)把data里每个属性都变成老六,谁读谁写都™被监控。
你改 this.msg = "祖安",Vue就在背后阴笑:"小样,老子看到了!"
第二步:收集依赖 ------ 建个"舔狗名单"
每个被监控的属性都有个 Dep(依赖收集器),专门记着"谁用了我"。
模板里用到 {``{ msg }}?Dep 就偷偷把这里记在小本本上,等会儿通知它。
第三步:派发更新 ------ 群发"你爹变了"
你一改数据,Dep 就翻出小本本群发短信给所有Watcher:"你关注的爹更新了,赶紧给老子重渲染!"
Watcher 一收到,立马逼组件重新生成VDOM → Diff → 更新真实DOM。
第四步:v-model 双向绑定的骚操作
v-model就™是个语法糖,拆开是:
:value把数据怼到 input 上(Model → View)@input监听输入,数据反向更新(View → Model) 等于两头都安了监听器,数据一改,两边自动同步,不用你手动又取值又设值。
总结:
劫持数据 + 监听变动 + 群发通知 + 自动同步 = 双向绑定
你只管改数据,Vue 当舔狗帮你更新 DOM;你只管输入,Vue 当苦力帮你更新数据。别™再手动操作了,懂?
(人话:Vue通过数据劫持+观察者模式实现数据变化自动更新视图,通过v-model指令实现视图输入自动更新数据。)
Vue 模板编译的过程
第一步:模板变AST ------ 从HTML到抽象语法树
你写的 <div>{``{ message }}</div>在Vue眼里就是一坨字符串。
Vue用解析器 把这坨字符串拆成AST(抽象语法树),变成JS能懂的树形结构对象,相当于给HTML拍X光,骨头缝都看清楚。
第二步:优化AST ------ 给静态节点贴"免死金牌"
Vue在AST里揪出永远不会变的静态节点 (比如纯<div>祖安人</div>),给它们打上static: true标记。
以后更新时直接跳过这些憨批,Diff算法不理它们,性能直接起飞!
第三步:生成渲染函数 ------ AST变可执行代码
把优化后的AST变成渲染函数代码字符串,大概长这样:
with(this){ return _c('div',[_v(_s(message))]) }
_c是创建元素,_v是创建文本,_s是转字符串 ------ 全是Vue的内部工具人函数。
第四步:生成VNode ------ 执行渲染函数,生成虚拟DOM
执行上一步的渲染函数,返回VNode(虚拟DOM节点),这玩意儿就是轻量级的DOM描述对象。
之后Diff和Patch就用这堆VNode去折腾,不动真实DOM,省得浏览器累出屁。
总结流程:
模板字符串 → 解析成AST → 优化静态节点 → 生成渲染函数 → 执行得VNode
编译就干一件事:把你写的模板变成能生成虚拟DOM的函数,顺便给静态节点开绿灯!
附加暴论:
- 开发阶段 :Vue用运行时+编译时,能编译模板(慢点但方便)。
- 生产阶段 :直接用
vue-loader提前编译好,用户不用带编译器,省了30%体积! - 写
template和直接写render函数区别 :template要被编译,render函数直接跳过编译步骤,性能一毛一样,但template对憨憨更友好。
人话总结:Vue模板编译就是把HTML模板字符串转换成可生成虚拟DOM的渲染函数,过程中优化静态节点以提升性能。
Vue 响应式原理
Vue响应式 = 数据劫持 + 依赖收集 + 派发更新
核心就三贱客:Observer、Dep、Watcher,一套丝滑小连招让数据变,视图自动变。
第一步:数据劫持 ------ 给每个数据装监控
Vue 2 用 Object.defineProperty,Vue 3 用 Proxy,把你data里的对象每个属性都变成老六。
- Getter :谁读取这个属性,就被记到小本本(依赖收集)。
- Setter :谁修改这个属性,就触发通知 (派发更新)。 数组方法也被重写(push/pop等),不然监听不到变化。
第二步:依赖收集 ------ 建"谁用了我"名单
每个被监控的属性都有个 Dep(依赖收集器) ,相当于跟踪狂小本本。
- 模板里用
{``{ user.name }}?渲染时触发Getter,Dep 就把当前 Watcher 记到本本上。 - 一个属性可能被多个地方使用(模板、计算属性、watch),本本上就有多个 Watcher。
第三步:Watcher ------ 苦力打工人
Watcher 分三种:
- 渲染Watcher:组件渲染时创建,负责更新视图。
- 计算属性Watcher:计算属性依赖变化时重新计算。
- 用户Watcher :
watch选项里你写的监听回调。 Watcher 就是接到Dep通知后,去执行对应更新的打工人。
第四步:派发更新 ------ 改数据就群发通知
你改 this.user.name = '祖安人':
- 触发 Setter,Dep 拿出小本本。
- 通知所有 Watcher:"你们关注的爹变了!"
- Watcher 去排队更新(异步队列,避免重复渲染)。
- 最终重新渲染 → 生成VNode → Diff → 更新DOM。
第五步:数组的监听骚操作
Vue 2 里数组下标修改监听不到(vm.items[0] = 'xxx'不行),所以重写了7个数组方法(push、pop、splice等),调用这些方法才能触发更新。
Vue 3 用 Proxy 直接搞定,随便你怎么操数组都行。
总结流程:
劫持数据 → 读取时记依赖 → 修改时通知 → 异步更新视图
你只管改数据,Vue 当舔狗帮你更新页面,再手动操作 DOM 就是憨批!
附加暴论:
- Vue 2 的缺陷 :
Object.defineProperty不能监听新增/删除属性(得用$set/$delete),数组下标修改不行。 - Vue 3 的进步 :
Proxy直接监听整个对象,数组随便操,性能还更好。 - 为什么异步更新:避免你一口气改10次数据,视图更新10次,合并成一次更新,性能炸裂!
人话总结:Vue通过数据劫持监听数据变化,在获取数据时收集依赖,在修改数据时通知依赖更新,从而实现数据驱动视图的响应式系统。
vue中为何 v-for 需要使用 key
v-for不用key的后果:
Vue更新列表时一脸懵逼,不知道哪个元素对应哪个数据,只能暴力对比+就地复用,结果就是:
- 性能拉胯:疯狂移动DOM元素,重排重绘卡成狗。
- 状态错乱:比如勾选的复选框、输入框内容,元素顺序一变全™乱了。
key的作用 ------ 给每个元素发身份证
key就是个唯一标识,Vue通过它知道:
- 哪个旧元素对应哪个新数据,能精准复用已有DOM。
- 哪个元素被删了/新增了,只更新必要的,不瞎折腾。
举个祖安例子:
你有个列表 ['张三', '李四', '王五'],渲染成三个<li>。
如果你删了'张三',没key的话Vue可能:
- 删掉第三个
<li>(王五) - 把前两个
<li>内容改成'李四'和'王五' - 顺序没变,但DOM被瞎移动了 ,性能喂狗。 有key的话,Vue直接精准删除第一个
<li>,其他原地不动,舒服!
用index当key?纯属脑瘫!
v-for="(item, index) in list" :key="index"看似省事,实属憨批:
- 列表顺序一变(增删、排序),index全对不上,Vue又懵逼了,原地复用全乱套。
- 有表单输入或组件状态时,数据错位到亲妈都不认识。
总结:
用key → Vue精准识别元素 → 高效复用DOM,性能起飞状态不乱。
不用key → Vue暴力对比 → 性能吃屎,状态错乱,bug满天飞。
key要用唯一值(如id),别用index,用了等于没用还倒贴bug!
人话总结:key帮助Vue在列表更新时高效跟踪每个节点的身份,从而重用和重新排序现有元素,避免不必要的DOM操作,同时维持组件内部状态正确。
Vue diff 算法的过程
Diff算法就干一件事:新旧VNode对比,找出最小修改,精准更新真实DOM
核心思想:能复用就复用,尽量不移动,实在不行再删了重做
第一步:同层级比较,不跨级对比
Vue的diff只在同一层级对比,不会跨级比较,复杂度直接从O(n³)降到O(n)。
发现不同就直接拆了重建,不浪费时间递归对比,暴力但高效。
第二步:头尾双指针,四步怼穿
新旧节点数组各俩指针(头头、尾尾、头尾、尾头),四种情况按顺序怼:
- 头头相同:直接复用,头指针都右移。
- 尾尾相同:直接复用,尾指针都左移。
- 头尾相同:把旧节点尾巴移到前面,头指针右移,尾指针左移。
- 尾头相同:把旧节点头部移到最后,尾指针左移,头指针右移。
这四步能快速处理简单顺序变化,比如列表反转、头尾移动。
第三步:乱序处理 ------ 上key能救命,没key就等死
如果上面四步都不匹配,有key:
- 用key建旧节点索引表,快速找到可复用节点。
- 移动已有节点到正确位置,实在找不到就新建。
- 最后删掉多余的旧节点。
没key:Vue直接摆烂,就地更新文本内容,节点顺序可能全乱,状态错乱bug起飞。
第四步:更新节点
找到可复用节点后,对比属性/子节点,最小化更新(比如只改class,不动结构)。
总结策略:
1. 同层比较,跨级滚蛋
2. 头尾指针,四种情况快速匹配
3. 有key用哈希暴打,没key就原地摆烂
4. 能复用就复用,尽量少移动,实在不行再删了重做
附加暴论:
- 为什么key重要:没key时Vue只能按位置对比,节点一调顺序就全乱套,有key才能精准跟踪。
- 为什么只同层比较:跨级对比复杂度爆炸,现实中跨级复用场景少,不值得。
- 为什么是O(n):双指针+哈希,把暴力比对优化到线性复杂度。
人话总结:Vue的Diff算法通过同层级比较、双指针快速匹配、基于key的复用策略,最小化DOM操作,提升更新性能。
Vue3 diff 算法做了哪些优化?
Vue3 Diff核心就一句话:老子不瞎™对比了!
Vue2的Diff是无脑全量对比 ,Vue3的Diff是带脑子局部暴击,优化全在刀尖上。
优化1:静态提升 ------ 不动的节点直接当祖宗供起来
编译时就把纯静态节点拎出来,在渲染函数外面生成一次,后面直接复用。
结果:每次更新跳过这些憨批节点,diff时当空气,性能飙升。
优化2:补丁标志 ------ 给动态节点贴标签
编译时分析模板,给动态节点打上patchFlag标记,比如:
1:文本动态2:class动态4:style动态8:props动态
diff时只看有标记的地方,其他部分直接跳过,对比量砍掉80%。
优化3:事件缓存 ------ 事件函数不重新生成
内联事件处理器(@click="handleClick")会被缓存,下次更新直接复用。
Vue2每次渲染都™是新函数,Vue3直接缓存成_cache,内存和性能双杀。
优化4:块树追踪 ------ 靶向治疗
引入block概念,把动态节点打包成一个块(block),更新时只diff块内节点,跳过静态兄弟。
比如v-if/v-for包裹的内容各自成块,精准打击变化区域。
优化5:diff策略升级 ------ 最长递增子序列暴打移动
处理乱序列表时,Vue3用最长递增子序列算法找最少移动方案。
举例 :旧[a,b,c,d,e]→ 新[a,d,c,b,e],Vue2可能移动3次,Vue3用LIS发现[a,c,e]不动,只移动d和b,移动次数最少化。
对比Vue2:
| 对比项 | Vue2 Diff | Vue3 Diff |
|---|---|---|
| 静态节点 | 每次对比 | 提升到外部,不参与diff |
| 动态追踪 | 全量对比 | 补丁标志,只比动态部分 |
| 事件处理 | 每次重新创建 | 缓存复用 |
| 列表更新 | 双指针+无脑移动 | 最长递增子序列,移动最少 |
| 块优化 | 无 | 块树追踪,跳过静态块 |
附加暴论:
- 为什么Vue3快:不动的部分不对比,动的部分精准对比,移动方案最优。
- 组合式API加持:响应式用Proxy,依赖追踪更细,diff需要对比的节点更少。
- 编译时优化:大部分优化在编译阶段完成,运行时躺赢。
人话总结:Vue3通过静态提升、补丁标志、事件缓存、块树追踪、最长递增子序列等优化,大幅减少Diff过程中需要对比的节点数量和移动操作,性能暴打Vue2。
Vue diff 算法和 React diff 算法的区别
React Diff:一根筋递归,能复用就复用,不行就重建
核心策略:
- 同类型节点对比,不同类型直接拆了重建。
- 列表用key,没key就按index暴力对比,性能吃屎。
- 递归深度优先,整个树从头到尾怼一遍。
结果:
- 简单场景还行,复杂列表更新可能憨批移动。
- 没key时顺序一变,组件状态全乱,bug满天飞。
Vue Diff:带脑子双指针,靶向治疗
核心优化:
- 同层双指针四步怼(头头、尾尾、头尾、尾头),快速处理头尾移动。
- key必须用,没key就摆烂,但Vue官方往死里强调用key。
- Vue3再加buff:静态提升、补丁标志、最长递增子序列,精准暴击。
结果:
- 列表更新移动更少,性能更猛。
- Vue3编译时优化,运行时直接躺赢。
对比表:
| 对比点 | React Diff | Vue Diff |
|---|---|---|
| 策略核心 | 递归深度优先,暴力但简单 | 同层双指针+key哈希,带优化脑子 |
| 列表处理 | 没key按index瞎对比,有key好点但优化有限 | 双指针快速匹配,Vue3用最长递增子序列移动最少 |
| 移动逻辑 | 找到可复用节点后,可能无脑移动多余节点 | 精准计算最少移动,尽量不浪费DOM操作 |
| 优化策略 | 运行时优化为主,编译时干预少 | 编译时狂做优化(静态提升、补丁标志等) |
| 哲学差异 | 拥抱重建("大不了重渲染") | 尽量复用("能不动就不动") |
具体例子:
列表 [A,B,C,D]变成 [D,A,B,C]
- React:可能移动A、B、C三个节点(发现D可复用,插到前面)。
- Vue:头尾指针发现D匹配,一次移动搞定,其他不动。
附加暴论:
- React憨在 :默认策略简单,复杂列表依赖开发者手动优化(
shouldComponentUpdate、React.memo)。 - Vue骚在:编译时把能优化的全做了,开发者躺平就行。
- 性能真相:小应用差别不大,复杂列表Vue3暴打React,但React Fiber可中断渲染更适合超大应用。
人话总结:
React Diff简单递归,列表优化依赖key和开发者手动优化;Vue Diff同层双指针+编译时优化,列表更新更高效,移动更少。两者都强,但Vue3在Diff优化上更激进。
简述 Vue 组件异步更新的过程
Vue异步更新的核心就一句:改数据不立马更新,先攒着一波带走!
你一口气改10个数据,Vue不会渲染10次,而是扔到队列里,下一帧统一更新,防止你手贱把页面搞崩。
第一步:触发更新就像点外卖
你this.msg = '祖安',Vue不会马上重渲染,而是:
- 触发setter,通知
Watcher。 - Watcher不直接打工,而是把自己塞进更新队列 (
queueWatcher)。 - 多个Watcher会去重,防止同一个组件被重复入队。 相当于你点了10个菜,厨师记在小本本上,不会炒一个上一个。
第二步:异步排队等叫号
Vue用nextTick把队列更新推进微任务(Promise.then)或者宏任务(setTimeout)里。
优先级 :微任务 > 宏任务,但Vue优先用Promise,不行降级到setTimeout。
结果就是:当前同步代码全执行完,再一次性更新DOM。
第三步:批量更新DOM
事件循环到微任务时,Vue从队列里掏出所有Watcher ,按父到子顺序(因为父组件可能更新子组件)挨个执行watcher.run()。
每个Watcher重新计算VDOM,Diff,然后一次性Patch到真实DOM。
这时候你看到的DOM已经是最新状态了。
第四步:nextTick让你抄底
this.$nextTick(callback)让你在DOM更新后执行回调,比如:
this.msg = '更新了'
this.$nextTick(() => {
// 这里DOM已经是最新了,可以操作DOM
})
Vue把回调也塞进队列,等更新完再执行,保证你能拿到最新DOM。
祖安例子:
你写:
this.a = 1
this.b = 2
this.c = 3
同步代码阶段:a、b、c的Setter触发,三个Watcher进队列。
微任务阶段:队列里的三个Watcher一次性执行,只一次DOM更新。
结果:页面只闪一次,不是三次,性能起飞。
为什么异步?
- 性能:避免一帧内多次重渲染,卡成狗。
- 去重:同一个Watcher多次触发只更新一次。
- 保证顺序:父组件更新先于子组件,避免子组件用过期props。
总结:
改数据 → 进队列 → 同步代码执行完 → 下一帧批量更新 → nextTick抄底
别™在改数据后立马操作DOM,用nextTick等更新完再搞!
人话总结 :Vue的异步更新机制将数据变更触发的更新任务放入队列,在下一个事件循环中批量执行,减少重复渲染,提升性能,并通过nextTick提供DOM更新后的回调时机。
Vue 组件是如何渲染和更新的
一、渲染阶段:从代码到页面的暴躁之路
1. 初始化 ------ 先给自己挖坑
new Vue()一创建,先把自己数据劫持 (变成响应式),methods、props、data全绑到实例上,但这时候模板还是字符串,DOM连个屁都没有。
2. 编译模板 ------ 把模板揍成渲染函数
- 如果用的 template,Vue 调用 编译器 把
<div>{``{msg}}</div>这种字符串: 模板 → AST → 优化静态节点 → 生成渲染函数 (一堆_c、_v、_s的代码)。 - 如果用的 render 函数,直接跳过编译,省事。
3. 执行渲染函数 ------ 生成 VNode 虚拟DOM
执行渲染函数,得到 VNode 树 (一堆描述DOM的JS对象),这时候还是虚拟的,页面上没东西。
4. 挂载 ------ 虚拟DOM怼进页面
patch函数把 VNode 递归变成真实DOM ,塞进 container(比如 #app),这时候页面终于能看到组件了 ,触发 mounted钩子。
二、更新阶段:数据变,视图跟着变
1. 触发更新 ------ 数据被改了
你 this.msg = "祖安",setter 触发,通知 Watcher:"数据变了,赶紧干活!"
2. 异步队列 ------ 先攒一波
Watcher 不立马更新,而是把自己扔进队列 (queueWatcher),等所有同步代码执行完,下一帧批量更新(避免你改10次数据渲染10次)。
3. 重新渲染 ------ 生成新VNode
从队列拿出Watcher,执行渲染函数,生成新的VNode树。
4. Diff 对比 ------ 找不同
新旧VNode树 diff 对比 (双指针+key哈希),找出真正变了的节点。
5. Patch 更新 ------ 最小化改DOM
把 diff 找到的差异,最小化更新到真实DOM (能复用的复用,不能的删了重建),触发 updated钩子。
三、例子
你有个组件:
js
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
data() { return { count: 0 } },
mounted() {
this.count = 1 // 触发更新
}
}
</script>
流程:
- 初始渲染:生成
count=0的VNode → 变成<div>0</div>真实DOM。 - 你改
count=1: setter 通知 Watcher,Watcher 进队列。 同步代码执行完,执行队列任务。 生成新VNode(count=1),和旧VNode diff。 发现只有文本变了,只更新<div>的文本节点(0→1),其他不动。
四、总结
渲染 :初始化 → 编译模板 → 生成VNode → patch成真实DOM
更新 :改数据 → 通知Watcher → 进队列 → 下一帧批量重新渲染 → diff找不同 → patch更新DOM
核心思想 :能复用就复用,能批量就批量,能异步就异步,绝不无脑重渲!
人话总结:Vue组件通过响应式系统监听数据变化,在渲染时生成虚拟DOM并挂载为真实DOM,在更新时通过Diff算法对比新旧虚拟DOM,最小化更新真实DOM,整个过程采用异步批量更新优化性能。
如何实现 keep-alive 缓存机制
keep-alive就是个"组件停尸房",不用的组件先别销毁,塞缓存里下次直接复活!
一、怎么用 ------ 简单到抠脚
vue
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
被包裹的组件切换时不会被销毁,而是缓存起来,切回来时直接复用。
二、核心原理 ------ 三大阴招
1. 缓存对象 ------ 搞个Map当停尸房
内部维护两个缓存对象:
js
cache = {} // 缓存VNode实例
keys = [] // 缓存键的队列,用于LRU淘汰
key默认是组件名 + 组件tag,你也可以自己指定:key。
2. 渲染劫持 ------ 偷梁换柱
keep-alive自己不渲染真实DOM,而是个抽象组件,只渲染子节点。
- 首次渲染:渲染子组件,把VNode塞进
cache。 - 再次切换:直接从cache里掏出来之前的VNode,不执行组件的created/mounted。
3. 生命周期作弊 ------ activated/deactivated
被缓存的组件有俩特殊钩子:
- activated:从缓存复活时触发(相当于二次进场)
- deactivated:被塞进缓存时触发(相当于躺进停尸房)
三、缓存策略 ------ LRU淘汰机制
如果缓存太多(默认不限),keep-alive用最近最少使用策略淘汰:
- 每次访问组件,把它的key扔到
keys数组末尾。 - 缓存数超过
max(默认不限),删掉keys[0]对应的最老缓存。 - 保证常用组件不被误杀,不用的组件自动滚蛋。
四、实现步骤 ------ 手撕源码精简版
1. 定义keep-alive组件
js
export default {
name: 'keep-alive',
abstract: true, // 抽象组件,不渲染自己
props: {
include: [String, RegExp, Array], // 白名单
exclude: [String, RegExp, Array], // 黑名单
max: [String, Number] // 最大缓存数
},
created() {
this.cache = Object.create(null) // 缓存VNode
this.keys = [] // 缓存键队列
},
render() {
// 1. 获取默认插槽的第一个组件VNode
const slot = this.$slots.default
const vnode = slot[0]
// 2. 判断是否缓存:无组件/非组件/被exclude排除/不被include包含
if (!满足缓存条件) return vnode
// 3. 生成缓存key
const key = vnode.key ?? `${vnode.tag}-${vnode.componentOptions.Ctor.cid}`
// 4. 命中缓存:从cache拿VNode,调整keys顺序
if (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
// 把key移到keys末尾(最近使用)
this.keys.splice(this.keys.indexOf(key), 1)
this.keys.push(key)
}
// 5. 未命中:缓存新VNode
else {
this.cache[key] = vnode
this.keys.push(key)
// 超过max则删除最老的
if (this.max && this.keys.length > this.max) {
const oldestKey = this.keys.shift()
delete this.cache[oldestKey]
}
}
// 6. 标记keepAlive,让组件知道自己被缓存
vnode.data.keepAlive = true
return vnode
}
}
2. 全局混入 ------ 给组件注入激活/停用钩子
Vue全局混入代码,在组件created时注册activated/deactivated钩子,在destroyed时清理缓存。
五、使用注意
1. 必须用key:
多个相同组件切换时,必须加:key区分,否则缓存会串,数据错乱。
2. 生命周期变化:
- 首次进入:
created → mounted → activated - 离开:
deactivated(不销毁!) - 再次进入:
activated(直接复用,不触发created/mounted)
3. 别乱用:
- 组件太多缓存爆内存
- 组件内有定时器/全局事件监听,记得在
deactivated中清理,activated中恢复
六、总结
keep-alive = 抽象组件 + 缓存对象 + LRU淘汰 + 激活钩子
核心:不销毁组件实例,直接缓存VNode,切回来时复活,避免重复渲染。
代价:占内存,得自己管理定时器/事件。
慎用:别TM啥都缓存,只缓存高频切换的静态组件!
人话总结:keep-alive通过抽象组件拦截渲染,将组件VNode实例缓存到内存中,再次渲染时直接复用实例,并通过activated/deactivated钩子管理组件状态,配合LRU策略防止内存溢出。
为何 ref 需要 value 属性
ref这个憨批,不整个.value就™没法区分你是基本类型还是响应式对象!
Vue 3的响应式核心是Proxy,但Proxy只能代理对象,搞不了基本类型(string、number、boolean这些)。
ref就是为了让基本类型也能变响应式,才用.value包装一层。
一、ref的本质 ------ 套壳侠
js
const count = ref(0)
// 实际上变成:
{
value: 0, // 你的值
__v_isRef: true, // 标记这是ref
getter/setter // 响应式劫持
}
ref就是个对象,把你的值塞进.value里,这样就能用Proxy监听变化了。
二、为什么非要.value? ------ 不.value就乱套
场景1:模板里自动解包
<template>
<div>{{ count }}</div> <!-- 不用.value,Vue自动给你解包 -->
</template>
<script setup>
const count = ref(0) // 这里要count.value = 1
</script>
模板里Vue自动给你脱掉.value的裤子,但JS里必须手动写,不然Vue不知道你要改的是ref对象还是普通值。
场景2:reactive里的ref自动解包
const num = ref(0)
const obj = reactive({ num }) // obj.num 直接是0,不是ref对象
在reactive里,Vue又自动脱裤子,但数组或Map/Set里的ref不会自动解包,傻逼设计。
场景3:不.value的灾难现场
js
const count = ref(0)
console.log(count) // 输出RefImpl对象,不是0!
count = 1 // 报错,你整个替换了ref对象
count.value = 1 // 这才是正确姿势
不写.value你操作的是ref对象本身,不是里面的值,响应式直接失效。
三、对比表
| 情况 | 用.value |
不用.value |
结果 |
|---|---|---|---|
| JS中赋值 | count.value = 1 |
count = 1 |
把ref对象覆盖了,响应式失效 |
| 模板中 | {``{ count.value }} |
{``{ count }} |
前者显示[object Object],后者自动解包显示值 |
| reactive中 | obj.count.value |
obj.count |
前者报错,后者自动解包 |
| 函数参数 | fn(count.value) |
fn(count) |
前者传值,后者传ref对象 |
四、为什么reactive不用.value?
reactive只能包裹对象 ,Proxy直接监听整个对象,属性访问用.就行:
js
const obj = reactive({ a: 1 })
obj.a = 2 // 直接操作,不用.value
但对象属性是ref时:
js
const count = ref(0)
const obj = reactive({ count })
console.log(obj.count) // 0,自动解包!
console.log(obj.count.value) // 报错,别多此一举
五、总结
1. 根本原因 :ref要包装基本类型实现响应式,必须套个对象壳,.value就是壳的入口。
2. 自动解包场景:模板、reactive对象属性里,Vue自动脱裤子,JS里必须手动。
3. 记住 :JS里操作ref的值必须用.value,不用就等着响应式失效!
4. 替代方案 :用reactive包裹对象,就不用烦人的.value,但失去基本类型直接赋值的爽感。
人话总结:ref通过.value属性将基本类型值包装为响应式对象,在JS中访问和修改必须通过.value,在模板和reactive中Vue会自动解包,这是为保持基本类型响应式能力的必要设计。