想象你请了两支装修队来改造同一个房子。传统装修队的做法是:客户说客厅沙发换个颜色,他们直接抡起大锤把整栋楼拆了重建,砌墙、布线、刷漆从头来一遍——又贵又慢,邻居还投诉。后来市面上出现了两支聪明的装修队:他们手里拿的不是锤子,而是图纸。接到需求先改设计图,再对比新旧图纸找出哪些墙变了,最后只派工人改掉有变化的那几处。 这套“先画图、再对比、只改动差异”的流程,就是 React 和 Vue 共用的虚拟 DOM 技术。但两支队伍找差异的风格完全不同:React 装修队(简称 R 队)从进门开始,沿着走廊一间间按顺序排查;Vue 装修队(简称 V 队)则从走廊两头同时包抄,还给每个房间都贴了门牌号(key),一眼就知道哪间房该动。故事就从这里开始,带你拆解两大框架的虚拟 DOM 与 Diff 算法。

岗位关联

理解虚拟 DOM 与 Diff 的内核,你才能写出高渲染性能的 React/Vue 组件,框架原理面试不虚,性能调优时不再瞎改代码。

目录
  1. 可视化演示
  2. 虚拟DOM原理——为什么先画图纸
  3. 虚拟DOM工作流程——画图、审图、施工四步法
  4. Diff核心思想——找差异的三条铁律
  5. React Diff策略——R队的标准排查法
  6. Vue Diff策略——V队的双端夹击
  7. 两框架对比——R队与V队的正面交锋
  8. 性能优化建议——让装修队省力的诀窍
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示React与Vue虚拟DOM的核心机制,通过总览/传输/转换/输出四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

React与Vue虚拟DOM:虚拟DO 虚拟DOM概念与JS对象描述渲 React Fiber架构与D 核心机制可视化 React与Vue虚拟DOM整体架构与关键组件

虚拟DOM原理——为什么先画图纸

老张的装修队以前干活,客户说“改一下玄关的鞋柜”,他们直接请来挖掘机把整栋房子推平重盖。这听起来荒唐,但浏览器处理真实 DOM 操作时就是这样:每改一次 DOM 节点,浏览器就要重新计算样式、重新布局、重新绘制,改的次数越多,页面卡得越厉害。频繁操作真实 DOM,就像频繁推倒重盖房子,每一次都代价高昂。

于是聪明的装修队不再直接动工,而是先在纸上画一份设计图。这份设计图就是虚拟 DOM(Virtual DOM):它不是真实的 HTML 标签,而是一棵由 JavaScript 普通对象组成的树。每个节点叫 vnode,记录着“这里有个 div、它有这些属性、下面挂着这些子节点”。改设计图只需要动笔,一行文字、一个对象,成本极低。

真实 DOM 操作贵在哪?贵在浏览器要经历一系列“重排重绘”流水线。而虚拟 DOM 把改动集中到 JS 对象层面,最后才一次性提交给浏览器,就像装修队先在图纸上反复推敲,确定方案后再进场施工,把几十次返工压缩成一次精准的施工。

所以装修队总结出一条经验:图纸怎么改都不心疼,墙不能随便砸。虚拟 DOM 的价值,就是把昂贵的“砸墙”变成便宜的“改图”,这就是它的核心价值。

Cvdom=Ccreate+Cdiff+Cpatch≪Crebuild\displaystyle C_{vdom} = C_{create} + C_{diff} + C_{patch} \ll C_{rebuild}
虚拟 DOM 的总成本(建图 + 对比 + 补丁施工)远小于直接重建整棵真实 DOM 树的成本。
  1. 建图成本\(C_{create}\) 表示根据组件状态创建新虚拟 DOM 树的成本,本质是 new 一些 JS 对象,速度极快。
    Ccreate=O(n)C_{create}=O(n)
  2. 对比成本\(C_{diff}\) 表示新旧虚拟 DOM 树做差异比较的成本,稍后你会看到它被优化到线性级。
    Cdiff=O(n)C_{diff}=O(n)
  3. 补丁成本\(C_{patch}\) 表示把差异应用到真实 DOM 上的成本,只操作有变化的那几个节点。
    Cpatch=O(m),m≪nC_{patch}=O(m),\quad m\ll n
  4. 重建成本\(C_{rebuild}\) 表示把整棵真实 DOM 树卸载重建的成本,涉及布局计算与重绘,数量级远超前者。

