React渲染与更新的基本原理与实现
完整代码: GitHub Gist
总览与环境搭建
模板: React 以 [JT]sx
作为模板, 无需自己实现编译模块.
响应式: React 对外暴露 useState
, 用户调用 setState
后 React 对组件做重新构建, 对比 vDOM 的变化并完成节点的替换
核心: React 的基本数据结构 Fiber 串起整个渲染流, 挂载, 更新等机制都通过 Fiber 链这一数据结构完成.
实验环境搭建
初始化项目
pnpm init
安装 Vite 用于 jsx 的编译与项目打包
pnpm i -D vite
创建入口文件
. ├── components │ └── Welcome.jsx ├── core │ ├── ReactDOM.js │ └── React.js ├── index.html ├── main.jsx ├── package.json └── pnpm-lock.yaml
实现简易挂载 (同步渲染, 同步挂载)
实现 index.html
我们需要一个组件的挂载容器, 引入 JS 的入口文件 main.jsx
<!doctype html>
<html lang="en">
<!--...-->
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>
实现 main.jsx
只需要实现获取挂载节点并挂载组件的操作
import ReactDOM from './core/ReactDOM';
import React from './core/React';
import Welcome from './components/Welcome';
ReactDOM.createRoot(document.getElementById('root')).render(Welcome);
在这里 import React from './core/React';
可能会报错引入但没有使用. 我们应该保留这段代码, 因为所有的JSX
Element 在编译后都会默认变成 React.createElement(nodeName, props, ...children)
如果不引入 React
编译后的代码就会报错找不到 createElement
暂时实现 Welcome.jsx
我们暂时没有实现函数式组件, 因此直接导出一个对象
import React from '../core/React';
export default (
<div>
<h1 id="title">
Hello World <span id="emoji">🤗</span>
</h1>
<p>This is a mini react</p>
Looks Cool !
</div>
);
在这里我们测试了:
- 嵌套 Element 的构建
- props 的构建
- TextNode 与 Element 混杂构建
实现 ReactDOM.js
需要实现 ReactDOM.createRoot
用于接受挂载根, 并暴露 render
方法用来接受根节点的组件
import React from './React.js';
const ReactDOM = {
createRoot(container) {
return {
render(app) {
React.render(app, container);
},
};
},
};
export default ReactDOM;
render
的实现交给 React.js
完成
实现 React.js
首先看一下 Component 的编译后代码
import React from "/core/React.js";
export default /* @__PURE__ */
// 创建最外层的 div, props 为空, 子元素有 h1, p, Looks Cool !
React.createElement("div", null, /* @__PURE__ */
// h1 元素, 有 id="title" 子元素 Hello World, emoji
React.createElement("h1", {
id: "title"
}, "Hello World ", /* @__PURE__ */
React.createElement("span", {
id: "emoji"
}, "🤗")), /* @__PURE__ */
React.createElement("p", null, "This is a mini react"), "Looks Cool !");
我们需要实现 createElement(nodeName, props, ...children)
创建 vDOM. vDOM 的数据结构为
{
nodeName: string|function,// 对于组件来说是 function, 对于 HTML Element 是标签名
props: {
attributeKey: value
children: []
}
}
注意到函数有如下重载:
props
: 可以为null
或者{k: v}
children
: 可以为String
,React.createElement()
我们先将重载问题处理掉
- 如果
props
是 null, 将 props 赋为{}
- 如果
children
String, 我们就将他包一层 vDOM
实现如下
function createElement(nodeName, props = {}, ...children) {
return {
nodeName,
props: {
...props,
children: children.map(formatNode),
},
};
}
再实现一个格式化节点功能处理 children
值为 String
的问题
function isTextNode(node) {
if (typeof node === 'function') return false; // 组件不是 TextNode
if (node === null || typeof node !== 'object') return true; // 考虑了 false 等非 string
const proto = Object.getPrototypeOf(node); // 考虑了 Array 等情况
return proto !== Object.prototype && proto !== null;
}
function formatNode(node) {
if (isTextNode(node))
return {
nodeName: 'TEXT_ELEMENT',
props: {
children: [`${node}`],
data: `${node}`,
},
};
node.props ??= {};
node.props.children ??= [];
node.props.children = Array.isArray(node.props.children)
? node.props.children
: [node.props.children];
node.props.children.forEach(formatNode);
return node;
}
最后实现 render(component, container)
函数交付给 ReactDOM 挂载. 渲染节点可以分为: DOM 创建, 绑定属性, 子节点处理, 挂载
function render(component, container) {
const componentFormatted = formatNode(component); // 格式化 String
const node = createNode(componentFormatted); // 创建 Node
patchProps(node, componentFormatted.props); // 绑定属性
fillNode(node, componentFormatted); // 填充子节点
container.append(node); // 挂载
}
// 创建节点: 将 TextNode 与 Element 分开
function createNode({ nodeName, props }) {
if (nodeName === 'TEXT_ELEMENT')
return document.createTextNode(props.children?.[0] || '');
return document.createElement(nodeName);
}
// 绑定属性: 将非 children 的属性绑定在 Element 上
function isEvent(k) {
return k.match(/^on[A-Z][a-zA-Z]*$/);
}
function getEventName(k) {
return k.replace(
/^on([A-Z])([a-zA-Z]*)$/,
(_, prefix, suffix) => prefix.toLowerCase() + suffix
);
}
function bindProps(node, k, v) {
if (isEvent(k)) node.addEventListener(getEventName(k), v);
else node[k] = v;
}
function patchProps(node, props) {
Object.keys(props).forEach((k) => {
if (k === 'children') return;
bindProps(node, k, v);
});
}
// 填充子节点: 如果当前节点是 TextNode: 将孩子合并为 String, 否则 render 子节点
function fillNode(node, { nodeName, props: { children } }) {
if (nodeName === 'TEXT_ELEMENT')
return (node.data = children.reduce((pre, cur) => pre + cur, ''));
children.forEach((childNode) => render(childNode, node));
}
引入 Fiber
的挂载 (异步渲染, 异步挂载)
- Fiber是什么: Fiber 是 React 的工作单元, 对应 React 中的一个组件, Fiber 上不仅包含组件 vDOM, 还包含了渲染与更新的上下文
- 为什么引入 Fiber: 目前的 React 挂载是同步实现的, 渲染非常重的页面会造成同步代码阻塞. 为了减少同步代码阻塞, React 16 引入了基于 Fiber 的组件异步渲染与挂载并基于 Fiber 实现了任务优先级调度.
- 如何实现基于 Fiber 的挂载: Fiber 以链表的形式构造, 在
render
的时候构造 Root Fiber, 渲染 Root Fiber 后 React 将指针切换到下一个 Fiber 渲染下一个 Fiber 中的组件 - 如何实现异步渲染与挂载:
- 异步渲染: React 使用
requestIdleCallback
向全局注册一个闲时回调, 只有在当前没有任务需要执行的时候浏览器才会调用 React 执行 Fiber 的渲染. 一旦有代码需要执行, React 会停止渲染, 保存当前的 Fiber 指针, 知道下一次全局空闲时执行 - 异步挂载: Fiber 在渲染后并不会直接挂载到全局, 只有当前全局 Fiber 全部执行完才会统一提交挂载. 这样一来减少了 DOM 重绘次数, 二来防止页面渲染了一半就展示给用户
- 异步渲染: React 使用
Fiber 的数据结构与 Fiber 链表
vDOM 是一棵树, Fiber 链是一个链表, 使用 DFS 序将树转为链表. 一个 Fiber 的数据结构为
{
// 记录渲染信息
dom, // 这个 vDOM 对应的 DOM
component, // Fiber 对应的 vDOM
// 记录树上信息
parentFiber, // 父 Fiber
firstChildFiber, // 第一个孩子对应 Fiber
siblingFiber, // 右兄弟组件对应 Fiber
}
为了减少递归, 我们不会在 render
的时候计算整个链表, 而是在处理 fiber 的时候动态计算链表
Fiber Loop 的实现
在全局定义一个 nextFiber
存储接下来要处理的 Fiber. 每次 React 在空闲渲染的时候就从这个 nextFiber
开始渲染
let nextFiber = null; // 接下来要处理的 Fiber
// 用户提交 render 请求的时设置接下来要处理的 Fiber
function render(component, container) {
// 这个 Fiber 比较特殊, 我们手动指定了 dom 为 container, 即 component 的父节点.
// 这是因为根节点是一个组件, 组件只能 return 一个 child 作为根. 我们可以跳过组件的渲染, 直接渲染他唯一的子组件
// 将本来的: container -> 根组件 -> 根组件唯一的 child
// 变成: container -> 根组件唯一的 child
nextFiber = {
dom: container,
component,
parentFiber: null,
firstChildFiber: null,
siblingFiber: null,
};
}
function performFiberLoop(idleDeadline) {
// 如果预计空闲时间 >1 有需要处理的 Fiber
while (idleDeadline.timeRemaining() > 1 && nextFiber) {
// 处理 Fiber
performFiber(nextFiber);
}
requestIdleCallback(performFiberLoop);
}
// 在空闲时候调用 performFiberLoop 完成 Fiber Loop
requestIdleCallback(performFiberLoop);
处理单个 Fiber (异步渲染, 同步挂载)
实现单个 Fiber 的处理过程与实现无 Fiber 时的 render
类似. 在 render
外我们在处理当前 Fiber 时还构造了孩子的 Fiber 并确定了孩子 Fiber 之间的关系
// 5. 处理当前 Fiber 的孩子
function processChildFiber(fiber, children) {
// Text Node 是最小单元了, 不处理 Text Node 的孩子
if (fiber.component.nodeName === 'TEXT_ELEMENT') return;
// 没有孩子直接返回
if (!children.length) return;
// 遍历所有孩子, cur = 当前孩子, prev = 当前孩子的左兄弟
children.reduce((prev, cur) => {
// 为当前孩子创建 Fiber
const curFiber = {
dom: null, // DOM 还没创建
component: cur,
parentFiber: fiber,
firstChildFiber: null,
siblingFiber: null,
};
// 如果是第一个孩子, 刷新父亲的第一个孩子信息
if (!prev) fiber.firstChildFiber = curFiber;
// 否则左兄弟的右兄弟就是当前 Fiber
else prev.siblingFiber = curFiber;
return curFiber;
}, null);
}
// 4. 获取下一个孩子
function getNextFiber(fiber) {
// 4.1. DFS 优先找孩子
if (fiber.firstChildFiber) return fiber.firstChildFiber;
// 4.2. 没有孩子找右兄弟
if (fiber.siblingFiber) return fiber.siblingFiber;
// 4.3. 没有兄弟则回溯到父亲节点, 找父亲的右兄弟, 如果没有兄弟就继续回溯
let grandFiber = fiber.parentFiber;
while (grandFiber) {
if (grandFiber.siblingFiber) return grandFiber.siblingFiber;
grandFiber = grandFiber.parentFiber;
}
// 4.4. 都没有就说明 Fiber 链执行完毕
return null;
}
// 3. 对于 FC, 他本身不需要渲染, 只需要将他的孩子加入他的父容器就可以
function processFunctionComponentFiber(fiber) {
// 执行函数, 获取组件
const component = fiber.component.nodeName(fiber.component.props);
// 生成子 Fiber, 绑定链表关系
processChildFiber(fiber, [component]);
}
// 2. 对于非 FC
function processHostFiber(fiber) {
// 针对 render 的 根组件特殊 Fiber 的处理
if (!fiber.dom) {
fiber.dom = createNode(fiber.component);
patchProps(fiber.dom, fiber.component.props);
// 完成同步挂载
// TODO: 未来要修改为异步挂载
fiber.parentFiber.dom.append(fiber.dom);
}
// 生成子 Fiber, 绑定链表关系
processChildFiber(fiber, fiber.component.props.children);
}
// 判断是不是 FC
function isFunctionComponent(component) {
return component.nodeName instanceof Function;
}
// 1. 处理单个 Fiber
function performFiber(fiber) {
// 针对 FC 与 非 FC 分开处理
if (isFunctionComponent(fiber.component))
processFunctionComponentFiber(fiber);
else processHostFiber(fiber);
// 获取 DFS 序链表中下一元素, 移动指针
nextFiber = getNextFiber(fiber);
}
实现异步挂载 (统一提交)
我们希望在 Fiber 全部执行完成后再实现节点的挂载, 也就是说在计算全部完成后重新遍历 Fiber 链完成 DOM 操作
首先删除同步挂载功能
function processHostFiber(fiber) {
if (!fiber.dom) {
fiber.dom = createNode(fiber.component);
patchProps(fiber.dom, fiber.component.props);
- fiber.parentFiber.dom.append(fiber.dom);
}
processChildFiber(fiber, fiber.component.props.children);
}
- function fillNode(node, { nodeName, props: { children } }) {
- if (nodeName === 'TEXT_ELEMENT')
- return (node.data = children.reduce((pre, cur) => pre + cur, ''));
-
- children.forEach((childNode) => render(childNode, node));
- }
持久化一个全局的 rootFiber, 用来记录这次 render 的时候的头 Fiber, 用于下次遍历
+ let rootFiber = null;
function render(component, container) {
nextFiber = {
dom: container,
component,
parentFiber: null,
firstChildFiber: null,
siblingFiber: null,
};
+ // 保存这次 render 的头 Fiber 方便提交的时候再次遍历链表
+ rootFiber = nextFiber;
}
在全部 Fiber 执行完成后调用提交函数执行全部 DOM
function performFiberLoop(idleDeadline) {
while (idleDeadline.timeRemaining() > 1 && nextFiber) {
performFiber(nextFiber);
}
+ // 已经没有要处理的 Fiber 了 (Fiber 执行完成 / 当前 Callback 没有任务) &&
+ // 当前 render 还没有结束, 不是没有任务的 callback => 提交并重置 rootFiber
+ if (!nextFiber && rootFiber) {
+ applySubmit(getNextFiber(rootFiber));
+ rootFiber = null;
+ }
requestIdleCallback(performFiberLoop);
}
提交的过程是对 Fiber 的第二次遍历, 我们每次提交以 Fiber
- 如果当前 Fiber 是 FC: FC 组件本身不渲染节点, 我们直接处理他的子 Fiber. 例如
const TextFC = ()=>(<div>text</div>) <div id="container"><TestFC /></div> // 对应的函数是 React.createElement( // <- 这个 vDOM 并不会渲染为 DOM TextFC, {}, React.createElement('div', {}, 'text') // <- 我们需要将这个 vDOM 渲染到 container 上 )
- 如果当前 Fiber 不是 FC: 我们应该将他的子节点挂载到他的 DOM 上, 如果他本身没有 DOM, 就挂载在他父亲身上(如果父亲也没有, 就继续回溯)
// 将当前 fiber 中的 DOM 加入最近的有 DOM 的祖先中
function appendDOM(fiber) {
let containerFiber = fiber.parentFiber;
while (!containerFiber.dom) containerFiber = containerFiber.parentFiber;
containerFiber.dom.append(fiber.dom);
}
// 切换到下一个 Fiber 并更新指针
function switchFiber(fiber) {
const nextFiber = getNextFiber(fiber);
return nextFiber && applySubmit(nextFiber);
}
// 提交挂载
function applySubmit(fiber) {
// 如果是 FC 直接切换处理子节点
if (isFunctionComponent(fiber.component)) return switchFiber(fiber);
// 如果不是 FC 就将当前 Fiber 挂载起来
appendDOM(fiber);
// 切换 Fiber
switchFiber(fiber);
}
实现更新 (新增节点)
更新的基本逻辑是重新渲染一份新的 Fiber 链, 在每一个新创建的 Fiber 上记录这个 Fiber 对应的老 Fiber (alternate Fiber), 对比新老 Fiber 确定更新的具体操作
例如, 当页面中新增了一个节点时
graph LR subgraph Fiber Fiber1 --> Fiber2 ----> Fiber3 end subgraph newFiber newFiber1 --> newFiber2 --> newFiber4 --> newFiber3 end newFiber1 -.alternate.-> Fiber1 newFiber2 -.alternate.-> Fiber2 newFiber3 -.alternate.-> Fiber3
我们会发现 Fiber 链表中多了一个 newFiber4
并且这个 newFiber4
没有 alternate Fiber, 这就意味着 newFiber4
是一个新增的节点, 我们将其加入即可
新增变量记录老队列队头
用户手动调用 React.update()
发起更新, 在更新时我们需要分别记录新链表和老链表的头, 因此我们要创建全局变量记录老链表.
我们已经实现过
nextFiber
: 为实现异步任务调度, 在全局注册的指向当前要处理的 FiberrootFiber
: 为实现统一提交, 在全局注册的记录的正在处理的链表的根 Fiber
我们定义一个新的 Fiber
currentRootFiber
: 表示在这次更新之前的根节点 (即将要被取代的链表的链表头)
将 rootFiber
改名为 wipRootFiber
. 这样我们就有了三个全局变量
nextFiber
: 指向当前要挂载/更新的 FiberwipRootFiber
: 挂载轮次中要挂载的 Fiber 链表的根 Fiber / 更新轮次中新 Fiber 链表的根 FibercurrentRootFiber
: 当前更新轮次中老的链表的根 Fiber
扩充 Fiber 属性
- 新增属性
alternateFiber
记录新 Fiber 对应的老 Fiber. 如果alternateFiber
存在, 说明是在更新链上的 - 新增属性
effectTag
标记统一提交时应该如何处理 Fiber (如果是placement
表示要新挂载,update
表示 DOM 已经存在, 只需要更新)
{
dom,
component,
parentFiber,
firstChildFiber,
siblingFiber,
// 新增属性
alternateFiber, // 新 Fiber 对应的老 Fiber
effectTag: 'update' | 'placement' // 统一提交时应该如何处理
}
重命名wipRootFiber
- let rootFiber = null;
+ let wipRootFiber = null;
function render(component, container) {
nextFiber = {
dom: container,
component,
parentFiber: null,
firstChildFiber: null,
siblingFiber: null,
};
- rootFiber = nextFiber;
+ wipRootFiber = nextFiber;
}
function performFiberLoop(idleDeadline) {
while (idleDeadline.timeRemaining() > 1 && nextFiber) {
performFiber(nextFiber);
}
- if (!nextFiber && rootFiber) {
- applySubmit(getNextFiber(rootFiber));
- rootFiber = null;
- }
+ if (!nextFiber && wipRootFiber) {
+ applySubmit(getNextFiber(wipRootFiber));
+ currentRootFiber = wipRootFiber; // 提交完成后刷新上一轮的 Root Fiber
+ wipRootFiber = null;
+ }
requestIdleCallback(performFiberLoop);
}
构造新的 Fiber
调用函数, 构造一个新的 Fiber 链的头, 将这个头设为下一个需要执行的 Fiber
// 存储上一轮 Fiber 头
let currentRootFiber = null;
function update() {
// 如果还没有挂载, 就直接返回
if (!currentRootFiber) return;
// 新建 Fiber 头, 新 Fiber 头继承上一轮的 Fiber 头属性, 同时加入 alternateFiber 表示这个新 Fiber 对应的老 Fiber
wipRootFiber = {
...currentRootFiber,
alternateFiber: currentRootFiber,
};
// 刷新下一个需要执行的 Fiber 等待 FiberLoop 的调用
nextFiber = wipRootFiber;
}
对于仅有新节点增加的情况, 统一提交的时候页面会新增 Fiber 链表中新加入的节点
实现更新 (同类型节点更新)
当一个新 Fiber 存在老 Fiber 的时候我们需要对其 DOM 更新. 我们无法修改 DOM 的 Node Name, 所以我们只对同类型节点做更新.
更新的时候考虑三个属性:
nodeName
: 保证相同, 无需更新props
: 需要更新children
: 无需在本轮次更新, 因为下一个 Fiber 就是当前 Fiber 的孩子, 孩子们会在之后的 Fiber 处理中完成更新. 这与 Vue 的处理逻辑不同, Vue 在更新元素的时候要递归处理子元素, 但是因为 React 是链式处理, 已经将 DFS 转换为一条链. 因此我们只需要聚焦节点, 关心当前节点的变换
因此在更新时, 我们只需要考虑 props
的更新 (或者说 React 对 Fiber 的更新是 shadow 的)
实现同类节点判定
function isSameType(component, alternateComponent) {
return component.nodeName === alternateComponent?.nodeName;
}
实现节点更新任务标注
创建新根组件 Fiber 后, 我们还需要修改子组件 Fiber 的实现 (将 processChildFiber
改名为 reconcileChildren
扩充功能).
- function processChildFiber(fiber, children) {
+ function reconcileChildren(fiber, children) {
if (fiber.component.nodeName === 'TEXT_ELEMENT') return;
+ // alternateFiber 是父 fiber 的第一个孩子, 将来将作为 cur 对应的 Fiber 同步步进
+ let alternateFiber = fiber?.alternateFiber?.firstChildFiber;
children.reduce((prev, cur) => {
const curFiber = {
dom: null,
component: cur,
parentFiber: fiber,
firstChildFiber: null,
siblingFiber: null,
+ effectTag: null,
};
+ // 老 Fiber 存在, 说明是更新, 这里我们处理同类型更新
+ if (alternateFiber && isSameType(cur, alternateFiber.component)) {
+ curFiber.dom = alternateFiber.dom;
+ curFiber.effectTag = 'update';
+ curFiber.alternateFiber = alternateFiber;
+ // 老 Fiber 不存在, 要么是不同类型, 要么是在挂载阶段, 要么是更新出了全新节点
+ } else {
+ curFiber.effectTag = 'placement';
+ }
if (!prev) fiber.firstChildFiber = curFiber;
else prev.siblingFiber = curFiber;
+ // 如果 alternate Fiber 存在, 那么切换 alternate Fiber 使其与 cur 的切换一致
+ if (alternateFiber) alternateFiber = alternateFiber?.siblingFiber;
return curFiber;
}, null);
}
修改函数调用
function processFunctionComponentFiber(fiber) {
const component = fiber.component.nodeName(fiber.component.props);
- processChildFiber(fiber, [component]);
+ reconcileChildren(fiber, [component]);
}
function processHostFiber(fiber) {
if (!fiber.dom) {
fiber.dom = createNode(fiber.component);
patchProps(fiber.dom, fiber.component.props);
}
- processChildFiber(fiber, fiber.component.props.children);
+ reconcileChildren(fiber, fiber.component.props.children);
}
实现属性更新
在统一提交时我们只需要更新节点上的 props, 所以重写 patchProps
function removeProps(node, k, v) {
if (isEvent(k)) node.removeEventListener(getEventName(k), v);
else node[k] = null;
}
function patchProps(node, props, oldProps = {}) {
// 以默认参数提交老的 props
const oldKey = Object.keys(oldProps);
const newKey = Object.keys(props);
// 对于全新的 key 直接绑定
newKey
.filter((k) => k !== 'children' && !oldKey.includes(k))
.forEach((k) => bindProps(node, k, props[k]));
// 部队不存在的 key 直接删除
oldKey
.filter((k) => k !== 'children' && !newKey.includes(k))
.forEach((k) => removeProps(node, k, oldProps[k]));
// 对于有变更的 key 移除再新增
newKey
.filter(
(k) => k !== 'children' && oldKey.includes(k) && oldProps[k] !== props[k]
)
.forEach((k) => {
removeProps(node, k, oldProps[k]);
bindProps(node, k, props[k]);
});
}
在统一提交时候根据 effectTag
判断是要挂载还是只更新
function applySubmit(fiber) {
if (isFunctionComponent(fiber.component)) return switchFiber(fiber);
appendDOM(fiber);
switchFiber(fiber);
}
function applySubmit(fiber) {
if (isFunctionComponent(fiber.component)) return switchFiber(fiber);
- appendDOM(fiber);
+ // 如果是挂载阶段或者是更新全新节点, DOM
+ if (fiber.effectTag === 'placement') {
+ appendDOM(fiber);
+ // 否则只更新节点上的属性
+ } else if (fiber.effectTag === 'update') {
+ patchProps(
+ fiber.dom,
+ fiber.component.props,
+ fiber?.alternateFiber?.component?.props
+ );
+ }
switchFiber(fiber);
}
实现更新 (节点移除)
如果老 Fiber 链中没有被任何 newFiber 的 alternateFiber
指向, 那么这个元素就应该被删除了 (例如图中的 Fiber2
)
当一个元素应该被删除时, 我们会将元素压入 deletions
数组, 在统一提交时统一删除. (创建 deletions
数组而不是在统一提交是查找是因为在新链表上已经找不到待删除的 oldFiber 了)
graph LR subgraph Fiber Fiber1 --> Fiber2 --> Fiber3 end subgraph newFiber newFiber1 ----> newFiber3 end newFiber1 -.alternate.-> Fiber1 newFiber3 -.alternate.-> Fiber3
创建 deletion
并在统一提交时删除
const deletions = []; // 等待删除元素
// 删除元素
function applyDeletions() {
while (deletions.length) {
const fiber = deletions.shift();
if (!isFunctionComponent(fiber.component)) fiber.dom.remove();
else fiber.childrenFiber.forEach((d) => deletions.push(d));
}
}
在统一提交时删除
function performFiberLoop(idleDeadline) {
while (idleDeadline.timeRemaining() > 1 && nextFiber) {
performFiber(nextFiber);
}
if (!nextFiber && wipRootFiber) {
+ applyDeletions();
applySubmit(getNextFiber(wipRootFiber));
currentRootFiber = wipRootFiber;
wipRootFiber = null;
}
requestIdleCallback(performFiberLoop);
}
建立老 Fiber 到新 Fiber 的映射
扩充 Fiber 属性
{
dom,
component,
parentFiber,
firstChildFiber,
siblingFiber,
alternateFiber,
effectTag: 'update' | 'placement'
// 新的 Fiber 属性
childrenFiber: [], // 记录当前 Fiber 的孩子 Fiber (方便检索当前 Fiber 上那些子 Fiber 没有对应元素)
newFiber, // 指向当前 Fiber 对应的新 Fiber (如果存在)
}
完成这两个属性的构建
function update() {
if (!currentRootFiber) return;
wipRootFiber = {
...currentRootFiber,
+ childrenFiber: [],
alternateFiber: currentRootFiber,
};
+ currentRootFiber.newFiber = wipRootFiber;
nextFiber = wipRootFiber;
}
在 reconcileChildren
的时候实现删除元素收集
function reconcileChildren(fiber, children) {
if (fiber.component.nodeName === 'TEXT_ELEMENT') return;
let alternateFiber = fiber?.alternateFiber?.firstChildFiber;
children.reduce((prev, cur) => {
const curFiber = {
dom: null,
component: cur,
parentFiber: fiber,
firstChildFiber: null,
siblingFiber: null,
effectTag: null,
+ childrenFiber: [],
+ newFiber: null,
};
- if (alternateFiber && isSameType(cur, alternateFiber.component)) {
+ if (alternateFiber) {
+ // 相同类型依然执行更新
+ if (isSameType(cur, alternateFiber.component)) {
curFiber.dom = alternateFiber.dom;
curFiber.effectTag = 'update';
curFiber.alternateFiber = alternateFiber;
+ // 新增对 newFiber 的映射
+ alternateFiber.newFiber = curFiber;
+ }
} else {
curFiber.effectTag = 'placement';
}
if (!prev) fiber.firstChildFiber = curFiber;
else prev.siblingFiber = curFiber;
if (alternateFiber) alternateFiber = alternateFiber?.siblingFiber;
+ // 收集子元素
+ fiber.childrenFiber.push(curFiber);
return curFiber;
}, null);
+ // 如果处于更新模式, 遍历子元素, 找到老 Fiber 的子 Fiber 中没有对应新 Fibber 的加入删除列表
+ if (fiber.alternateFiber) {
+ fiber.alternateFiber.childrenFiber.forEach(
+ (d) => !d.newFiber && deletions.push(d)
+ );
+ }
}
实现更新 (不同类型节点)
不同节点之间无法更新, 实际上是做了删除然后新增的操作
function reconcileChildren(fiber, children) {
if (fiber.component.nodeName === 'TEXT_ELEMENT') return;
let alternateFiber = fiber?.alternateFiber?.firstChildFiber;
children.reduce((prev, cur) => {
const curFiber = {
dom: null,
component: cur,
parentFiber: fiber,
firstChildFiber: null,
siblingFiber: null,
effectTag: null,
childrenFiber: [],
newFiber: null,
};
if (alternateFiber) {
if (isSameType(cur, alternateFiber.component)) {
curFiber.dom = alternateFiber.dom;
curFiber.effectTag = 'update';
curFiber.alternateFiber = alternateFiber;
alternateFiber.newFiber = curFiber;
+ // 不同类型的更新元素: 删除, 然后将当前 Fiber 标记为新增
+ } else {
+ deletions.push(alternateFiber);
+ curFiber.effectTag = 'placement';
+ }
} else {
curFiber.effectTag = 'placement';
}
if (!prev) fiber.firstChildFiber = curFiber;
else prev.siblingFiber = curFiber;
if (alternateFiber) alternateFiber = alternateFiber?.siblingFiber;
fiber.childrenFiber.push(curFiber);
return curFiber;
}, null);
if (fiber.alternateFiber) {
fiber.alternateFiber.childrenFiber.forEach(
(d) => !d.newFiber && deletions.push(d)
);
}
}
将更新粒度缩小为组件
之前 update 的使用方法为
const demoFC () => {
const handleChange = () => {
num += 1
React.update();
}
}
此时我们执行 update 会将整个页面刷新一遍. 我们希望实现调用 update
时仅对当前组件做刷新
const demoFC () => {
const update = React.update();
const handleChange = () => {
num += 1
update();
}
}
实现思路:
- 向函数暴露一个
update
工厂函数, 通过闭包存储当前正在调用的组件对应的 Fiber. 从而实现在调用update
时找到调用者 - 在调用 update 的时候将
currentRootFiber
设置为调用update()
的组件, 将wipRootFiber
也同步为这个组件 - 由于
currentRootFiber
不再是根组件, 当 currentRootFiber 的后代 Fiber 全部遍历结束后, 我们应该立即停止, 而不是遍历currentRootFiber
的右兄弟
实现 update()
我们知道组件函数 (包括 update = React.update()
) 会在构造的时候被调用, 构造的时候 nextFiber
恰好是当前组件的 Fiber, 因此直接用闭包保存 nextFiber
function update() {
+ const currentFiber = nextFiber;
+ return () => {
+ // 新 Fiber 链表的头不再是上次处理的头 Fiber
+ currentRootFiber = currentFiber;
wipRootFiber = {
...currentRootFiber,
alternateFiber: currentRootFiber,
childrenFiber: [],
};
+ nextFiber = wipRootFiber;
+ };
- currentRootFiber.newFiber = wipRootFiber;
- nextFiber = wipRootFiber;
}
仅允许 Fiber 在 currentRootFiber
内部切换
function getNextFiber(fiber) {
if (fiber.firstChildFiber) return fiber.firstChildFiber;
if (fiber.siblingFiber) return fiber.siblingFiber;
let grandFiber = fiber.parentFiber;
while (grandFiber) {
+ if (grandFiber === wipRootFiber) return null;
if (grandFiber.siblingFiber) return grandFiber.siblingFiber;
grandFiber = grandFiber.parentFiber;
}
return null;
}
实现简易 useState
Hook (同步更新)
在实现 useState
之前我们需要通过如下方式手动更新
const demoFC () => {
const update = React.update();
const handleChange = () => {
num += 1
update();
}
}
我们希望实现为
const demoFC () => {
const [num, setNum] = React.useState(0);
const handleChange = () => {
setNum(num + 1);
}
}
这意味着我们需要实现一个 React.useState
- 在构造阶段返回初始值与更新回调
- 在更新阶段返回持久化的值与回调
- 调用回调后更新状态, 调用
update()
, 在下一次构造时返回更新后值 React.useState
应该可以在组件中被多次调用, 并且在下次构造的时候根据调用顺序返回值
简而言之:
- 持久化状态是跟随组件存储的
- 组件实例之间状态不能共享的 (同时如果页面中有两个相同的组件, 两个组件各自维护状态)
- 状态在更新时可以被传递
- 一个组件可以有多个状态, 根据调用函数顺序返回不同的值
这么看组件对应的 Fiber 是一个很好的存储仓库.
- 持久化状态是跟随组件存储的 => 状态对应一个组件对应一个Fiber
- 组件实例之间状态不能共享的 => 同一组件不同实例是不同 Fiber
- 状态在更新时可以被传递 => 在创建新 Fiber 时赋值
- 一个组件可以有多个状态, 根据调用函数顺序返回不同的值 => 在 Fiber 中维护数组, 与构造时
React.useState
计数, 每次调用返回数组中下一个元素
扩展 Fiber 数据结构
新 Fiber 结构如下
{
dom,
component,
parentFiber,
firstChildFiber,
siblingFiber,
childrenFiber,
newFiber,
// 新增属性
useStateCount: 0, // 每次构造新 Fiber 的时候清空
useStateStorage: [], // 在构造新 Fiber 的时候从老 Fiber 继承
}
应用修改
function render(component, container) {
wipRootFiber = {
dom: container,
component,
parentFiber: null,
firstChildFiber: null,
siblingFiber: null,
childrenFiber: [],
newFiber: null,
+ useStateCount: 0,
+ useStateStorage: [],
};
nextFiber = wipRootFiber;
}
function update() {
const currentFiber = nextFiber;
return () => {
currentRootFiber = currentFiber;
wipRootFiber = {
+ // 解构时继承数据
...currentRootFiber,
alternateFiber: currentRootFiber,
childrenFiber: [],
+ useStateCount: 0, // 重置计数
};
nextFiber = wipRootFiber;
};
}
function reconcileChildren(fiber, children) {
if (fiber.component.nodeName === 'TEXT_ELEMENT') return;
let alternateFiber = fiber?.alternateFiber?.firstChildFiber;
children.reduce((prev, cur) => {
const curFiber = {
dom: null,
component: cur,
parentFiber: fiber,
firstChildFiber: null,
siblingFiber: null,
effectTag: null,
childrenFiber: [],
newFiber: null,
+ useStateCount: 0,
+ useStateStorage: [],
};
if (alternateFiber) {
if (isSameType(cur, alternateFiber.component)) {
curFiber.dom = alternateFiber.dom;
curFiber.effectTag = 'update';
curFiber.alternateFiber = alternateFiber;
+ // 状态传递
+ curFiber.useStateStorage = alternateFiber.useStateStorage;
alternateFiber.newFiber = curFiber;
} else {
deletions.push(alternateFiber);
curFiber.effectTag = 'placement';
}
} else {
curFiber.effectTag = 'placement';
}
// ...
}, null);
// ...
}
实现 React.useState
function useState(value) {
// 获取针对当前组件的 update
const updateComponent = update();
// 缓存当前 Fiber
const currentFiber = nextFiber;
// 记录是第几次调用 useState
const index = currentFiber.useStateCount++;
// 如果没有这个 index 说明是初次调用, 直接将传入的初始值记录下来
if (currentFiber.useStateStorage.length <= index)
currentFiber.useStateStorage.push(value);
// 返回持久化的值, 更新函数
return [
currentFiber.useStateStorage[index],
(f) => {
// 对 setState 的参数做类型合并
const newValue = f instanceof Function ? f(value) : f;
// 前后结果相同跳过
if (newValue === value) return;
// 更新持久化的值
currentFiber.useStateStorage[index] = newValue;
// 重新渲染
updateComponent();
},
];
}
实现 useState
Hook (异步更新)
如果代码中连续调用多次 useState
我们就会进行多次计算, 此时可以做如下优化:
将 state 作为对象 {value, queue: []}
存储, 所有的 setState
会被压入 queue, 请求更新, 再更新时再计算新数据
function useState(value) {
const updateComponent = update();
const currentFiber = nextFiber;
const index = currentFiber.useStateCount++;
if (currentFiber.useStateStorage.length <= index) {
- currentFiber.useStateStorage.push(value);
+ currentFiber.useStateStorage.push({
+ value,
+ queue: [],
+ });
}
+ const hook = currentFiber.useStateStorage[index]; // 获取需要这次的 state
+ hook.queue.forEach((f) => (hook.value = f(hook.value))); // 执行压入的变更
+ hook.queue = []; // 清空待变更队列
const hook = currentFiber.useStateStorage[index];
hook.queue.forEach((f) => (hook.value = f(hook.value)));
hook.queue = [];
- return [
- currentFiber.useStateStorage[index],
- (f) => {
- const newValue = f instanceof Function ? f(value) : f;
- if (newValue === value) return;
- currentFiber.useStateStorage[index] = newValue;
- updateComponent();
- },
- ];
+ const setState = (f) => {
+ const action = f instanceof Function ? f : () => f;
+ // 如果值没有变化就跳过
+ const eagerValue = action(hook.value);
+ if (hook.value === eagerValue) return;
+ hook.queue.push(action);
+ updateComponent();
+ };
+ return [currentFiber.useStateStorage[index].value, setState];
}
实现简易 useEffect
Hook (同步回调)
需要实现 useEffect(cb, deps)
- 在初始化阶段无论
deps
是什么都调用cb
- 在更新阶段只有
deps
变化才调用cb
效仿 useState 的思路实现:
扩充 Fiber
function update() {
const currentFiber = nextFiber;
return () => {
currentRootFiber = currentFiber;
wipRootFiber = {
...currentRootFiber,
alternateFiber: currentRootFiber,
childrenFiber: [],
useStateCount: 0,
+ useEffectCount: 0,
};
nextFiber = wipRootFiber;
};
}
function render(component, container) {
wipRootFiber = {
dom: container,
component,
parentFiber: null,
firstChildFiber: null,
siblingFiber: null,
childrenFiber: [],
newFiber: null,
useStateCount: 0,
useStateStorage: [],
+ useEffectCount: 0,
+ useEffectStorage: [],
};
nextFiber = wipRootFiber;
}
function reconcileChildren(fiber, children) {
if (fiber.component.nodeName === 'TEXT_ELEMENT') return;
let alternateFiber = fiber?.alternateFiber?.firstChildFiber;
children.reduce((prev, cur) => {
const curFiber = {
dom: null,
component: cur,
parentFiber: fiber,
firstChildFiber: null,
siblingFiber: null,
effectTag: null,
childrenFiber: [],
newFiber: null,
useStateCount: 0,
useStateStorage: [],
+ useEffectCount: 0,
+ useEffectStorage: [],
};
if (alternateFiber) {
if (isSameType(cur, alternateFiber.component)) {
curFiber.dom = alternateFiber.dom;
curFiber.effectTag = 'update';
curFiber.alternateFiber = alternateFiber;
curFiber.useStateStorage = alternateFiber.useStateStorage;
+ curFiber.useEffectStorage = alternateFiber.useEffectStorage;
alternateFiber.newFiber = curFiber;
} else {
deletions.push(alternateFiber);
curFiber.effectTag = 'placement';
}
} else {
curFiber.effectTag = 'placement';
}
// ...
}, null);
// ...
}
实现 useEffect()
function useEffect(cb, dep) {
const currentFiber = nextFiber;
const effectIndex = currentFiber.useEffectCount++;
// 初始化阶段, 无论是否传入 dep 列表, 都执行
if (currentFiber.useEffectStorage.length <= effectIndex) {
currentFiber.useEffectStorage.push({ cb, dep });
cb();
return;
}
// 非初始化阶段, 只有 deps 变化才执行, 同时更新 deps
const hook = currentFiber.useEffectStorage[effectIndex];
if (
dep.length !== hook.dep.length ||
dep.some((item, index) => item !== hook.dep[index])
) {
hook.dep = dep;
cb();
}
}
实现 useEffect
Hook (异步回调)
实际的 useEffect
还具有如下特性
- 回调并不是在
React.useEffect()
时执行, 而是在统一提交后执行 (否则拿不到 DOM) - 我们需要收集 useEffect 中回调的返回函数 (
cleanup
函数), 并在下次执行回调的时候先执行上次返回的cleanup
函数
实现第一点只需要维护一个全局队列记录需要执行的回调. 实现第二点需要在 hook 中维护一个 cleanup
参数
维护全局回调 Hook 数组
+ // 需要执行的 Hook 数组
+ let useEffectQueue = [];
function useEffect(cb, dep = []) {
const currentFiber = nextFiber;
const effectIndex = currentFiber.useEffectCount++;
if (currentFiber.useEffectStorage.length <= effectIndex) {
- currentFiber.useEffectStorage.push({ cb, dep });
- cb();
+ // 首次执行无需执行 cleanup, 设为空
+ const hook = { cb, dep, cleanUp: undefined };
+ currentFiber.useEffectStorage.push(hook);
+ useEffectQueue.push(hook);
return;
}
const hook = currentFiber.useEffectStorage[effectIndex];
if (
dep.length !== hook.dep.length ||
dep.some((item, index) => item !== hook.dep[index])
) {
hook.dep = dep;
- cb();
+ // 更新时执行
+ useEffectQueue.push(hook);
}
}
在统一提交时调用回调
执行全部的 Hooks
function applyEffect() {
useEffectQueue.forEach((hook) => {
if (hook.cleanUp) hook.cleanUp();
hook.cleanUp = hook.cb();
});
useEffectQueue = [];
}
function performFiberLoop(idleDeadline) {
// ...
if (!nextFiber && wipRootFiber) {
applyDeletions();
applySubmit(getNextFiber(wipRootFiber));
+ applyEffect();
currentRootFiber = wipRootFiber;
wipRootFiber = null;
}
// ...
}