Appearance
理念篇
第一章 React 理念
React 理念
React 的主要目标是构建快速响应的大型 Web 应用程序。但在实际应用中,两个主要的瓶颈阻碍了快速响应:CPU 瓶颈和 IO 瓶颈。这篇文章旨在探讨 React 是如何解决这两个问题的。
CPU 瓶颈与时间切片
在大型项目中,渲染大量组件可能会导致 CPU 瓶颈。一个明显的例子是渲染 3000 个 li 元素的列表。由 于 JS 和 GUI 渲染线程是互斥的,长时间的 JS 执行会导致页面卡顿。
React 解决这个问题的方法是通过 时间切片(Time Slicing)。它将长任务分解成多个小任务,在每一帧 的时间中只执行一小部分任务。 这样,浏览器就有时间执行样式布局和样式绘制,减少了卡顿的可能性。
例如,使用 ReactDOM.unstable_createRoot 开启 Concurrent Mode 可以启用时间切片。
js
ReactDOM.unstable_createRoot(rootEl).render(<App />);ReactDOM.unstable_createRoot(rootEl).render(<App />);IO 瓶颈与 Suspense
网络延迟是另一个无法完全避免但可以最小化的问题。React 的解决方案是通过 Suspense 和 useDeferredValue 来优化用户体验。
例如,在加载数据时,React 会先保持当前界面一小段时间。如果这段时间足够短,用户几乎不会感到延迟。如果 加载时间较长,React 会显示 Loading 效果。
js
const data = useDeferredValue(fetchData(), { timeoutMs: 2000 });const data = useDeferredValue(fetchData(), { timeoutMs: 2000 });总结
React 的核心理念是提供快速响应的用户体验。为了实现这一目标,React 采用了时间切片来解决 CPU 瓶颈, 以及使用 Suspense 和 useDeferredValue 来解决 IO 瓶颈。 这两种方法都是通过将同步更新转换为可中断 的异步更新来实现的。
通过这些高级特性和优化,React 成功地解决了构建大型快速响应 Web 应用中的主要瓶颈问题。
老的 React 架构
React 15 的架构
在 React 15 中,整个架构主要分为两个部分:
- Reconciler(协调器): 负责找出需要更新的组件。
- Renderer(渲染器): 负责把这些更新实际渲染到页面或其他宿主环境。
Reconciler 的工作流程:
- 调用组件的
render方法生成新的虚拟 DOM。 - 将新的虚拟 DOM 与旧的虚拟 DOM 进行对比。
- 找出变化的部分。
- 通知 Renderer 进行实际的 DOM 更新。
Renderer 的多样性:
- ReactDOM: 用于浏览器环境。
- ReactNative: 用于移动应用。
- ReactTest: 用于测试。
- ReactArt: 用于 Canvas, SVG 或 VML。
React 15 架构的问题
- 递归更新: 在 React 15 的 Reconciler 中,更新是通过 递归 的方式进行的。这意味着一旦更新开 始,就 不能中断 ,直到所有子组件都更新完毕。
- 用户交互卡顿: 由于更新不能中断,如果组件树很深,更新可能会超过 16 毫秒,导致用户界面卡顿。
- 不支持异步更新: React 15 的架构是同步的,无法很好地支持可中断的异步更新。
由于上述问题,React 团队决定进行架构重构,从而更好地支持异步更新和提高用户体验。这也是 React 从版本 15 升级到版本 16 的主要动力。
新的 React 架构
React 16 引入了一些新特性和优化,使其更高效和灵活。这里,我们重点介绍三个核心部分 :Scheduler(调度器)、Reconciler(协调器)和 Renderer(渲染器)。
Scheduler(调度器)
在 React 16 中,Scheduler 是一个新成员。这是一种智能机制,负责确定何时执行哪些任务。它可以基于任 务的优先级,有效地安排高优先级任务先进入到 Reconciler 阶段。
Scheduler 用自己的机制取代了浏览器的 requestIdleCallback,因为后者存在浏览器兼容性问题和触发 频率不稳定的问题。
Reconciler(协调器)
Reconciler 在 React 16 中得到了彻底的重写。与 React 15 的递归方式不同,新的 Reconciler 采用了可中 断的循环过程。每次循环都会检查是否有剩余时间,从而更加高效地处理虚拟 DOM。
其中,React 16 引入了 Fiber 架构,使得 Reconciler 可以更高效地标记和管理虚拟 DOM 的变更。例如,它 会给变化的虚拟 DOM 打上增加、删除或更新的标记。
Renderer(渲染器)
最后一步是 Renderer,它负责根据 Reconciler 的标记,同步地将这些变更应用到实际的 DOM 上。
更新流程和中断
整个更新流程可能会因为两个原因被中断:
- 有其他更高优先级的任务。
- 当前帧没有剩余时间。
但因为所有这些工作都是在内存中完成的,所以即使过程被反复中断,用户也不会看到不完整的 DOM 更新。
总结
通过这个新的架构,React 16 实现了更高效和灵活的 DOM 更新。Scheduler 的引入使任务更优先级化,新的 Reconciler 则通过 Fiber 架构更精确地处理虚拟 DOM。最后,Renderer 保证了所有的变更都能准确地反映到实 际 DOM 上。这三者共同为开发者提供了一个更优秀的工具集,使得复杂应用的开发更为简单和高效。
Fiber 架构的心智模型
什么是代数效应
代数效应(Algebraic Effects) 是函数式编程中一个重要的概念,用于把函数内的副作用(如 IO 操作、状 态变更等)从函数的主要逻辑中分离出去。
问题背景
假设有一个函数 getTotalPicNum ,该函数接收两个用户名,然后获取这两个用户在某个平台上保存的图片 数量,并将这两个数量加在一起。
js
function getTotalPicNum(user1, user2) {
const picNum1 = getPicNum(user1);
const picNum2 = getPicNum(user2);
return picNum1 + picNum2;
}function getTotalPicNum(user1, user2) {
const picNum1 = getPicNum(user1);
const picNum2 = getPicNum(user2);
return picNum1 + picNum2;
}在该函数内部,调用了另一个函数 getPicNum ,这是一个异步操作,如何处理这种情况呢?
代数效应的模拟实现
假想有一种**try...handle语法与perform、resume**操作符。
js
function getPicNum(name) {
const picNum = perform name;
return picNum;
}
try {
getTotalPicNum('kaSong', 'xiaoMing');
} handle (who) {
switch (who) {''
case 'kaSong':
resume with 230;
break;
case 'xiaoMing':
resume with 122;
break;
default:
resume with 0;
break;
}
}function getPicNum(name) {
const picNum = perform name;
return picNum;
}
try {
getTotalPicNum('kaSong', 'xiaoMing');
} handle (who) {
switch (who) {''
case 'kaSong':
resume with 230;
break;
case 'xiaoMing':
resume with 122;
break;
default:
resume with 0;
break;
}
}这里, perform关键字会暂停函数的执行,跳出到最近的try...handle结构中。resume 关键字用于继 续执行被暂停的函数,并注入一个新的值。
代数效应与 React
代数效应与 React 的联系主要体现在 Hooks(如 useState、useEffect 等)。
js
function App() {
const [num, updateNum] = useState(0);
return <button onClick={() => updateNum(num => num + 1)}>{num}</button>;
}function App() {
const [num, updateNum] = useState(0);
return <button onClick={() => updateNum(num => num + 1)}>{num}</button>;
}在使用 Hooks 时,我们不需要关注状态是如何保存和更新的,这是代数效应思想的体现。
代数效应与 Generator
Generator 有传染性并且执行的中间状态是上下文关联的,这两点限制了其在 React 内部的应用。
代数效应与 Fiber
Fiber 是 React 内部实现的一套状态更新机制,它支持任务不同优先级、可中断与恢复,并且能够复用之前的 中间状态。 这种设计哲学与代数效应非常相似。
js
// 简化版Fiber结构
{
stateNode: null, // 对应的DOM节点或组件实例
child: null, // 第一个子Fiber
sibling: null, // 下一个兄弟Fiber
return: null, // 父Fiber
}// 简化版Fiber结构
{
stateNode: null, // 对应的DOM节点或组件实例
child: null, // 第一个子Fiber
sibling: null, // 下一个兄弟Fiber
return: null, // 父Fiber
}总体来说,代数效应是一个极具潜力的编程模型,尤其在处理异步操作和状态管理时具有显著优势。通过 React Hooks 和 Fiber,我们可以看到代数效应如何在现实项目中得到应用。
Fiber 架构的实现原理
Fiber 在 React 中的起源与作用
起源
Fiber 架构的提出是为了解决 React 15 及以前版本中的一些性能问题。在 React 15 及其之前的版本中,虚拟 DOM 的生成和更新是通过 Reconciler 的递归算法实现的,而这种递归过程是不能被中断的。当组件树层级很深 时,递归会占用大量的计算时间,导致 UI 渲染出现卡顿。
为了解决这个问题,React 16 引入了全新的 Fiber 架构。与之前不可中断的递归更新不同,Fiber 允许异步的、 可中断的更新。
含义与作用
Fiber 主要有三层含义:
- 作为架构 :React 16 的 Reconciler 是基于 Fiber 节点实现的,与 React 15 的基于递归调用栈 (Stack Reconciler)有明显的不同。
- 作为静态的数据结构 :每个 Fiber 节点对应一个 React 元素,并保存了该组件的 类型(如函数组 件、类组件或原生组件)以及相应的 DOM 节点等信息。
- 作为动态的工作单元 :每个 Fiber 节点还保存了与本次更新有关的状态变更、需要执行的工作等。
结构
每个 Fiber 节点都有多个属性,这些属性按照上面的三层含义分类:
js
function FiberNode(tag, pendingProps, key, mode) {
// 静态数据结构的属性
this.tag = tag;
this.key = key;
// 等等
// 连接其他Fiber节点
this.return = null;
this.child = null;
this.sibling = null;
// 动态工作单元的属性
this.pendingProps = pendingProps;
// 等等
}function FiberNode(tag, pendingProps, key, mode) {
// 静态数据结构的属性
this.tag = tag;
this.key = key;
// 等等
// 连接其他Fiber节点
this.return = null;
this.child = null;
this.sibling = null;
// 动态工作单元的属性
this.pendingProps = pendingProps;
// 等等
}在 Fiber 树中,Fiber 节点通过三个属性连接:
this.return指向父级 Fiber 节点this.child指向第一个子 Fiber 节点this.sibling指向右边的第一个兄弟 Fiber 节点
以一个简单的组件 App 为例:
js
javascriptCopy code
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
)
}javascriptCopy code
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
)
}这个组件的 Fiber 树包含一个**<div>节点和一个<span>节点。<div>的child指向<span>, 而<span>的return指向<div>**。
总结
Fiber 架构不仅解决了 React 中长久以来的性能问题,还提供了一种更灵活的方式来管理组件的更新。通过 Fiber,React 能更有效地进行 DOM 的更新和渲染。在后续的讨论中,我们还会详细介绍 Fiber 如何影响 React 的整个更新和渲染流程。
希望这篇文章能让您对 React 中的 Fiber 架构有更深入的了解。
Fiber 架构的工作原理
Fiber 树与 DOM 树
在 React 中,Fiber 树是 DOM 树的内存表示。每个 Fiber 节点都保存了对应的 DOM 节点信息。
双缓存(Double Buffering)
双缓存是一种在内存中构建内容并直接替换的技术。这在动画和其他高性能渲染场景中很有用,因为它减少了视觉 上的闪烁和延迟。
js
// 用于清除上一帧和绘制新一帧的canvas代码示例
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除上一帧
ctx.drawImage(offscreenCanvas, 0, 0); // 绘制新一帧// 用于清除上一帧和绘制新一帧的canvas代码示例
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除上一帧
ctx.drawImage(offscreenCanvas, 0, 0); // 绘制新一帧双缓存在 React 中的应用
React 使用这种双缓存技术通过两棵 Fiber 树来实现 DOM 的高效更新。这两棵树分别是:
- current Fiber 树: 对应于当前在屏幕上显示的内容。
- workInProgress Fiber 树: 正在内存中构建的新的树。
这两个树通过**alternate**属性连接:
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;更新流程
- 首次渲染(Mount 时): **
fiberRootNode是整个 React 应用的根节点,而rootFiber**是具体组件树 的根节点。首次渲染时,会在内存中构建一个新的 workInProgress Fiber 树。
js
function App() {
const [num, add] = useState(0);
return <p onClick={() => add(num + 1)}>{num}</p>;
}
ReactDOM.render(<App />, document.getElementById("root"));function App() {
const [num, add] = useState(0);
return <p onClick={() => add(num + 1)}>{num}</p>;
}
ReactDOM.render(<App />, document.getElementById("root"));- 状态更新(Update 时) : 用户交互或其他操作触发状态变化时,React 会构建一个新的 workInProgress Fiber 树。这个新树与当前的 current Fiber 树进行 Diff 算法比较,然后确定哪些部分需要更新。
总结
通过 双缓存和 Fiber 架构 ,React 能够高效地更新 DOM。这个机制减少了不必要的渲染和视觉上的闪烁, 提高了性能。
第二章 前置知识
源码的文件结构
React 16 架构与源码结构
React 16 的架构分为三大核心组件:Scheduler(调度器)、Reconciler(协调器)和 Renderer(渲染器)。
Scheduler
Scheduler 负责管理和调度任务的优先级。高优先级的任务会先进入 Reconciler 进行处理。
Reconciler
Reconciler 负责识别哪些组件需要更新或改变,并构建相应的工作单元。
Renderer
Renderer 负责将 Reconciler 标记的需要更新或改变的组件渲染到界面上。
源码文件结构
在 React 的源码中,这三层架构如何体现呢?
顶层目录
fixtures: 包含给贡献者准备的小型 React 测试项目。packages: 包含 React 仓库中所有的 package 的源码和元数据。scripts: 包含各种工具链的脚本。
packages 目录
react: React 的核心代码,包括全局的 React API, 如**React.createElement、React.Component**等。scheduler: 实现 Scheduler 的代码。shared: 包含各模块公用的方法和全局变量。
Renderer 相关
react-dom: 这里同时包括 DOM 和服务端渲染(SSR)的代码。react-native-renderer: React Native 的 Renderer 代码。react-test-renderer和其他:用于特定测试和实验的 Renderer 实现。
实验性包
react-reconciler: 尽管这是一个实验性的包,但大部分源码学习将集中在这里。它是连接 Scheduler 和 Renderer 的关键组成部分。
辅助包
- 如**
react-is和react-fetch**等,用于特定的功能或测试。
总结
这个文件结构与架构设计紧密相关,不仅体现了 React 的灵活性,也方便了开发者更深入地理解和学习 React 的 内部工作原理。从**react-reconciler到scheduler**和各种 Renderer,我们可以看到 React 如何优雅地将 各个部分连接起来,构成一个高效和可扩展的系统。
调试源码
如何调试 React 的源码
对于深入了解 React 或贡献 React 代码的开发者来说,能够调试源码是一项非常有用的技能。本文将指导您如何 步骤化地实现这一目标。
获取源码
首先,从 Facebook 的 React 项目的 master 分支拉取最新源码。
bash
git clone <https://github.com/facebook/react.git>git clone <https://github.com/facebook/react.git>对于网络不佳的情况,可以考虑使用以下方案:
- 使用 cnpm 代理:
git clone <https://github.com.cnpmjs.org/facebook/react> - 使用码云的镜像:
git clone <https://gitee.com/mirrors/react.git>
安装依赖
进入 React 源码所在的文件夹,并使用**yarn**安装依赖。
bash
cd react
yarncd react
yarn打包 React
执行特定的打包命令,生成可以用于开发环境的 cjs 包。
bash
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODEyarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE使用 yarn link
**yarn link**可以改变项目中依赖包的目录指向,用于链接到您的本地 React 库。
bash
cd build/node_modules/react
yarn link
cd build/node_modules/react-dom
yarn linkcd build/node_modules/react
yarn link
cd build/node_modules/react-dom
yarn link创建新的 React 项目
使用**create-react-app**来创建一个新项目。
bash
npx create-react-app a-react-demonpx create-react-app a-react-demo链接到本地 React 库
在新项目中,使用**yarn link**指向之前创建的 React 和 ReactDOM 库。
bash
yarn link react react-domyarn link react react-dom测试
在**react/build/node_modules/react-dom/cjs/react-dom.development.js中添加打印语句,并在新项目下执 行yarn start**。此时,浏览器控制台应显示您添加的打印内容。
通过这一系列操作,您现在可以在一个由最新 React 源码支持的环境中进行开发和学习。这对于理解 React 的内 部工作原理以及为 React 项目做贡献是非常有价值的。
深入理解 JSX
什么是 JSX?
JSX 是 JavaScript 的一种扩展语法,让我们能用类似 HTML 的方式描述界面结构。在 React 里面,JSX 非常常 用。
js
const element = <h1>Hello, world!</h1>;const element = <h1>Hello, world!</h1>;JSX 的编译过程
JSX 不是纯粹的 JavaScript,所以需要通过 Babel 进行编译。编译后的代码会使用 React.createElement 函数。
js
// 编译前
<h1>Hello, world!</h1>;
// 编译后
React.createElement("h1", null, "Hello, world!");// 编译前
<h1>Hello, world!</h1>;
// 编译后
React.createElement("h1", null, "Hello, world!");你也可以通过**@babel/plugin-transform-react-jsx**插件改变编译后的函数调用。
React Element 是什么?
当 React.createElement 被调用时,它返回一个被称为 "React Element" 的对象。
js
const element = React.createElement("h1", null, "Hello, world!");const element = React.createElement("h1", null, "Hello, world!");这个对象包含了一些属性,比如:类型(type),属性(props)以及其他与元素相关的信息。
React Component vs React Element
React Component 是用于生成 React Element 的函数或类。当一个 React Component 被渲染时,它返回一个或 多个 React Element。
js
class AppClass extends React.Component {
render() {
return <h1>Hello, world!</h1>;
}
}
function AppFunc() {
return <h1>Hello, world!</h1>;
}class AppClass extends React.Component {
render() {
return <h1>Hello, world!</h1>;
}
}
function AppFunc() {
return <h1>Hello, world!</h1>;
}这里,<AppClass/> 和 <AppFunc/> 是 React Element,而 AppClass 和 AppFunc 是 React Component。
Fiber 节点是什么?
Fiber 节点是 React 内部用于追踪组件状态和属性的数据结构。它包括了很多其他信息,比如组件的优先级, 组件的状态,以及需要被渲染的标记等。
JSX 与 Fiber 节点的关系
JSX 只是描述组件应该如何渲染的一种方式,而 Fiber 节点则负责实现这一点。当组件首次挂载时,React 会 根据 JSX 生成对应的 Fiber 节点。
js
// JSX 描述了 UI 应该是什么样子
const jsxElement = <h1>Hello, world!</h1>;
// Fiber 节点包含了如何让 UI 变为那个样子的信息
const fiberNode = {
type: "h1"
// ...其他信息
};// JSX 描述了 UI 应该是什么样子
const jsxElement = <h1>Hello, world!</h1>;
// Fiber 节点包含了如何让 UI 变为那个样子的信息
const fiberNode = {
type: "h1"
// ...其他信息
};在组件更新时,React 将新的 JSX 和旧的 Fiber 节点进行对比,并据此更新 Fiber 节点。