虚拟DOM工作流程——画图、审图、施工四步法

R 队和 V 队虽然风格不同,但接单后的流程出奇一致。第一步是“量房”:组件收到新的状态(state)后,重新执行渲染函数,把当前界面完整地画成一份新图纸,这就是 render 阶段。第二步是“画图”:渲染函数返回的是一棵新的虚拟 DOM 树,树上每个节点都是轻量的 JS 对象。

第三步是“审图”:装修队长左手拿着旧图纸,右手拿着新图纸,逐层比对,找出所有变化的房间和墙面的位置,这个动作就是 diff。第四步是“施工”:队长拿着差异清单,派工人精确修改真实 DOM 中对应的位置,这个动作叫 patch。四步走完,页面就完成了更新。

注意一个关键点:diff 阶段只发生在 JS 对象的“图纸空间”里,完全没有触碰真实 DOM。直到 patch 阶段才把差异一次性提交给浏览器。这就像图纸评审会上可以随意讨论方案,确定方案后工人只跑一趟工地,避免了传统装修队“改一次拆一次房”的浪费。

这套流程最妙的地方在于:真实 DOM 的更新被集中在了最后一步,前面的所有环节都是在“纸面”上完成的低成本推演。R 队和 V 队的区别,只在于第三步“审图”用了完全不同的策略。

Vnew=render(S),Δ=diff(Vold,  Vnew),patch(Δ)\displaystyle V_{new} = render(S),\qquad \Delta = diff(V_{old},\; V_{new}),\qquad patch(\Delta)
状态变化 → 生成新图纸 → 对比得差异清单 → 只改差异对应的真实 DOM,这就是虚拟 DOM 的完整工作流。
  1. 生成新图纸\(V_{new} = render(S)\) 表示组件状态 \(S\) 变化后,渲染函数产出一棵全新的虚拟 DOM 树 \(V_{new}\)。
    Vnew=render(S)V_{new}=render(S)
  2. 求得差异\(\Delta = diff(V_{old},\, V_{new})\) 表示把旧树 \(V_{old}\) 和新树 \(V_{new}\) 对比,得到差异集合 \(\Delta\)。
    Δ=diff(Vold,  Vnew)\Delta = diff(V_{old},\; V_{new})
  3. 施工更新\(patch(\Delta)\) 表示根据差异集合 \(\Delta\),在真实 DOM 上完成最小化的更新操作。
    patch(Δ)patch(\Delta)

Diff核心思想——找差异的三条铁律

两家装修队都知道,最理想的审图方案是算出“把旧房子改成新房子”的最小改动方案——比如移动几堵墙、拆除几个房间。但这件事在计算机里属于“树编辑距离”问题,复杂度高达 \(O(n^3)\),节点一多,图纸还没审完,客户都等睡着了。所以两队都放弃了完美方案,改用工程化的近似策略。

第一条铁律:只比较同一层,不跨层移动。在真实项目里,把整个组件子树从一层挪到另一层的操作极其罕见,所以两家都默认“跨层移动 = 销毁重建”。有节点从第 2 层搬到第 3 层?两边直接把它卸了重新装,不做任何移动优化。第二条铁律:按广度逐层扫描,上一层比完再比下一层,绝不回头。第三条铁律:用 key 给节点配身份证。在同一层的兄弟节点列表里,key 帮算法快速判断“这个新节点以前是不是那一个旧节点”,没有 key 就只能靠位置蒙。

