← 返回 构建与部署图解 目录

⚛️ React代码到浏览器全链路

一个 .jsx 文件从写出到在浏览器里跑起来,和 Vue 有什么不同

① 源码:你写的 JSX 到底是什么

React 用 JSX 语法,不是 HTML,也不是标准 JS。浏览器完全不认识 <div> 这种写法出现在 JS 文件里——JSX 是 Babel 发明的语法糖,必须经过编译。

JSX 就像在中文文章里夹了英文单词。 文章整体是中文(JS),但中间突然出现英文(HTML-like语法)。出版前编辑(Babel)要把所有英文翻译成中文表达,最终读者(浏览器)看到的才是纯中文。
src/components/Counter.jsx
📂 src
📂 components
📄 Counter.jsx
📄 App.jsx
📄 index.js
import { useState } from 'react' import './Counter.css' function Counter() { const [count, setCount] = useState(0) return ( <div className="counter"> <h1>点击了 {count} 次</h1> <button onClick={() => setCount(count + 1)}> 点击 </button> </div> ) } export default Counter
React 文件和 Vue 的区别
.jsx / .tsxVue 用 .vue(三段式分开)。React 把 HTML、JS、CSS 混在一个 JS 文件里——JSX 写在 return() 里,CSS 用 import 引入
JSXVue 的 <template> 有独立标签。React 直接在 JS 里写 <div>,靠 Babel 编译成 React.createElement()
CSSVue 用 <style scoped>。React 通常用 import './xxx.css'(全局)或 CSS Modules(style.className),或 styled-components(CSS-in-JS)
为什么 React 用 className 不用 class?因为 class 是 JS 的保留字(ES6 的类)。JSX 本质是 JS,写 class="xxx" 会报语法错误。所以 React 要求用 className 代替。编译后最终还是生成 DOM 的 class 属性。

② 编译:Babel 把 JSX 变成了什么

这是 React 和 Vue 最大的区别:Vue 用专用编译器(@vitejs/plugin-vue),React 用通用编译器 Babel(或 SWC/esbuild)。Babel 只做一件事——把 JSX 翻译成 React.createElement() 调用。

JSX 源码 <div className="counter"> <h1>{count}次</h1> <button>点击</button> Babel / SWC @babel/preset-react JSX → createElement ES6+ → ES5(可选) 编译后的纯 JS(浏览器执行的) React.createElement("div", {className:"counter"}, React.createElement("h1", null, "点击了", count, "次"), React.createElement("button", {onClick: fn}, "点击") )
Babel 把 JSX 的每个标签翻译成 React.createElement() 调用——一个返回虚拟 DOM 对象的函数

新写法:jsx() 函数(React 17+)

编译后(新版自动运行时)
// React 17+ 不再直接 import React,而是用 jsx() 运行时 import { jsx as _jsx } from "react/jsx-runtime" function Counter() { const [count, setCount] = useState(0) return _jsx("div", { className: "counter", children: [ _jsx("h1", { children: ["点击了 ", count, " 次"] }), _jsx("button", { onClick: () => setCount(count + 1), children: "点击" }) ] }) }
旧版 vs 新版:React 16 及之前,每个 JSX 文件都要 import React from 'react'(因为编译后调用 React.createElement)。React 17+ 改为自动引入 jsx() 运行时,你不再需要在文件顶部写 import React

③ 打包:React 项目的 dist 长什么样

React 通常用 Webpack(CRA 脚手架)或 Vite。打包结果和 Vue 类似,但 chunk 拆分策略不同。

dist/ # Webpack 打包结果(CRA) ├── index.html # 入口HTML ├── static/ │ ├── js/ │ │ ├── main.a1b2c3d4.js # 主chunk:你的所有组件代码 │ │ ├── 2.chunk-5e6f7a8b.js # 懒加载的路由chunk(React.lazy) │ │ └── 3.chunk-9c0d1e2f.js # 另一个懒加载chunk │ ├── css/ │ │ └── main.b3c4d5e6.css # 所有CSS合并 │ └── media/ │ └── logo-f7a8b9c0.png # 图片资源 └── favicon.ico

React 的懒加载(React.lazy)

App.jsx (路由懒加载)
import { lazy, Suspense } from 'react' // React.lazy + import() → 单独打成 chunk const About = lazy(() => import('./pages/About')) const Dashboard = lazy(() => import('./pages/Dashboard')) function App() { return ( <Suspense fallback={<div>加载中...</div>}> <Routes> <Route path="/about" element={<About />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </Suspense> ) }
React 的 lazy() 等价于 Vue 的 () => import() 路由懒加载。但 React 必须包一层 <Suspense> 来处理加载中的状态(fallback)。Vue 不需要,路由自己处理。

④ React Fiber:虚拟 DOM 是怎么变成真实 DOM 的

这是 React 独有的概念。Vue 也有虚拟 DOM,但 React 16+ 重写了核心算法——Fiber 架构

