Vue原理(暴力版)

什么是 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 到飞起,随便你怎么折腾都不卡。


它俩关系就像:

  1. 你写代码改数据 → 先拿 VDOM(替身) 疯狂乱改(反正不花钱)。
  2. 改完一通 → VDOM 和上次的替身搞"找不同"(Diff 算法),找出哪儿变了。
  3. 最后只把变的地方真实 DOM(真身) 上打个小补丁(Patch),完事!

总结:别动不动就怼真实 DOM!拿 VDOM 这个替身先演练,改完只同步必要的,省得浏览器累成狗你页面卡成屎!

(人话:VDOM 是 JS 对象模拟的 DOM 树,通过差异化比对最小化更新真实 DOM,提升渲染性能。)

Vue 组件初始化的各个阶段都做了什么?

阶段一:beforeCreate------ 毛都没有,穷得叮当响

这时候组件就是个光杆司令datamethodprops全特么是 undefined

能干的只有注册点全局事件,但基本属于"要啥没啥,别来沾边"。


阶段二:created------ 数据有了,但还™是空气人

datamethodsprops都初始化好了,数据能读写,能调方法

但模板还没挂载到 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就™是个语法糖,拆开是:

  1. :value把数据怼到 input 上(Model → View)
  2. @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 分三种:

  1. 渲染Watcher:组件渲染时创建,负责更新视图。
  2. 计算属性Watcher:计算属性依赖变化时重新计算。
  3. 用户Watcherwatch选项里你写的监听回调。 Watcher 就是接到Dep通知后,去执行对应更新的打工人。

第四步:派发更新 ------ 改数据就群发通知

你改 this.user.name = '祖安人'

  1. 触发 Setter,Dep 拿出小本本。
  2. 通知所有 Watcher:"你们关注的爹变了!"
  3. Watcher 去排队更新(异步队列,避免重复渲染)。
  4. 最终重新渲染 → 生成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更新列表时一脸懵逼,不知道哪个元素对应哪个数据,只能暴力对比+就地复用,结果就是:

  1. 性能拉胯:疯狂移动DOM元素,重排重绘卡成狗。
  2. 状态错乱:比如勾选的复选框、输入框内容,元素顺序一变全™乱了。

key的作用 ------ 给每个元素发身份证

key就是个唯一标识,Vue通过它知道:

  • 哪个旧元素对应哪个新数据,能精准复用已有DOM。
  • 哪个元素被删了/新增了,只更新必要的,不瞎折腾。

举个祖安例子:

你有个列表 ['张三', '李四', '王五'],渲染成三个<li>

如果你删了'张三',没key的话Vue可能:

  1. 删掉第三个<li>(王五)
  2. 把前两个<li>内容改成'李四'和'王五'
  3. 顺序没变,但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)。

发现不同就直接拆了重建,不浪费时间递归对比,暴力但高效。


第二步:头尾双指针,四步怼穿

新旧节点数组各俩指针(头头、尾尾、头尾、尾头),四种情况按顺序怼:

  1. 头头相同:直接复用,头指针都右移。
  2. 尾尾相同:直接复用,尾指针都左移。
  3. 头尾相同:把旧节点尾巴移到前面,头指针右移,尾指针左移。
  4. 尾头相同:把旧节点头部移到最后,尾指针左移,头指针右移。

这四步能快速处理简单顺序变化,比如列表反转、头尾移动。


第三步:乱序处理 ------ 上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]不动,只移动db移动次数最少化


对比Vue2:

对比项 Vue2 Diff Vue3 Diff
静态节点 每次对比 提升到外部,不参与diff
动态追踪 全量对比 补丁标志,只比动态部分
事件处理 每次重新创建 缓存复用
列表更新 双指针+无脑移动 最长递增子序列,移动最少
块优化 块树追踪,跳过静态块

附加暴论:

  • 为什么Vue3快:不动的部分不对比,动的部分精准对比,移动方案最优。
  • 组合式API加持:响应式用Proxy,依赖追踪更细,diff需要对比的节点更少。
  • 编译时优化:大部分优化在编译阶段完成,运行时躺赢。

人话总结:Vue3通过静态提升、补丁标志、事件缓存、块树追踪、最长递增子序列等优化,大幅减少Diff过程中需要对比的节点数量和移动操作,性能暴打Vue2。