这三条铁律把原本指数级的比较难题,压缩成了线性的扫描任务,复杂度从 \(O(n^3)\) 直降到 \(O(n)\)。所谓 Diff 算法,本质就是“在工程可接受的误差内,用最小的代价找出新旧两棵树的差异”。就像装修队审图时不去推演墙体移动的最优路径,而是简单地逐层标记“哪间房变了,哪间房没变”。

这三条铁律是两支装修队的“行规”。但行规之下,R 队和 V 队的实操风格截然不同,我们分别去看看他们是怎么审图的。

T(n)=O(n3)  →  O(n)\displaystyle T(n) = O(n^3) \;\rightarrow\; O(n)
朴素树 diff 是指数级灾难,靠“同层比较 + key 复用”两大铁律,两框架把比较复杂度降到线性。
  1. 指数灾难\(O(n^3)\) 是朴素树编辑距离的复杂度,节点数上百后就无法接受,必须用近似策略替代。
    T(n)=O(n3)T(n)=O(n^3)
  2. 线性优化\(O(n)\) 是同层比较后的复杂度,每层节点只被访问常数次,整体与节点总数成正比。
    T(n)=O(n)T(n)=O(n)
  3. 节点规模\(n\) 表示虚拟 DOM 树的节点总数,是复杂度的基准单位。

React Diff策略——R队的标准排查法

R 队审图有一套严格的标准动作。队长站在门口,先看客厅(根节点)的“用途”变没变——在代码里这就是节点的 type。如果客厅变成了车库,类型都不同,那整栋房子直接推倒重盖,里面的所有房间都不必再对比。如果 type 相同,队长才继续检查客厅的家具摆放(props),然后进入下一层,逐一检查每个房间。

这就是 React 三大假设的第一条:不同类型的元素会产生不同的树,直接重建。第二条:开发者用 key 提示哪些子元素是稳定的,让列表对比时能复用节点。第三条:跨层移动极少发生,所以不深入处理。这三条假设像极了装修行业经验:用途不同的房间没法复用,有门牌号的房间才好认,没人会没事儿把整间房子搬到二楼去。

在层内列表对比时,React 的做法很朴素:把新旧两个列表按索引顺序位对齐,逐个比较。如果同一个位置上 key 相同,就认为这个节点可以复用,只更新变化的部分;如果 key 对不上,就标记为增删或重建。如果一个列表只有中间一个位置变了,React 也只会处理那一个位置,不过它不会像 Vue 那样聪明地从两头同时夹击。

R 队的方法稳定可靠、可预测性强,唯一的代价是:它不像 V 队那样有一张“key 快速定位表”,遇到列表中有节点被删掉再插回前面时,需要多花几步搬运。这正是 Vue 装修队琢磨出的改进点。

vnode={type,  props,  key,  children}\displaystyle vnode = \{ type,\; props,\; key,\; children \}
虚拟 DOM 节点长这样:type 决定整棵子树去留,key 是复用的身份凭证,children 是下一层子节点。
  1. 类型字段\(type\) 是节点的标签名或组件类型,React 遇到 type 变更,直接卸载整棵子树重建。
    type:string∣Componenttype: string | Component
  2. 属性字段\(props\) 是节点的属性集合,type 相同才会继续对比 props 差异。
    props:{attr:value}props: \{ attr: value \}
  3. 身份凭证\(key\) 是列表渲染时开发者提供的唯一标识,帮助 React 判断新旧节点是否为同一个。
    key:string∣numberkey: string | number
  4. 子节点数组\(children\) 是递归进行下一层比较的子节点列表。

Vue Diff策略——V队的双端夹击

V 队审图的姿势比 R 队更灵活。他们不从门口一间间走到头,而是派两个人:一个从走廊最左端开始,一个从右端开始,同时向中间汇合。每轮比较,先看最左边的房间是否有变化,再看最右边的房间是否有变化,哪边相同就先处理哪边,两头同时推进。