Fiber 就像一个能中断的工作清单。 旧版 React 渲染一个大列表时,必须一口气渲染完,中间不能停——如果列表太大,页面会卡死(掉帧)。Fiber 把渲染工作拆成小任务,每次做完一小段就检查"时间够不够",不够就暂停让浏览器处理用户输入,然后继续。
React 渲染的两个阶段 Render 阶段(可中断) 纯计算,不操作真实DOM JSX → React Element 树 → Fiber 节点树 → Diff 对比变更 ⚡ 可中断 ⚡ 可分片 ⚡ 可优先级 ⚡ 低优先级让位 Commit 阶段(不可中断) 操作真实DOM,同步执行 根据 Diff 结果 → 插入/删除/更新真实DOM节点 → 触发 useEffect / useLayoutEffect → 浏览器绘制到屏幕
Render 阶段算出要做什么变更,Commit 阶段同步执行变更。Fiber 让 Render 阶段可以中断
React FiberVue 响应式
触发更新的方式setState/useState → 调度更新 → 重新 renderref.value = xxx → Proxy 拦截 → 派发更新
虚拟 DOM diffFiber 树对比,可中断分片静态提升 + Patch Flag,编译时已优化大部分
更新粒度组件级(组件 re-render)属性级(精确知道哪个属性变了)
性能靠 Fiber 分片避免卡顿靠编译时优化,不需要分片(量少)

⑤ F12:React 页面在浏览器里怎么看

F12 面板React 特有的
Elements看到渲染后的 DOM。React 不会像 Vue 加 data-v-xxx 属性(除非你用了 CSS Modules 的 .module.css)。DOM 结构和你写的 JSX 几乎一致
ConsoleReact 的警告(key 缺失、useEffect 依赖)在这里。错误堆栈指向 .jsx 文件
Sources能看到打包后的 main.[hash].js。有 SourceMap 时能看到 .jsx/.tsx 源码,直接打断点
Network首屏加载 main.[hash].js + chunk.[hash].js。React.lazy 触发时会多出 chunk 请求
React DevTools💡 额外装的浏览器插件!有专门的 Components 和 Profiler 面板——这是 Vue DevTools 的 React 版
必装工具:React DevTools 浏览器插件。安装后 F12 会多出两个面板:
Components 面板:像树一样看组件结构,点开能看到 props、state、hooks 的值
Profiler 面板:录制一段操作,看哪些组件渲染了、各花多长时间——性能优化必备

⑥ Vue vs React:编译和运行的核心区别

对比维度VueReact
源码文件.vue(template + script + style 三段式).jsx / .tsx(全在 JS 里)
编译器@vue/compiler-sfc(专用)Babel + @babel/preset-react(通用)
模板 → JS<div>_createVNode("div")<div>_jsx("div")
编译优化静态提升 + PatchFlag(编译时标记动态节点)不优化模板(JSX 太灵活,编译时无法分析)
运行时性能更新时只对比动态节点(量少)整个组件 re-render + Fiber diff(量大但可中断)
CSS 隔离<style scoped>(编译器加 data-v 属性)CSS Modules(.module.css)或 styled-components
SourceMap.vue → .js 映射.jsx → .js 映射
打包结果dist/assets/index-[hash].js + CSSdist/static/js/main.[hash].js + CSS(结构几乎一样)
一句话总结:Vue 是编译时优化派——编译器帮你做了很多性能优化(静态提升、PatchFlag),运行时工作量少。React 是运行时灵活派——JSX 太灵活编译时没法优化,但 Fiber 架构让大组件渲染不卡顿。两者殊途同归,最终产物都是"JS 操作真实 DOM"。

⑦ FAQ

为什么 React 文件里不需要 import React 了?(React 17+)

React 16 及之前,JSX 编译后调用 React.createElement(),所以必须 import React。React 17+ 改为编译后调用 react/jsx-runtime 里的 jsx() 函数,Babel 会自动引入,你的代码里不再需要手动 import React

但如果你用了 React.FragmentReact.memo 等 API,还是需要按需 import。

Hooks(useState/useEffect)是编译时还是运行时的?

纯运行时。Hooks 是 React 内部维护的一个链表结构——每次组件 render 时按顺序调用 useState/useEffect,React 按调用顺序把 hook 存在 Fiber 节点上。编译器不参与。

这就是为什么 Hooks 不能放在 if/for 里——调用顺序变了,React 就对不上了。

为什么 React 打包后有那么多 chunk 文件?

因为 React.lazy(() => import())import() 动态导入会让 Webpack/Vite 把这些代码拆成单独 chunk。你有多少个 lazy 组件,就有多少个 chunk 文件。用户访问对应路由时才下载。

可以用 webpack-bundle-analyzer 或 Vite 的 rollup-plugin-visualizer 可视化各 chunk 的内容。

Vite 可以用来构建 React 吗?

可以!npm create vite@latest my-app -- --template react 就行。Vite 开发时用 esbuild 编译 JSX(比 Babel 快 10-100 倍),生产构建用 Rollup 打包。CRA(create-react-app)已停止维护,现在 React 官方推荐用 Vite 或 Next.js。

← 返回 构建与部署图解 目录