vue和react的区别
1. 核心驱动力:数据响应式原理 (The Reactivity Model)
这是两者最底层的分歧,决定了你写代码的心智模型。
-
React:基于“拉(Pull)”的机制,粗更新- 本质: React 并
不清楚具体是哪个变量变了。当你调用 setState 或 useState 的 setter 时,你只是告诉 React:“嘿,状态变了,你重新运行一下整个组件函数吧。” - 后果:
React 会默认递归更新该组件及其所有子组件。 - 代价:
为了性能,开发者必须手动优化(使用 React.memo, useMemo, useCallback)来阻止不必要的渲染。 - 心智模型: UI = f(state)。
每次渲染都是一次全新的函数调用,强调**不可变数据(Immutability)** 。
- 本质: React 并
-
Vue:基于“推(Push)”的机制,细更新- 本质: Vue 通过
Object.defineProperty(Vue 2) 或Proxy(Vue 3) 劫持了数据。当你修改 state.count = 1 时,Vue 的响应式系统精确地知道哪个属性变了,以及哪个组件(甚至哪个 DOM 节点)依赖于这个属性。 - 后果: Vue 可以直接通知对应的组件进行更新,
不需要像React那样从根部开始遍历比对。 - 优势: 自动化程度高,
性能优化通常由框架内部完成,开发者心智负担小。 - 心智模型: 可变数据(Mutability) 。你直接修改对象,系统自动响应。
- 本质: Vue 通过
2. 渲染机制与编译优化 (Runtime vs. Compile-time)
这是近年来 Vue 3 和 React 发展方向分道扬镳的关键点。
静态:编译时就能确定、运行时永远不变的ui部分(如写死的标签、文本)。优化目标:创建一次,永久复用,跳过对比。
动态:依赖运行时数据的ui部分。优化目标:精确定位变化位置,只更新变动的地方。
-
React:重运行时 (Runtime Heavy)
- React 的 JSX 本质上是 JavaScript 的语法糖,非常灵活(Too flexible)。
- 因为太灵活(你可以在 JSX 里写任意 JS 逻辑),
编译器很难在编译阶段静态分析出哪些部分是静态的,哪些是动态的。 - 解决之道: React 放弃了编译时优化,转而在运行时发力。于是有了
Fiber架构和Concurrent Mode(并发模式) 。既然无法避免大量的 Diff 计算,那就把计算任务切片,利用浏览器的空闲时间分批处理,保证页面不卡顿。
-
Vue:编译时优化 (Compile-time Optimization)
-
Vue 使用的是
模板(Template) 。模板的语法是受限的(你只能用 v-if, v-for 等指令)。 -
这种限制给了编译器巨大的优化空间。Vue 3 的编译器可以在构建阶段就分析出哪些节点是静态的(永远不变),哪些是动态的。
-
黑科技:
- Patch Flags: 标记节点具体是文本变了还是 Class 变了,Diff 时只比对变化的部分。
- Block Tree: 将动态节点提取出来,Diff 时直接遍历动态节点数组,忽略静态节点。
-
结论: Vue 试图通过更聪明的编译器,让运行时的 Diff 变得极快。
-
react 重新渲染的条件
第一部分:React 的更新机制(The Fiber Architecture)
React 的更新过程并不是“一变立刻改 DOM”,而是一条精密的流水线。自 React 16 引入 Fiber 架构 后,这个过程被分成了两个核心阶段:
1. 触发阶段 (Trigger)
这是起点。React 收到一个信号(Signal),知道有数据变了,需要在未来某个时间点更新 UI。
- 关键点: 在 React 18 中,这些更新通常是**批量(Batched)**处理的。你连续调 3 次 setState,React 只会跑一次更新流程。
2. 渲染阶段 (Render Phase) —— "计算与找不同"
这是 React 最累的阶段,也是“粗粒度”体现的地方。
-
动作: React 从根节点(或受影响的节点)开始,重新调用组件函数。
-
产出: 组件函数返回新的 Virtual DOM(React Elements)。
-
Diff(协调 Reconciliation): React 拿着新的 Virtual DOM 和 旧的 Fiber 树 进行对比。
- 它会标记出哪些节点需要修改(Update)、哪些需要插入(Placement)、哪些需要删除(Deletion)。
-
特性: 这个阶段是纯计算,只发生在内存里,不碰真实 DOM。它是可以被中断的(Concurrent Mode 的核心)。如果浏览器此时有高优先级的任务(比如用户正在输入),React 会暂停 Diff,先去响应用户,回头再继续算。
3. 提交阶段 (Commit Phase) —— "动手修改"
这是最后一步。
- 动作: React 根据 Render 阶段计算出来的“修改清单(Effect List)”,一次性把变化应用到真实的浏览器 DOM 上。
- 特性: 这个阶段是同步且不可中断的。一旦开始改 DOM,必须一口气改完,防止用户看到画面撕裂。
- 收尾: DOM 改完后,React 会回过头来执行 useEffect 和 useLayoutEffect。
第二部分:除了 setState,还有谁能触发react更新?
基于 React 的“拉(Pull)”机制和“粗粒度更新”模型,触发 React 组件重新渲染(Update)的情况主要可以归纳为以下 4 种核心场景。
理解这些场景的关键在于:React 默认是“宁可错杀一千(多渲染),不可放过一个(漏渲染)”。
1. 组件内部状态发生变化 (State Change)
这是最直接的触发方式。当你调用能够改变“快照”的函数时,React 就会安排一次更新。
- API:
useState的 setter (如setCount) 或useReducer的dispatch。 - 类组件:
this.setState()。 - 机制: React 会比较新旧 State(使用
Object.is)。- 注意: 如果新旧值完全相等(
引用相等),React 会执行 Bailout(紧急退出)策略,不会触发重新渲染。这就是为什么必须强调不可变数据(Immutability)——如果你直接修改对象属性但引用没变,React 以为没变,就不更新。
- 注意: 如果新旧值完全相等(
为什么react强调不可变数据,因为react使用
Object.is比较两个值是否相同。 Object.is 原理:基本类型直接比较值是否相同,引用数据类型比较引用地址是否相同
2. 父组件重新渲染 (Parent Re-render)
这是 React **“粗粒度更新”**最典型的体现,也是性能问题的最大来源。
- 机制: 只要父组件重新渲染了,默认情况下,它的所有子组件都会无条件重新渲染。
- 重要误区: 很多人以为只有当子组件的 Props 变了,子组件才会更新。错! 哪怕 Props 完全没变,只要父组件动了,子组件就会跟着动(除非使用了
React.memo)。 - 后果: 这就是你提到的“连锁反应”。父组件一动,整棵子树全部重新运行一遍
f(state)。
3. Context 发生变化
Context 是为了解决“透传”问题,它能像虫洞一样穿越组件层级。
没有Context的话,我们想要给孙子组件传递信息的话,需要一层一层往下传。十分不方便
- 机制: 当
<Context.Provider>的value发生变化时,所有消费该 Context 的后代组件(使用了useContext的组件)都会强制重新渲染。 - 特点: 这种更新会穿透中间层。即使中间的父组件使用了
React.memo试图阻挡更新,底层的消费者依然会收到通知并更新。
4. Hooks 触发的外部存储更新
随着 React 18 的普及,很多状态管理库(Redux, Zustand 等)不再单纯依赖 Context,而是使用专门的 Hook。
- API:
useSyncExternalStore。 - 机制: 当外部 store(如 Redux store)发生变化并通知 React 时,React 会强制触发当前组件的更新。
特殊情况:强制更新与 Key
-
Key 发生变化 (Identity Change)
- 这不是常规的“更新”,而是 销毁 + 重建 。
- 如果你给一个组件的
key从1变成了2,React 会认为这是两个完全不同的组件。它会卸载旧组件(Unmount),然后挂载新组件(Mount)。这会导致状态全部丢失。
-
强制更新 (Force Update)
- 类组件中的
this.forceUpdate(),或者在 Function Component 中利用useReducer模拟强制刷新。这会跳过shouldComponentUpdate检查。
- 类组件中的
总结:什么是“不”触发更新?
为了理解“更新”,必须反过来理解什么不触发更新(这也是新手常犯的错):
- 修改 useRef 的值:
ref.current = 123不会触发渲染。Ref 是脱离于 React 渲染数据流之外的逃生舱。 - 直接修改变量/对象:
user.name = 'New Name'(而不调用 setState)。React 不知道数据变了,因为它是基于“setter 通知”机制,而不是像 Vue 那样的“Proxy 劫持/监听”机制。 - Memo 拦截: 如果组件被
React.memo包裹,且 Props 的浅比较(Shallow Compare)结果相同,且自身 State/Context 无变化,则父组件渲染不会带动它渲染。
对应你的心智模型
- Pull 机制: 只有上面这几种情况发生(发出了信号),React 才会去“拉取”新的 UI。
- 粗更新: 尤其是 #2 (父组件更新),是 React 默认性能开销大的主要原因,也是为什么我们需要
useMemo/useCallback来维持引用的稳定性,配合React.memo来截断这股“更新的洪流”。
fiber
因为 JSX 太灵活,导致静态编译优化困难,React 被迫选择了一条更艰难的路:重写底层引擎,在运行时(Runtime)通过“时间管理大师”般的调度来解决性能问题。
这条路的结果,就是 Fiber 和 Concurrent Mode(现在 React 18 中更倾向于叫 Concurrent Features)。
Fiber 架构诞生的最根本原因,就是为了让 Diff 阶段(Render Phase)可以“暂停”、“中断”和“恢复”。
1. 为什么 React 15 不能暂停?
在 Fiber 出现之前(React 15),React 做 Diff 用的是递归(Recursion)。
- 技术限制: 递归依赖的是 JavaScript 引擎
原生的 “调用栈”(Call Stack)。 - 现象:
function diff(node) { // ...对比逻辑... node.children.forEach(child => diff(child)); // 递归调用 } - 死穴: 浏览器的调用栈是
系统管理的。一旦你开始递归,函数就会一层层压栈。你(开发者/React)没有权限去“暂停”这个栈的执行,然后去处理别的任务(比如点击事件),最后再完美地把栈恢复原样。- 这就好比你坐过山车,一旦车开了(递归开始),必须跑完全程才能停下来,中间绝对不能下车。
2. Fiber 是如何实现“暂停”的?
既然“系统自动管理的栈”不能暂停,React 团队决定:那就不要用系统的栈了,我们在内存(堆)里自己手动模拟一个栈!
这个“手动模拟的栈”,就是 Fiber。
- 数据结构的变化: Fiber 把树形的递归遍历,转换成了链表的循环遍历。
- 节点关系: 每个 Fiber 节点包含三个关键指针:
- Child(大儿子)
- Sibling(二兄弟)
- Return(回父节点)
有了这套结构,React 就可以通过循环(Loop)来工作:
// 伪代码:Fiber 的工作循环
let nextFiber = root;
function workLoop() {
while (nextFiber && 还有剩余时间) {
// 1. 干活:处理当前节点(Diff)
performUnitOfWork(nextFiber);
// 2. 存档:找到下一个节点,存入变量
// 先找儿子,没儿子找兄弟,没兄弟找叔叔...
nextFiber = findNextNode(nextFiber);
}
// 3. 暂停:如果时间没了,就跳出循环
if (nextFiber) {
// 把控制权交还给浏览器,等下一帧再说
// 关键点:nextFiber 这个全局变量记住了我们干到哪了!
requestIdleCallback(workLoop);
}
}
3. 总结:Fiber 的本质
- React 15 的递归: 是深度优先遍历,状态保存在系统调用栈中。
系统栈不可中断。 - React Fiber: 依然是深度优先遍历,但状态保存在
堆内存的链表对象(Fiber Node)中。- 因为对象是在堆里的,React 可以随时把
nextFiber这个指针停住。 - 去处理别的高优先级任务(如响应点击)。
- 回来后,读取
nextFiber,继续往下遍历。
- 因为对象是在堆里的,React 可以随时把
打个比方:
- React 15 (递归):像潜水。你必须一口气潜到底再浮上来,中间不能换气,否则就挂了。
- Fiber (链表):像徒步旅行。你手里拿着地图(Fiber树),每走到一个路标(Fiber节点)就可以停下来喝口水、拍张照、回个微信(响应用户),然后看着地图继续走下一段路。
所以,Fiber 架构就是为了把“同步的一口气潜水”变成“可中断的徒步旅行”,从而实现并发模式(Concurrent Mode)。
Concurrent Mode
Concurrent Mode 让 React 具有了“同时通过不同优先级处理多个状态更新的能力,并且可以让渲染过程“可中断”。
Concurrent Mode(并发模式) 是 React 发展史上最大的变革,没有之一。
用一句话概括:Concurrent Mode 让 React 具有了“同时通过不同优先级处理多个状态更新”的能力,并且可以让渲染过程“可中断”。
在 React 18 发布后,官方不再强调“模式(Mode)”这个词(因为不需要你手动开启了),而是称之为 Concurrent Features(并发特性)。只要你使用了这些特性(如 useTransition),React 就会自动开启并发渲染。
为了让你彻底理解,我们从现象、核心能力和比喻三个角度来说。
1. 现象:同步 vs 并发
假设你有一个输入框,下面是一个包含 10,000 条数据的列表。你每打一个字,列表就要根据关键字过滤并重新渲染。
没有并发模式(同步渲染 - React 15/16/17 默认行为)
这是一个 “阻塞(Blocking)” 的世界。
- 你输入 "A"。
- React 开始计算 10,000 条数据的新 DOM。
- 主线程被锁死:这时候你发现写错了,想按 "Backspace" 删除,或者想点别的按钮。
- 没反应! 屏幕卡住了。
- 直到 10,000 条数据渲染完成,浏览器才缓过劲来,处理你的删除操作。
- 总结: 一旦开始渲染,必须一口气干完,谁也别想插队。
有并发模式(Concurrent React 18)
这是一个 “非阻塞(Non-Blocking)” 的世界。
- 你输入 "A"。
- React 开始在后台计算 10,000 条数据。
- 还没算完,你突然按了 "Backspace"。
- React 说:“哟,用户又有新操作了,这个优先级高!”
- React 抛弃还没算完的 10,000 条数据,立刻响应你的删除操作,更新输入框的值。
- 等你手停了,React 再回头去重新计算列表。
- 总结: 渲染可以暂停、废弃、插队。
你好!很高兴以高级 React 开发者的身份与你交流。
React 的并发模式(Concurrent Mode)(在 React 18 中更准确的说法是“并发特性 Concurrent Features”)是 React 历史上最底层、最革命性的架构升级。
为了让你理解透彻,我们分三个层面来讲:核心概念、底层原理、以及实际用处。
一、 什么是并发模式?(What)
一句话总结:并发模式让 React 的渲染过程变成了“可中断”的。
在 React 16 以前(Stack Reconciler 时代),React 更新 DOM 就像坐滑梯:一旦开始滑(开始渲染组件树),就必须滑到底(完成整个树的渲染和 DOM 更新),中途无法停下来。如果组件树很大,计算耗时超过 16ms(人眼感知卡顿的阈值),浏览器主线程被阻塞,用户的点击、输入就会没反应,页面就“卡死”了。
并发模式引入后,React 的更新变得像“切土豆”:
- React 不再一口气干完所有活,而是把任务切成一个个小片(Time Slicing)。
- 做完一片,React 会抬头看看:“浏览器主线程有没有更紧急的事情?(比如用户点击了按钮、输入了文字)”
- 如果有,React 暂停当前的渲染,先去处理紧急交互。
- 处理完紧急的,再回来继续之前的渲染,或者如果数据变了,甚至废弃之前的渲染重新开始。
关键点:并发(Concurrency) ≠ 并行(Parallelism)。 React 依然运行在 JS 的单线程上,它通过精细的调度机制,让多个更新任务在时间线上交替执行,给人一种“同时进行”的错觉。
二、 底层原理是什么?(How)
并发模式的实现依赖于三个核心支柱:Fiber 架构、Scheduler(调度器) 和 Lane 模型。
1. 基础架构:Fiber (纤程)
这是并发的基石。在 React 16 重构中引入。
- 以前的 VDOM: 是递归调用的函数栈,一旦递归开始,无法中断。
- Fiber: 将组件树变成了链表结构。每个组件实例对应一个 Fiber 节点。因为是链表,React 可以在处理完任意一个 Fiber 节点后,停下来,保存现场,把控制权交还给浏览器。
2. 执行机制:双重缓冲 (Double Buffering)
React 维护两棵 Fiber 树:
- Current Tree: 屏幕上当前显示的内容。
- Work-in-Progress Tree (WIP): 正在后台构建的新树。 React 在 WIP 树上进行计算(Render 阶段)。因为是在内存中计算,还没操作真实 DOM,所以这个过程可以随时暂停、废弃。只有当 WIP 树全部计算完成,React 才会一次性将其切换为 Current Tree 并更新 DOM(Commit 阶段,这个阶段不可中断)。
3. 动力引擎:Scheduler (调度器)
React 自己实现了一套类似于操作系统的任务调度系统。
- 它利用
MessageChannel(或requestIdleCallback的 polyfill)来实现时间切片(Time Slicing)。 - 默认情况下,React 会分配给当前任务 5ms 的时间片。时间到了,无论活干没干完,都要把主线程还给浏览器,防止卡顿。
4. 优先级管理:Lane 模型
在 React 18 中,使用二进制位(Bitmaps)来表示任务的优先级,这被称为 Lane 模型。
- 高优先级: 用户的点击、键盘输入(Discrete Event)。
- 低优先级: 数据请求返回后的渲染、大量列表的过滤渲染。 如果一个低优先级任务正在执行(比如渲染一个巨大的列表),突然用户输入了一个字(高优先级),Scheduler 会打断低优先级任务,插队执行高优先级任务,等用户舒服了,再回去处理列表。
三、 有什么用处?(Why)
你可能会问:“原理听起来很牛,但对我写业务代码有什么实际帮助?”
并发模式解锁了 React 18+ 的几个核心 Hook 和特性,解决了传统 SPA 开发中的痛点。
1. useTransition:解决“输入卡顿”
场景: 你有一个搜索框,下面是一个包含 10,000 条数据的列表。以前,用户输入 "a",React 开始渲染列表,界面卡死。用户想删掉 "a",但因为主线程被阻塞,键盘按不出来。
并发模式下:
const [isPending, startTransition] = useTransition();
// 高优先级:更新输入框的值(让用户觉得丝滑)
setInputValue(input);
// 低优先级:更新列表(可以慢一点,可以被打断)
startTransition(() => {
setSearchQuery(input);
});
效果: 输入框永远保持响应,列表的渲染在后台进行,不会阻塞打字。
2. useDeferredValue:值的“防抖/节流”升级版
它类似于 useTransition,但是针对的是值。它允许 React “延迟”更新某个耗时的部分,而优先更新其他部分。
3. 更好的 Suspense 体验 (Render-as-you-fetch)
以前,我们必须等数据回来了(await),才能开始渲染组件。
现在,React 可以在数据还在请求时,先尝试渲染组件。如果发现数据没好,它会“挂起”(Suspend)这个组件的渲染,先去渲染其他的 UI(比如骨架屏),等数据好了再自动恢复渲染。这一切的调度都依赖于并发机制。
4. 流式 SSR (Streaming SSR)
因为渲染是可中断、可分块的。服务器端渲染不再需要等整个 HTML 生成完才发送给浏览器。React 可以先把“外壳”(Header, Sidebar)发送给浏览器显示,等耗时的部分(比如评论列表)生成好了,再通过流的方式“塞”到页面里。这就是 React Server Components 的基础。
总结 (作为 Senior Dev 的观点)
React 并发模式 并不是一个新的 API,而是 React 引擎的一次从“同步阻塞”到“异步可中断”的底层换心手术。
- 原理: 利用 Fiber 实现可中断渲染,利用 Scheduler 实现时间切片和优先级调度。
- 核心价值: CPU 密集型任务不再阻塞用户的交互。 它让 React 应用在复杂的计算负载下,依然能保持 60fps 的流畅交互体验。
对于开发者来说,最大的变化是你拥有了控制渲染优先级的能力(通过 startTransition 等),从而能够构建出体验上限更高的应用。