这就是 Vue2 经典的双端比较(Double-End Diff)。每轮循环有四路判断:旧头对新头、旧尾对新尾、旧头对新尾、旧尾对新头,任一路命中就说明节点可以复用,剩下的交给 key 映射表快速查找。当列表里某个节点被删掉了,V 队能立刻发现尾部缺失,直接把它标记删除,而不必像 R 队那样把整段列表重新对位。

到了 Vue3,V 队又多了一把利器:编译时 patchFlag。因为 Vue 的模板是编译器处理的,它能在编译阶段就看出哪些地方是静态文本、哪些地方是动态属性,并在虚拟 DOM 节点上贴上“这里会变”的标记。运行时 diff 看到静态标记直接跳过,连比较都省了,只盯着真正会变的节点处理。

V 队的“双端夹击 + 门牌号快速查表 + 编译期贴标记”三件套,让它在处理列表增删和模板渲染时都比 R 队更省力。门牌号的价值在此时展露无遗:不用把所有房间都排查一遍,凭 key 就能直接定位要改的那间房。

oldStart≤oldEnd  ∧  newStart≤newEnd\displaystyle oldStart \le oldEnd \;\wedge\; newStart \le newEnd
双端 diff 的循环条件:旧列表的头尾指针没有交错,新列表的头尾指针也没有交错,就继续扫。
  1. 旧头指针\(oldStart\) 指向旧列表最左端未处理节点的下标,每次命中头部匹配后自增。
    oldStart←oldStart+1oldStart \leftarrow oldStart + 1
  2. 旧尾指针\(oldEnd\) 指向旧列表最右端未处理节点的下标,命中尾部匹配后自减。
    oldEnd←oldEnd−1oldEnd \leftarrow oldEnd - 1
  3. 新头指针\(newStart\) 指向新列表最左端未处理节点的下标,头部命中后自增。
    newStart←newStart+1newStart \leftarrow newStart + 1
  4. 新尾指针\(newEnd\) 指向新列表最右端未处理节点的下标,尾部命中后自减。
    newEnd←newEnd−1newEnd \leftarrow newEnd - 1

两框架对比——R队与V队的正面交锋

现在让 R 队和 V 队同台竞技,看看两支装修队的真实差距。先说相同点:两队都用虚拟 DOM 做中间层,都遵守“同层比较 + key 复用”这两条铁律,最终都以 patch 的方式更新真实 DOM,这是行业共识,不是某家的独家秘笈。

真正的差异在第二层。R 队的 React 是纯运行时 diff:它不关心你的代码是怎么写的,每次更新都从根节点开始,把整棵虚拟 DOM 树扫一遍,靠三条假设砍掉不必要的比较。V 队的 Vue 则是运行时 + 编译时组合拳:编译阶段就通过 patchFlag 标记出了“哪些节点是死的”,运行时只扫活的节点,天然跳过静态内容。

因此在同等规模下,\(T_{Vue}\) 的实际比较节点数明显少于 \(T_{React}\),尤其适合模板大量、交互相对固定的中后台页面。而 React 的纯运行时方案换来了更高的灵活性——JSX 可以写在任何地方,diff 逻辑统一且可预测,大型复杂应用里生态更成熟、掌控感更强。没有绝对的谁好谁坏,只有匹配不匹配。

总结成一句:R 队靠“标准流程 + 经验假设”保证不犯错,V 队靠“提前做功课 + 精准定位”节省体力。两支队伍没有绝对的优劣,而是给人留下了两个不同的优化思路——这也是为什么理解 diff 差异,能帮你在真实项目里做出更聪明的渲染决策。