Vue diff 算法和 React diff 算法的区别

React Diff:一根筋递归,能复用就复用,不行就重建

核心策略

  1. 同类型节点对比,不同类型直接拆了重建。
  2. 列表用key,没key就按index暴力对比,性能吃屎。
  3. 递归深度优先,整个树从头到尾怼一遍。

结果

  • 简单场景还行,复杂列表更新可能憨批移动。
  • 没key时顺序一变,组件状态全乱,bug满天飞。

Vue Diff:带脑子双指针,靶向治疗

核心优化

  1. 同层双指针四步怼(头头、尾尾、头尾、尾头),快速处理头尾移动。
  2. key必须用,没key就摆烂,但Vue官方往死里强调用key。
  3. 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憨在 :默认策略简单,复杂列表依赖开发者手动优化(shouldComponentUpdateReact.memo)。
  • Vue骚在:编译时把能优化的全做了,开发者躺平就行。
  • 性能真相:小应用差别不大,复杂列表Vue3暴打React,但React Fiber可中断渲染更适合超大应用。

人话总结

React Diff简单递归,列表优化依赖key和开发者手动优化;Vue Diff同层双指针+编译时优化,列表更新更高效,移动更少。两者都强,但Vue3在Diff优化上更激进。

简述 Vue 组件异步更新的过程

Vue异步更新的核心就一句:改数据不立马更新,先攒着一波带走!

你一口气改10个数据,Vue不会渲染10次,而是扔到队列里,下一帧统一更新,防止你手贱把页面搞崩。


第一步:触发更新就像点外卖

this.msg = '祖安',Vue不会马上重渲染,而是:

  1. 触发setter,通知Watcher
  2. Watcher不直接打工,而是把自己塞进更新队列queueWatcher)。
  3. 多个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更新。

结果:页面只闪一次,不是三次,性能起飞。


为什么异步?

  1. 性能:避免一帧内多次重渲染,卡成狗。
  2. 去重:同一个Watcher多次触发只更新一次。
  3. 保证顺序:父组件更新先于子组件,避免子组件用过期props。

总结

改数据 → 进队列 → 同步代码执行完 → 下一帧批量更新 → nextTick抄底

别™在改数据后立马操作DOM,用nextTick等更新完再搞!


人话总结 :Vue的异步更新机制将数据变更触发的更新任务放入队列,在下一个事件循环中批量执行,减少重复渲染,提升性能,并通过nextTick提供DOM更新后的回调时机。

Vue 组件是如何渲染和更新的

一、渲染阶段:从代码到页面的暴躁之路

1. 初始化 ------ 先给自己挖坑

new Vue()一创建,先把自己数据劫持 (变成响应式),methodspropsdata全绑到实例上,但这时候模板还是字符串,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>

流程

  1. 初始渲染:生成 count=0的VNode → 变成 <div>0</div>真实DOM。
  2. 你改 count=1: setter 通知 Watcher,Watcher 进队列。 同步代码执行完,执行队列任务。 生成新VNode(count=1),和旧VNode diff。 发现只有文本变了,只更新 <div>的文本节点(01),其他不动。

四、总结

渲染初始化 → 编译模板 → 生成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用最近最少使用策略淘汰:

  1. 每次访问组件,把它的key扔到keys数组末尾。
  2. 缓存数超过max(默认不限),删掉keys[0]对应的最老缓存
  3. 保证常用组件不被误杀,不用的组件自动滚蛋。

四、实现步骤 ------ 手撕源码精简版

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会自动解包,这是为保持基本类型响应式能力的必要设计。

相关推荐
Bigger2 小时前
Coco AI 技术演进:Shadcn UI + Tailwind CSS v4.0 深度迁移指南 (踩坑实录)
前端·css·weui
踢球的打工仔2 小时前
jquery的基本使用(3)
前端·javascript·jquery
前端无涯2 小时前
Trae的使用
前端·ide·trae
WG_172 小时前
Linux:进程控制
前端·chrome
[seven]2 小时前
React Router TypeScript 路由详解:嵌套路由与导航钩子进阶指南
前端·react.js·typescript
无我Code3 小时前
前端-2025年末个人总结
前端·年终总结
VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue敬老院管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
文刀竹肃3 小时前
DVWA -SQL Injection-通关教程-完结
前端·数据库·sql·安全·网络安全·oracle