这篇文章会实现一个极小版 React。它不会覆盖真实 React 的优化、调度细节和边界情况,但会保留一条非常重要的主线:JSX 如何变成 element,element 如何变成 DOM node,更新时如何通过 fiber tree 做协调(reconciliation),最后再支持函数组件和 Hooks。
我们会把这个小库叫作 MiniReact。最终它能完成这些能力:
- 用
createElement接住 JSX 转译结果 - 把 element tree 渲染成真实 DOM node
- 把渲染工作拆成可中断的工作单元
- 用 fiber tree 描述 work in progress tree
- 把 render 阶段和 commit 阶段分开
- 通过协调处理新增、更新和删除
- 支持函数组件
- 支持一个简化版
useState
整篇文章会按从 element 到 Hooks 的顺序推进,但会减少重复的完整代码块。第一次引入概念时展示完整实现;后续演进只看关键变化,并在关键阶段末尾放一份可运行版本,方便你对照。
Step Zero:回顾
本节要解决的问题:先把 React、JSX 和 DOM 之间的关系拆开,确认后面自己实现时到底要替换哪些部分。
从一个只有三行的 React 应用开始:
const element = <h1 title="foo">Hello</h1>;
const container = document.getElementById("root");
ReactDOM.render(element, container);
第一行里的 JSX 不是浏览器能直接执行的 JavaScript。Babel 这类工具会把它转成一次 createElement 调用:
const element = React.createElement("h1", { title: "foo" }, "Hello");
React.createElement 的核心产物是一个对象。真实 React 的 element 还有更多内部字段,但我们只关心 type 和 props。如果把函数调用直接替换成结果,大概是这样:
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
};
type 描述要创建的东西。这里它是字符串 "h1",对应 document.createElement("h1")。后面到函数组件时,type 也可以是一个函数。
props 保存 JSX attributes。children 也在 props 里,它可能是字符串,也可能是一组嵌套 element。也就是说,element 本身天然会组成一棵树。
第二个要替换的是 ReactDOM.render。它负责把 element tree 变成真实 DOM:
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
};
const container = document.getElementById("root");
const node = document.createElement(element.type);
node["title"] = element.props.title;
const text = document.createTextNode("");
text["nodeValue"] = element.props.children;
node.appendChild(text);
container.appendChild(node);
这段代码里有三个术语需要固定下来:
element:React element,也就是描述 UI 的普通对象DOM node:浏览器里的真实节点,比如h1或 text nodefiber:后面用来表示一个工作单元的数据结构
到这里,我们已经知道自己要实现两件事:一个 createElement,一个 render。下一步先处理 JSX 到 element 的转换结果。
Step I:createElement 函数
本节要解决的问题:让 JSX 转译后能调用我们自己的函数,并产出统一结构的 element。
换一个稍复杂的例子:
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
const container = document.getElementById("root");
ReactDOM.render(element, container);
Babel 会把它转成嵌套的 createElement 调用:
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
);
所以 createElement 只需要返回一个带 type 和 props 的对象。为了让后面的渲染逻辑更简单,我们还会保证 children 永远是数组。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
};
}
不过 children 里可能出现字符串或数字,比如 <a>bar</a> 里的 "bar"。为了让渲染过程统一处理,我们把这些原始值包成一种特殊 element:TEXT_ELEMENT。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
真实 React 不会用这个方式包装原始值,也不会为了简化逻辑强行创建空的 children 数组。MiniReact 只保留理解主线所需的结构,所以这里优先选择更少的分支。
接下来给库起名,并告诉 Babel 用我们的 createElement:
const MiniReact = {
createElement,
};
/** @jsx MiniReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
/** @jsx MiniReact.createElement */ 会让 Babel 把 JSX 转成 MiniReact.createElement(...)。目前我们已经能创建 element tree,但还不能把它显示出来。下一步实现 render。
Step II:render 函数
本节要解决的问题:把 element tree 递归转换成 DOM node,并追加到容器里。
先写最小版本:
function render(element, container) {
const dom = document.createElement(element.type);
container.appendChild(dom);
}
这个版本只能处理普通 DOM node。为了支持文本,需要识别 TEXT_ELEMENT:
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
然后把非 children 的 props 赋给 DOM node,并递归渲染 children:
const isProperty = key => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name];
});
element.props.children.forEach(child => render(child, dom));
这一阶段的完整版本如下:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
const isProperty = key => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name];
});
element.props.children.forEach(child => render(child, dom));
container.appendChild(dom);
}
const MiniReact = {
createElement,
render,
};
/** @jsx MiniReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
const container = document.getElementById("root");
MiniReact.render(element, container);
现在 MiniReact 能把 JSX 渲染到 DOM,但它有一个明显问题:递归渲染一旦开始,就会同步跑完整棵树。下一步把工作拆开。
Step III:Concurrent Mode
本节要解决的问题:避免一次同步递归渲染长时间占用主线程。
目前的 render 会在一次调用里递归处理完整 element tree:
element.props.children.forEach(child => render(child, dom));
如果树很大,主线程会被占住。浏览器想处理输入、动画或布局,也只能等这次渲染结束。
解决思路是把渲染拆成小的工作单元。每完成一个单元,就检查浏览器是否还有空闲时间;如果没有,就把控制权还给浏览器。
let nextUnitOfWork = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
requestIdleCallback 会在浏览器空闲时调用回调。回调收到的 deadline 可以告诉我们当前还剩多少空闲时间。
真实 React 已经不再使用 requestIdleCallback,而是使用自己的 scheduler package。这里保留它,是因为它能用很少代码表达“可中断渲染”的核心概念。
现在缺的是工作单元本身。下一步引入 fiber。
Step IV:Fibers
本节要解决的问题:用 fiber tree 表示工作单元,并让每个单元都能找到下一个单元。
每个 element 会对应一个 fiber。每个 fiber 既保存渲染所需的数据,也是一个工作单元。

假设要渲染这棵树:
MiniReact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
);
在 render 里先创建 root fiber,并把它设置为 nextUnitOfWork。之后 performUnitOfWork 会对每个 fiber 做三件事:
- 为当前 fiber 创建 DOM node
- 为 children 创建新的 fibers
- 返回下一个工作单元

为了快速找到下一个工作单元,每个 fiber 都有三个指针:child、sibling 和 parent。

规则很直接:有 child 就先处理 child。

没有 child 就处理 sibling。

如果既没有 child 也没有 sibling,就沿着 parent 往上找,直到找到某个祖先的 sibling。如果一路回到 root 也找不到,说明整棵树处理完了。
先把创建 DOM node 的逻辑抽成 createDom:
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
const isProperty = key => key !== "children";
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name];
});
return dom;
}
render 不再直接改 DOM,而是设置第一项工作:
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
};
}
performUnitOfWork 的核心逻辑如下:
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
这一版已经具备 fiber 的骨架:工作可以被拆开,也能从一个 fiber 找到下一个 fiber。但它还有一个严重缺陷:每处理一个 fiber 就立即改 DOM。浏览器如果中途暂停,用户可能看到半成品 UI。下一步把计算和提交分开。
Step V:Render 和 Commit 阶段
本节要解决的问题:让可中断的 render 阶段不直接修改 DOM,只在整棵 fiber tree 准备好后一次性提交。
之前 performUnitOfWork 里有这段:
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
它会在处理每个 fiber 时直接插入 DOM。现在把这段删除,改成在内存里构建 work in progress tree。
render 创建 wipRoot,再把它作为第一项工作:
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
};
nextUnitOfWork = wipRoot;
}
let nextUnitOfWork = null;
let wipRoot = null;
当工作循环发现没有下一个工作单元时,说明 render 阶段完成,可以进入 commit 阶段:
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
commit 阶段不能中断,因为它会真实修改 DOM:
function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
现在 MiniReact 的两个阶段已经分开:
- render 阶段:可中断,只创建 fiber 和 DOM node,不挂载
- commit 阶段:不可中断,把完整结果提交到真实 DOM
下一步处理更新。只有新增还不够,我们需要比较新旧树。
Step VI:Reconciliation
本节要解决的问题:把新的 elements 和上一次提交的 fiber tree 做比较,决定新增、更新还是删除 DOM node。
首先要保存上一次 commit 完成的 fiber tree:
function commitRoot() {
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
}
let currentRoot = null;
alternate 指向旧 fiber,也就是上一次提交到 DOM 的版本。
然后把 performUnitOfWork 里创建 child fibers 的逻辑抽到 reconcileChildren:
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
协调时同时遍历新 elements 和旧 fibers:
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
判断规则很简化:
- type 相同:复用 DOM node,标记
UPDATE - 有新 element 但 type 不同:创建新 DOM node,标记
PLACEMENT - 有旧 fiber 但 type 不同:删除旧 DOM node,标记
DELETION
真实 React 还会使用 key 改善 children 的比较,比如识别数组里的移动。MiniReact 暂时不处理这些优化。
DELETION 没有对应的新 fiber,所以需要额外数组保存:
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUnitOfWork = wipRoot;
}
let deletions = null;
commit 阶段根据 effectTag 做不同操作:
function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
最后补上 updateDom。它负责移除旧属性、设置新属性,并处理事件监听:
const isEvent = key => key.startsWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = (prev, next) => key => !(key in next);
function updateDom(dom, prevProps, nextProps) {
Object.keys(prevProps)
.filter(isEvent)
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = "";
});
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name];
});
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
这一阶段的完整版本如下:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
updateDom(dom, {}, fiber.props);
return dom;
}
const isEvent = key => key.startsWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = (prev, next) => key => !(key in next);
function updateDom(dom, prevProps, nextProps) {
Object.keys(prevProps)
.filter(isEvent)
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = "";
});
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name];
});
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUnitOfWork = wipRoot;
}
let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
const MiniReact = {
createElement,
render,
};
/** @jsx MiniReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
const container = document.getElementById("root");
MiniReact.render(element, container);
现在 MiniReact 已经能处理 DOM element 的新增、更新和删除。但如果 type 是函数,它还不知道怎么处理。下一步支持函数组件。
Step VII:函数组件
本节要解决的问题:让 type 为函数的 element 也能进入 fiber 流程。
函数组件有两个关键差异:
- 函数组件对应的 fiber 没有自己的 DOM node
- 它的 children 来自执行函数组件后的返回值
也就是说,普通 DOM element 的 children 来自 fiber.props.children,函数组件的 children 来自 fiber.type(fiber.props)。
先在 performUnitOfWork 里分流:
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
普通 DOM element,也就是 host component,沿用之前的逻辑:
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren(fiber, fiber.props.children);
}
函数组件执行后得到一个 child element。为了复用 reconcileChildren,把它包成数组:
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
还要修改 commit 阶段。因为函数组件 fiber 没有 DOM node,寻找 DOM parent 时要一路向上,直到找到有 dom 的祖先:
function commitWork(fiber) {
if (!fiber) {
return;
}
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
删除时也一样:被删除的 fiber 可能没有 DOM node,所以要向下找到真正的 DOM node:
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
现在函数组件可以工作了,但它还是纯渲染函数,没有状态。下一步加入 Hooks。
Step VIII:Hooks
本节要解决的问题:实现一个最小版 useState,让函数组件能保存状态并触发重新渲染。
先看目标用法:
function Counter() {
const [state, setState] = MiniReact.useState(1);
return <h1 onClick={() => setState(c => c + 1)}>Count: {state}</h1>;
}
每次渲染函数组件前,需要准备两个全局变量:
let wipFiber = null;
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
wipFiber 指当前正在渲染的函数组件 fiber。hookIndex 记录当前读到第几个 hook,因为一个组件里可以多次调用 useState。
useState 需要从旧 fiber 上找到上一次的 hook,并把排队的更新函数依次应用到旧状态上:
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
};
const actions = oldHook ? oldHook.queue : [];
actions.forEach(action => {
hook.state = action(hook.state);
});
const setState = action => {
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
hook.queue.push(action);
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
这里的 setState 没有立即修改状态,而是把 action 放进当前 hook 的 queue,再创建新的 wipRoot,触发一次从 root 开始的重新渲染。
最终完整版本如下:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
updateDom(dom, {}, fiber.props);
return dom;
}
const isEvent = key => key.startsWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = (prev, next) => key => !(key in next);
function updateDom(dom, prevProps, nextProps) {
Object.keys(prevProps)
.filter(isEvent)
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = "";
});
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name];
});
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUnitOfWork = wipRoot;
}
let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
let wipFiber = null;
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
};
const actions = oldHook ? oldHook.queue : [];
actions.forEach(action => {
hook.state = action(hook.state);
});
const setState = action => {
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
hook.queue.push(action);
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
reconcileChildren(fiber, fiber.props.children);
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
const MiniReact = {
createElement,
render,
useState,
};
/** @jsx MiniReact.createElement */
function Counter() {
const [state, setState] = MiniReact.useState(1);
return <h1 onClick={() => setState(c => c + 1)}>Count: {state}</h1>;
}
const element = <Counter />;
const container = document.getElementById("root");
MiniReact.render(element, container);
Epilogue
MiniReact 到这里就完成了。它不是 React 的复制品,而是一条压缩过的学习路径:从 element 到 DOM,从同步递归到 fiber,从新增 DOM 到协调更新,再从函数组件到 Hooks。
它和真实 React 仍然有大量差异。比如,React 还需要处理优先级、错误边界、Context、memoization、更复杂的事件系统、更完整的 diff 策略、Suspense、服务端渲染,以及各种性能优化。
如果想继续扩展 MiniReact,可以从这些点开始:
- 展平嵌套的 children arrays
- 为 reconciliation 加上
key - 支持更多 DOM 属性和事件边界情况
- 实现
useEffect - 为
requestIdleCallback加 polyfill 或替换成自定义 scheduler
附注:本文实现路线参考了 Build your own React,但正文聚焦 MiniReact 的实现过程。