TReact≈O(n),TVue≤O(n)\displaystyle T_{React} \approx O(n),\qquad T_{Vue} \le O(n)
两者复杂度同为线性,但 Vue 因编译时跳过了静态节点,实际比较的次数常数更小。
  1. React 的线性\(T_{React} \approx O(n)\) 表示 React 每次更新要把新旧整棵树完整扫描,比较次数与节点数成正比。
    TReact=O(n)T_{React} = O(n)
  2. Vue 的上界\(T_{Vue} \le O(n)\) 表示 Vue 因为跳跃了静态标记节点,比较次数总是小于等于节点总数。
    TVue≤O(n)T_{Vue} \le O(n)
  3. 常数优势实际差距体现在常系数上,Vue 扫的节点更少,常数更小,复杂模板下优势更明显。

性能优化建议——让装修队省力的诀窍

既然已经摸清了两支装修队的工作方式,那我们作为“客户”,怎么配合才能让他们更快干完活?第一个诀窍:把门牌号挂对。列表渲染时给每项一个稳定且唯一的 key,比如数据库里的 id。最忌讳的是用数组下标 index 当 key——列表一增删,下标全部错位,React 和 Vue 都会把原节点误判成新节点,导致一堆不该重建的节点被重建,甚至引发输入框内容错乱。

第二个诀窍:别随便改房子的承重结构。顶层组件的根标签要从 <div> 切成 <section>,或者把一个组件从“渲染 A 组件”改成“渲染 B 组件”?类型一变,React 会把整棵子树推倒重建,你精心优化的内部组件全部白搭。保持顶层结构稳定,diff 才有机会深入内层做精细比较。

第三个诀窍:让不需要装修的房间自动锁门。React 里用 React.memo 或 PureComponent 对组件做浅比较,让 props 没变的子组件跳过重新渲染;Vue 里配合 computed 自动缓存计算结果。这样父组件无论怎么折腾,子组件只要“没收到新指令”就直接说:我这间房不用改。下面这个复用率公式,就是衡量门牌号和结构做得好不好的标尺。

把这三个诀窍做到位,你写出来的组件哪怕面对每秒几十次的数据刷新,也能让页面保持流畅。理解虚拟 DOM 和 Diff,不是为了背概念,而是为了在写代码时知道:每一次渲染,装修队都在按什么规则为你省下真金白银。

R=NmatchedNtotal→1\displaystyle R = \frac{N_{matched}}{N_{total}} \rightarrow 1
节点复用率 = 可复用节点数 / 总节点数。key 越稳定、结构越稳定,复用率越接近 1,渲染就越快。
  1. 复用节点数\(N_{matched}\) 是 diff 时判定为“同一个节点可直接复用”的数量,是优化成效的直接体现。
    Nmatched=复用节点数N_{matched}=复用节点数
  2. 总节点数\(N_{total}\) 是新虚拟 DOM 树里需要处理的节点总数。
    Ntotal=本次渲染节点数N_{total}=本次渲染节点数
  3. 复用率趋近 1\(R \rightarrow 1\) 表示几乎所有节点都能被复用,只有真正的差异触发 patch,渲染性能达到最优。

AI 应用场景

大型表格渲染优化

阶段:前端组件性能优化

解决的问题:AI 系统的运营后台常常有上千行的实时数据表,订单、日志、指标每秒都在刷新。传统做法每次刷新都重新渲染整张表格,浏览器频繁重排,页面卡成幻灯片;用 index 做 key 还会导致行错位、输入框内容串行。

具体案例:某电商 AI 风控系统的订单流水表,通过 WebSocket 每秒推送新订单。前端给每行绑定订单号 orderId 作为稳定 key,并给行组件包裹 React.memo,让 diff 只命中真正变化的那几行。改造后表格从 300ms 刷新时长降到 30ms 以内。

可视化大屏实时刷新

阶段:数据渲染管线优化

解决的问题:AI 可视化大屏往往同时挂着十多个图表组件,数据流每秒多次更新。如果每次更新都让所有图表重新 diff 和渲染,CPU 会长期占用高企,动画掉帧。

