打开一个React应用,每点一次按钮、每输入一个字符,组件都在后台默默执行重新渲染。那不是Bug,是React刻意设计的心跳——状态变了,UI就必须更新。但React不能每次都把整个页面重画一遍,那会让浏览器瞬间变得跟拨号上网时代一样卡。
所以它手里握着一张底牌,叫Virtual DOM。
这东西本质上是一份用JavaScript对象写成的UI快照。你声明了count这个state的值是0,React就先在内存里画一棵虚拟的DOM树,树上挂着所有组件此刻的样子。这份快照不直接丢给浏览器,而是留在React自己的地盘里,充当开发者和真实DOM之间的一层缓冲。
当按钮被按下,setCount(count + 1)把状态从0推到1,React的反应不是仓促地去改浏览器里的那行字。它先不动真实页面,而是在内存中重新跑了一遍组件逻辑,生成了一份全新的虚拟DOM树。现在它手里有了两份快照:一份是更新前的旧树,一份是更新后的新树。
React使用虚拟DOM作为内存中的JavaScript对象层,隔离真实DOM操作以提升渲染效率
接下来这套操作叫reconciliation,可以把它理解成一场逐行比对。React的diffing算法开始在两个虚拟DOM之间找差异:哪个
标签里的数字变了、哪个组件的位置挪了、哪一项被从列表中删掉了。它不会粗暴地说“全换一遍吧”,而是像校对稿子一样,只圈出真正改动的几处。
这套算法靠两条硬规矩压低了计算量。一条是,如果两个元素的类型不同——比如
变成了——React直接销毁旧树、重建新树,不钻牛角尖去比对属性。另一条是,开发者给列表项加key属性之后,React能凭标识快速判断哪些项只是挪了位置、哪些是真的被新增或删除了,避免无脑重绘整张列表。
比对结束,React手里攒出了一份最小变更清单。只有这份清单上的操作,才会被应用到浏览器里真实的DOM节点上。屏幕上Count: 0变成Count: 1,变化的只是那一个
标签里的文本内容,其余部分纹丝不动。浏览器不需要重新计算整个页面的布局,性能开销被压在了指甲盖大小的范围内。
以一个Counter组件为例来看全过程:初始渲染时useState(0)把count定在0,虚拟DOM把这棵装着
Count: 0
的树丢给浏览器,用户看到画面。点击按钮触发setCount,状态更新为1,组件函数重新执行,新的虚拟DOM树生成。diffing算法在新旧两树之间走过一遍,发现只有
标签里的数字从0变成了1。最终,真实DOM上只有这一处文本被精准替换,按钮本身连眼皮都没抬一下。
这套机制的起点其实很朴素:React假定开发者懒得手动操作DOM,与其让你到处写document.getElementById然后小心翼翼地改节点,不如你把想要的界面样子声明出来,剩下的脏活累活都交给它。Virtual DOM就是这台自动洗衣机的核心滚筒——你扔进来的是状态和JSX,转完之后拿出去的是干净利落、只更新了必要部分的可交互界面。
热门跟贴