具体案例:城市交通 AI 大屏的车辆轨迹图,每秒钟收到上千条轨迹点。开发团队将每个轨迹图层拆成独立组件,用稳定 key 标记每辆车,配合 patchFlag 精准更新变化图层,未变化的静态图层被直接跳过,大屏动画稳定在 60 帧。

框架选型评估

阶段:技术架构决策

解决的问题:AI 产品的 Web 端在选型时,需要评估渲染性能与开发效率的平衡。React 的纯运行时 diff 灵活可控,适合交互复杂、需要精细管理渲染的 AI 工作台;Vue 的编译时优化开箱即用,适合模板密集、快速迭代的中后台系统。

具体案例:某 AI 公司同时维护两个产品:模型标注工具选择了 React,因为画布交互复杂、团队需要手动控制重渲染边界;内部 AI 服务管理后台选择了 Vue,因为页面模板多、组件更新频率低,Vue 的静态优化让开发几乎不用手动做 memo。

Python 演示

python57 行
def react_style_scan(old_list, new_list):    """React 风格:按索引从前往后逐一比对"""    ops = []    n = max(len(old_list), len(new_list))    for i in range(n):        old = old_list[i] if i < len(old_list) else None        new = new_list[i] if i < len(new_list) else None        tag = "复用" if old == new else "更新"        ops.append((old, new, tag))    return ops  def vue_style_scan(old_list, new_list):    """Vue 风格:双端指针同时向中间扫描"""    ops = []    old = list(old_list)    new = list(new_list)    while old and new:        if old[0] == new[0]:            ops.append((old[0], new[0], "头部匹配"))            old.pop(0)            new.pop(0)        elif old[-1] == new[-1]:            ops.append((old[-1], new[-1], "尾部匹配"))            old.pop()            new.pop()        elif old[0] == new[-1]:            ops.append((old[0], new[-1], "头尾复用"))            old.pop(0)            new.pop()        elif old[-1] == new[0]:            ops.append((old[-1], new[0], "尾头复用"))            old.pop()            new.pop(0)        else:            ops.append((old[0], new[0], "未知需查表"))            old.pop(0)            new.pop(0)    return ops  def reuse_rate(ops):    """统计可复用节点的比例"""    reused = sum(1 for _, _, tag in ops if "复用" in tag or "匹配" in tag)    return reused / max(len(ops), 1)  if __name__ == "__main__":    old_rooms = ["客厅", "卧室", "书房", "厨房"]    new_rooms = ["客厅", "书房", "主卧", "厨房"]    print("React 风格扫描:")    for step in react_style_scan(old_rooms, new_rooms):        print("  ", step)    print("Vue 风格扫描:")    for step in vue_style_scan(old_rooms, new_rooms):        print("  ", step)    print("Vue 风格复用率:", reuse_rate(vue_style_scan(old_rooms, new_rooms)))
目的:用两套简单的扫描函数模拟 React 与 Vue 的 Diff 思路,直观对比“顺序比较”与“双端比较”的异同。
  1. def react_style_scan(old_list, new_list):模拟 React 在层内按索引顺序逐个比较子节点。
  2. n = max(len(old_list), len(new_list))按较长列表的长度对位,保证每个位置都被检查到。
  3. tag = "复用" if old == new else "更新"同位置同值判定为可复用,否则标记为需要更新。
  4. while old and new:Vue 双端比较的循环条件,只要有未处理的节点就继续扫描。
  5. elif old[-1] == new[-1]:尾部节点相同直接复用,这是双端比较省力的关键。
  6. elif old[0] == new[-1]:旧头部跑到新尾部也能识别,避免了 React 式的整段重排。
  7. elif old[-1] == new[0]:旧尾部跑到新头部同样可以捕获复用机会。
  8. ops.append((old[0], new[0], "未知需查表"))两头都无法匹配,交给 key 映射表做精确查找。
  9. if __name__ == "__main__":直接运行脚本即可看到两种策略的扫描轨迹差异。

常见误区

误区听说虚拟 DOM 快,那我就永远用它,直接操作 DOM 是愚蠢的行为。反正虚拟 DOM 性能天下第一。

正解虚拟 DOM 快在“频繁小改动”和“批量更新”的场景,它用 JS 对象计算换取了更少的真实 DOM 操作。但如果只改一个按钮的文案,直接 textContent 赋值可能比走完“创建 vnode → diff → patch”全流程更快。虚拟 DOM 的最大价值不是绝对速度,而是让开发者不用手动管理 DOM 操作,同时保证可接受的性能。

为何常见:框架宣传和面试题都强调虚拟 DOM 性能好,初学者容易把“相对优势”理解成“绝对优势”。

误区列表数据反正只有文字,用 index 当 key 也不会报错,还能少写一行代码,应该没问题吧?

正解列表一旦发生头部插入或删除,index 会全部错位。比如列表 [A,B,C] 删掉 A 后变成 [B,C],此时下标 0 的元素从 A 变成 B,而 key 却还是 0,框架会误以为“原来的 A 变成了 B”,导致本应复用的 B 的 DOM 被错误地当作 A 来更新,可能引发输入框内容串行、组件状态错乱。正确做法是用数据唯一的 id 做 key。

为何常见:初学者在静态列表上没踩过坑,看不到 index 在动态增删时的连锁反应,自然觉得“能用就行”。

误区React 和 Vue 都用虚拟 DOM,也都同层比较,那它们的 diff 就是同一个东西,只是打包方式不同。

正解两者虽然都遵守“同层比较 + key 复用”,但实现差异很大:React 是纯运行时,靠三大假设简化,列表层用单循环按索引遍历;Vue 在运行时之外还有编译期 patchFlag 标记,列表层用双端指针 + key 映射表。Vue3 还能跳过静态节点,React 则需要开发者手动 memo 来达到类似效果。

为何常见:只看到了“同层比较”的共同点,没有深入到列表比较策略和编译时优化这两个关键差异层面。

误区Diff 算法就是把新旧两棵树的每个节点都递归地互相比较一遍,找出所有不同之处,所以复杂度是 O(n²)。

正解实际上 React 和 Vue 都采用“同层比较”策略,只比较相同层级的节点,默认不跨层移动。之所以敢这样简化,是因为跨层移动在实际业务中极为罕见,强行支持反而会带来 O(n³) 的复杂度灾难。所以复杂度被压缩到 O(n),而不是递归全树两两比较。

为何常见:看到“树”就联想到全递归遍历,忽略了框架工程上“用假设换性能”的取舍智慧。

面试题

Q1. 虚拟 DOM 的本质是什么?

单选题 · easy

  • A.浏览器里的内存对象
  • B.用JS对象模拟真实DOM← 正确答案
  • C.一套全新的 HTML 标签
  • D.浏览器自带的渲染引擎

答案:B。虚拟 DOM 是一棵由 JavaScript 普通对象构成的树,用对象结构描述真实 DOM 的样子,而不是浏览器原生对象。它就像装修队在动工前画的设计图纸,本质是轻量的 JS 数据,不是浏览器内存里的真实节点。

Q2. 虚拟 DOM 最核心的优势是什么?

单选题 · easy

  • A.减少真实DOM操作次数← 正确答案
  • B.彻底消灭页面重绘流程
  • C.让 HTML 代码更加简洁
  • D.让 CSS 样式自动生效

答案:A。虚拟 DOM 把多次零散的 DOM 操作合并成一次 patch,集中批量更新,从而减少浏览器重排重绘。它不可能消灭重绘(页面总得画出内容),也不负责 HTML 简洁或 CSS 生效,那些不是它的职责。

Q3. 下列哪一项是 React Diff 的三大假设之一?

单选题 · mid

  • A.不同类型的元素产生不同树← 正确答案
  • B.开发者必须给每个节点加 key
  • C.所有组件都必须是纯函数
  • D.Diff 只能在整个应用顶层进行

答案:A。React 的第一个假设是“不同类型的元素会产生不同的树”,所以 type 变了直接重建。key 是列表优化建议而非强制;纯函数是组件规范不是 diff 假设;diff 是递归在每一层进行的,不是只在顶层。

Q4. 在列表中不推荐使用 index 作为 key 的原因?

单选题 · mid

  • A.index 不会随列表发生变化
  • B.index 会随增删导致错位复用← 正确答案
  • C.index 是数字而不是字符串
  • D.index 无法通过编译器校验

答案:B。列表增删时 index 会重新编号,框架误以为旧节点变成了新节点,导致 key 与节点对应关系错乱。这就像门牌号按排队序号来贴,队伍一插人,全部房间身份都乱了,复用逻辑必然出错。

Q5. Vue 的 Diff 最突出的特点是什么?

单选题 · mid

  • A.只比较同层不做跨层移动
  • B.双端指针同时向中间扫描← 正确答案
  • C.每次更新都会重建整棵树
  • D.完全依赖编译时的静态代码

答案:B。Vue2 列表 diff 采用双端比较,头尾四路同时推进;Vue3 又加上了编译时 patchFlag。A 是两家共同点而非 Vue 独有,C 和 D 表述错误。

Q6. React 与 Vue 的 Diff 都遵循的规则是?

单选题 · extreme

  • A.跨层移动节点不进行深入比较← 正确答案
  • B.每个节点必须标记为静态节点
  • C.使用双端指针快速匹配复节点
  • D.只在初次渲染时执行 diff

答案:A。两家都默认“跨层移动极少发生”,所以跨层移动的节点被直接卸载重建,不做精细比较。静态标记是 Vue3 特有,双端指针是 Vue 特有,diff 在每次更新时都会执行,D 明显错误。

Q7. 虚拟 DOM 的优点有哪些?

多选题 · easy

  • A.减少频繁的真实DOM操作← 正确答案
  • B.让框架可以批量更新视图← 正确答案
  • C.让浏览器不需要解析 HTML
  • D.使渲染性能一定超过原生

答案:AB。虚拟 DOM 通过批量 patch 减少真实 DOM 操作,这是核心优势。浏览器仍然要解析 HTML,C 错;性能上并非必然超过原生操作,D 是绝对化说法,错误。

Q8. 属于 React 与 Vue Diff 差异的有?

多选题 · mid

  • A.Vue 对静态节点有编译标记← 正确答案
  • B.React 靠三大假设简化 diff← 正确答案
  • C.Vue 使用双端指针推进扫描← 正确答案
  • D.React 会复用跨层移动的节点

答案:ABC。Vue3 编译时标记静态节点,React 依赖三大假设,Vue 列表使用双端比较,这些都是真实差异。React 明确假设跨层移动罕见,遇到跨层移动直接重建,D 错误。

Q9. 虚拟 DOM 的工作流程顺序是什么?

单选题 · mid

  • A.vnode创建→diff对比→patch更新
  • B.render触发→vnode创建→diff→patch← 正确答案
  • C.diff对比→vnode创建→patch→render
  • D.patch更新→diff→render→vnode创建

答案:B。状态变化先触发 render 函数,产出新的虚拟 DOM 树(vnode),然后与旧树 diff 得到差异,最后 patch 更新真实 DOM。顺序就像“量房→画图→审图→施工”,一步都不能乱。

Q10. 以下哪些是提升渲染性能的正确做法?

多选题 · hard

  • A.列表key保持稳定且唯一← 正确答案
  • B.尽量保持组件顶层结构稳定← 正确答案
  • C.对昂贵组件使用 memo 缓存← 正确答案
  • D.每次渲染前手动删除多余节点

答案:ABC。稳定 key 提高复用率,顶层结构稳定避免整棵子树重建,memo 拦截不必要的子组件重渲,三者都是标准优化手段。手动删除节点违背框架声明式思想,还会干扰 diff 的正确性,是反模式。