实现一个 Mini React
React 18 带来的一个巨大的性能提升就在于整个更新过程是异步、可中断的。React 源码非常庞大,直接上去生啃可能有点困难,因此可以借助一个 Mini React 来了解 React 内部机制。
createElement 和 render
首先回忆一下,我们平时在写 React 组件的时候基本会使用 JSX,然后借助 babel 将其转为 React.createElement
。具体 babel 是如何转译的本文暂不涉及,只需要知道最终还是调用 React.createElement
即可。先来看一个最简单的例子:
React.render(React.createElement('div', {}, 'hello'), document.getElementById('root'));
这里我们需要实现 render
和 createElement
方法。
const isVirtualElement = (e) => typeof e === 'object';
const createTextElement = (text) => ({
type: 'TEXT',
props: {
nodeValue: text,
},
});
const createElement = (type, props = {}, ...child) => {
const children = child.map((c) =>
isVirtualElement(c) ? c : createTextElement(String(c)),
);
return {
type,
props: {
...props,
children,
},
};
};
createElement
非常简单,就是把元素标准化成一个对象,type
表示元素的类型,目前也就是元素标签 div
,props
整合了自身传入的 props
和 children
。children
如果是纯文本节点,则进行标准化,否则原封不动地放进数组。
例如对于:
<div id="test">
<h1>hello</h1>
world
</div>
则会先转为:
React.createElement('div', { id: 'test' }, [React.createElement('h1', {}, 'hello'), 'world'])
调用 createElement
后被标准化为一颗映射到真实 DOM 的虚拟 DOM 树:
{
type: 'div',
props: {
id: 'test',
children: [
{
type: 'h1',
props: {
children: {
type: 'TEXT',
props: {
nodeValue: 'hello',
}
}
}
},
{
type: 'TEXT',
props: {
nodeValue: 'world',
},
}
]
}
}
下面来实现 render
方法。还记得这个例子 React.render(React.createElement('div', {}, 'hello'), document.getElementById('root'))
,render
第一个参数为要渲染的 DOM,即上文得到的虚拟 DOM,第二个参数为真实 DOM 最终挂载的根节点。
const render = (element, container) => {
currentRoot = null;
wipRoot = {
type: 'div',
dom: container,
props: {
children: [
{
...element,
},
],
},
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
};
wipRoot
表示当前 React 正在着手处理的整个虚拟 DOM 的根节点,它跟我们上文得到的虚拟 DOM 节点都有着相似的结构,类比 React 中的 FiberNode。还注意到 wipRoot
的值赋给了 nextUnitOfWork
。nextUnitOfWork
是个非常重要的角色,它表示当前正在处理的节点。在 React 中,每次都是一个一个节点来处理,节点与节点之间是可以打断的,这里的处理包括新旧节点对比更新、状态计算等,最终得到一个最新的虚拟 DOM 树,然后将其一并提交一口气更新到真实 DOM。这样设计的好处就在于,如果遇到大量节点更新的情况,能及时将主线程的控制权让渡出去,以便响应更高优先级的任务,例如用户点击操作等等;而不至于一直长时间占用主线程,造成页面卡顿。
render
的工作也做完了,仅仅只是定义了一个根节点,并把其他子节点加入到 children
里;然后将当前工作节点指向根节点。
循环工作
前面做完了准备工作,React 可以开始干活了。那谁来指派 React 干活?很简单:
const workLoop = (deadline) => {
while (nextUnitOfWork && deadline.timeRemaining() > 1) { // 剩余时间>1ms
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
window.requestIdleCallback(workLoop);
};
void (function main() {
window.requestIdleCallback(workLoop);
})();
requestIdleCallback
能够在浏览器空闲时调用传入的回调函数。
workLoop
里列出了需要干哪些活:
- 如果当前有需要处理的节点
nextUnitOfWork
,且还有剩余时间(这部分后文会详细叙述),就处理该节点,并返回下一个待处理的节点; - 如果所有节点都处理完毕,就可以提交整个虚拟 DOM 来一次性完成真实 DOM 的渲染;
- 进入下一轮巡逻,如果有任何变动将会进行下一轮更新。
第 1 点中,我们提到了当前节点处理完后,会返回下一个待处理的节点,这是怎么做到的?这里其实用到了链表。下图是一个虚拟 DOM 树,节点之间有三种关系:子节点 child、上级节点 return/parent、相邻节点 sibling。具体来说:Root 是根节点,其 child 指针指向唯一的子节点 div,div 的 return 指针指向上级节点 Root;div 的 child 指针指向第一个子节点 span,span 和 h1 的 return 指针指向其上级节点 div,span 的 sibling 指针指向其相邻节点 h1;h1 的 child 指针指向 h2,h2 的return 指针指向 h1。有了这些指针,就能通过深度优先遍历,来挨个处理树上的所有节点了。
节点计算
根据传入的节点类型不同,分情况处理,上文我们举的例子中,type
基本都是元素标签或者文本类型,对于函数组件和类组件,则是 function
类型,处理方式后文将会详细说明。节点计算核心方法是 performUnitOfWork
。
const updateDOM = (DOM, prevProps, nextProps) => {
const defaultPropKeys = 'children';
for (const [removePropKey, removePropValue] of Object.entries(prevProps)) {
if (removePropKey.startsWith('on')) {
DOM.removeEventListener(
removePropKey.slice(2).toLowerCase(),
removePropValue,
);
} else if (removePropKey !== defaultPropKeys) {
DOM[removePropKey] = '';
}
}
for (const [addPropKey, addPropValue] of Object.entries(nextProps)) {
if (addPropKey.startsWith('on')) {
DOM.addEventListener(addPropKey.slice(2).toLowerCase(), addPropValue);
} else if (addPropKey !== defaultPropKeys) {
DOM[addPropKey] = addPropValue;
}
}
};
const createDOM = (fiberNode) => {
const { type, props } = fiberNode;
let DOM = null;
if (type === 'TEXT') {
DOM = document.createTextNode('');
} else if (typeof type === 'string') {
DOM = document.createElement(type);
}
if (DOM !== null) {
updateDOM(DOM, {}, props);
}
return DOM;
};
const performUnitOfWork = (fiberNode) => {
const { type } = fiberNode;
switch (typeof type) {
case 'number':
case 'string':
if (!fiberNode.dom) {
fiberNode.dom = createDOM(fiberNode);
}
reconcileChildren(fiberNode, fiberNode.props.children);
break;
case 'symbol':
if (type === Fragment) {
reconcileChildren(fiberNode, fiberNode.props.children);
}
break;
default:
if (typeof fiberNode.props !== 'undefined') {
reconcileChildren(fiberNode, fiberNode.props.children);
}
break;
}
if (fiberNode.child) {
return fiberNode.child;
}
let nextFiberNode = fiberNode;
while (typeof nextFiberNode !== 'undefined') {
if (nextFiberNode.sibling) {
return nextFiberNode.sibling;
}
nextFiberNode = nextFiberNode.return;
}
return null;
};
上面这段代码中,对于没有真实 DOM 节点的 FiberNode,会先创建一个真实 DOM,然后执行了 reconcileChildren
方法,在这个方法中,将会对节点树中的节点建立链接,以便可以进行节点树的遍历。最后返回下一个待处理的节点,按照以下顺序:
- 如果有子节点则返回子节点;
- 如果有相邻节点则返回相邻节点,如果没有相邻节点则向上一层,查找上层的相邻节点;
- 如果都没有,则返回 null,说明所有节点已处理完。
reconcileChildren
方法接收两个参数:当前节点,和其 children。具体请看代码注释:
const reconcileChildren = (fiberNode, elements = []) => {
let index = 0;
let oldFiberNode = void 0;
let prevSibling = void 0;
const virtualElements = elements.flat(Infinity);
// 1. 检查有没有旧节点,如果是第一次渲染,则没有;如果是 update 则其 alternate 就是相应的旧节点;这里取的是当前节点旧节点的第一个子节点
if (fiberNode.alternate && fiberNode.alternate.child) {
oldFiberNode = fiberNode.alternate.child;
}
// 2. 循环处理当前节点所有子节点
while (
index < virtualElements.length ||
typeof oldFiberNode !== 'undefined'
) {
const virtualElement = virtualElements[index];
let newFiber = void 0;
const isSameType = Boolean(
oldFiberNode &&
virtualElement &&
oldFiberNode.type === virtualElement.type, // !注意,只要 type 相同,则粗略认为两个节点是相同的
);
if (isSameType && oldFiberNode) {
newFiber = { // 3. 为当前子节点创建新的 FiberNode
type: oldFiberNode.type,
dom: oldFiberNode.dom,
alternate: oldFiberNode,
props: virtualElement.props,
return: fiberNode, // 将当前子节点与当前节点建立上下级链接
effectTag: 'UPDATE', // 该标志在真实DOM渲染阶段会指示如何操作真实DOM
};
}
if (!isSameType && Boolean(virtualElement)) {
newFiber = {
type: virtualElement.type,
dom: null,
alternate: null,
props: virtualElement.props,
return: fiberNode,
effectTag: 'REPLACEMENT',
};
}
if (!isSameType && oldFiberNode) {
deletions.push(oldFiberNode); // 这个全局变量deletions记录了需要删除的旧节点
}
if (oldFiberNode) {
oldFiberNode = oldFiberNode.sibling; // 当前子节点像下一个循环时,旧节点也指向下一个旧节点
}
if (index === 0) {
fiberNode.child = newFiber; // !注意,只有第一个子节点会与上级节点建立双向链接
} else if (typeof prevSibling !== 'undefined') {
prevSibling.sibling = newFiber; // 第一个后面的子节点只与前面的子节点建立链接
}
prevSibling = newFiber;
index += 1;
}
};
最后回到 workLoop
,如果 performUnitOfWork
执行完了所有的节点,最终会返回 null,此时 nextUnitOfWork
的值为 null
,表示没有待处理的节点了,下一步就可以渲染所有的真实 DOM 了,即 commitRoot
。
const workLoop = (deadline) => {
while (nextUnitOfWork && deadline.timeRemaining() > 1) { // 剩余时间>1ms
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
window.requestIdleCallback(workLoop);
};
渲染真实 DOM
commitRoot
中 commitWork
主要进行 DOM 的更新,并且只对有效节点更新真实 DOM,比如一些函数组件、类组件其自身也会创建一个占位FiberNode,但是对于真实 DOM 而言是不需要的,所以不需要更新。然后根据标志,如果是新添加的,则直接挂载到上级节点上,如果是复用旧的节点,则只更新 DOM 节点的属性即可。
DOM 渲染过程也是一个深度优先遍历,就基于 FiberNode 上创建的指针。
对于不需要的旧节点,也会统一删除。
const isDef = (param) => param !== void 0 && param !== null;
const commitRoot = () => {
const findParentFiber = (fiberNode) => {
if (fiberNode) {
let parentFiber = fiberNode.return;
while (parentFiber && !parentFiber.dom) {
parentFiber = parentFiber.return;
}
return parentFiber;
}
return null;
};
const commitDeletion = (parentDOM, DOM) => {
if (isDef(parentDOM)) {
parentDOM.removeChild(DOM);
}
};
const commitReplacement = (parentDOM, DOM) => {
if (isDef(parentDOM)) {
parentDOM.appendChild(DOM);
}
};
const commitWork = (fiberNode) => {
if (fiberNode) {
if (fiberNode.dom) { // 这里判断了有无 DOM 节点,因为有些节点并不需要渲染
const parentFiber = findParentFiber(fiberNode);
const parentDOM = parentFiber?.dom;
switch (fiberNode.effectTag) {
case 'REPLACEMENT':
commitReplacement(parentDOM, fiberNode.dom);
break;
case 'UPDATE':
updateDOM(
fiberNode.dom,
fiberNode.alternate ? fiberNode.alternate.props : {},
fiberNode.props,
);
break;
default:
break;
}
}
commitWork(fiberNode.child);
commitWork(fiberNode.sibling);
}
};
// 1. 上文 reconcileChildren 中对于已经需要的旧节点收集到数组中,这里一并删除
for (const deletion of deletions) {
if (deletion.dom) {
const parentFiber = findParentFiber(deletion);
commitDeletion(parentFiber?.dom, deletion.dom);
}
}
// 2. 执行 DOM 挂载,因为根节点就是 div#root,所以从子节点开始
if (wipRoot !== null) {
commitWork(wipRoot.child);
currentRoot = wipRoot;
}
wipRoot = null;
};
最后,清空 wipRoot
,表示当前没有待处理的更新;同时,用 currentRoot
来存储这一次最新的节点树;等到下一次更新的时候,这次的节点树就会成为旧的节点树,供最新的节点来比较和计算。
函数组件和类组件
截至目前,实现了只能渲染类似 React.createElement('div', {}, children)
这种。但我们平常使用的都是函数组件或类组件。
还记得上文在进行节点计算的时候,performUnitOfWork
中,提到会根据 typeof type
来决定执行的逻辑。这里的 type
除了元素标签、'TEXT',还可以是函数组件或类组件。
比如下面这个例子:
import React from './mini-react.js';
function Child() {
return (
<div>
点击了0次
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
title: 'hello'
}
}
render() {
const {title} = this.state;
return (
<div style={{background: '#eee'}}>
<div>{title}</div>
<Child />
</div>
)
}
}
React.render(<App />, document.getElementById('root'));
Component 实现如下:
class Component {
props;
constructor(props) {
this.props = props;
}
static REACT_COMPONENT = true;
}
在节点计算的时候,会有相应的处理。简单来说,对于类组件,就实例化类组件,执行render方法,返回的内容作为其 children,也是真正需要渲染的内容;函数组件直接调用函数,返回内容也是作为 children。
const performUnitOfWork = (fiberNode) => {
const { type } = fiberNode;
switch (typeof type) {
case 'function': {
wipFiber = fiberNode;
wipFiber.hooks = [];
hookIndex = 0;
let children;
if (Object.getPrototypeOf(type).REACT_COMPONENT) { // 类组件
const C = type;
const component = new C(fiberNode.props); // 实例化类组件
const [state, setState] = useState(component.state);
component.props = fiberNode.props;
component.state = state;
component.setState = setState;
children = component.render?.bind(component)(); // 执行类中的 render 方法,返回值作为该类节点的 children
} else {
children = type(fiberNode.props); // 函数组件,直接调用函数,返回值为 children
}
reconcileChildren(fiberNode, [
isVirtualElement(children)
? children
: createTextElement(String(children)),
]);
break;
}
// ...
};
注意这里还有一个细微的区别,就是没有创建真实 DOM 挂到 dom 下,因为他们本身只是用来占位,并不需要映射到真实 DOM 节点,他们返回的内容才是需要渲染真实 DOM 的。
加入状态:useState
第一次调用 useState
时,会用传入的 initState
来初始化 state;组件更新时,第二次渲染时,再调用 useState
就会根据当前的 hookIndex
直接取出之前的 state,这就解释了为什么 hook 调用必须要遵循两个规则:
- 必须在函数第一层,不允许嵌套;
- 必须每次都存在,不得使用条件判断。
因为始终要保持相同的顺序,才能通过 hookIndex
顺利取到上一次的 hook。
每次调用 setState
后,值先进队列,等下次渲染的时候跟前一个值进行合并或代替;其次重新将之前已经清空的 wipRoot
再次赋值,等待下一次 workLoop
检测到,进行下一轮更新。
function useState(initState) {
const hook = wipFiber?.alternate?.hooks
? wipFiber.alternate.hooks[hookIndex]
: {
state: initState,
queue: [],
};
while (hook.queue.length) {
let newState = hook.queue.shift();
if (isPlainObject(hook.state) && isPlainObject(newState)) {
newState = { ...hook.state, ...newState };
}
hook.state = newState;
}
if (typeof wipFiber.hooks === 'undefined') {
wipFiber.hooks = [];
}
wipFiber.hooks.push(hook);
hookIndex += 1;
const setState = (value) => {
hook.queue.push(value);
if (currentRoot) {
wipRoot = {
type: currentRoot.type,
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
currentRoot = null;
}
};
return [hook.state, setState];
}
关于 requestIdleCallback
React 内部并不是直接使用 requestIdleCallback
来调度任务的,而是使用了 scheduler,这里的逻辑就复杂多了,比如还包含了更新任务的优先级等等。
下面这段代码是一段 Mock。
((global) => {
const id = 1;
const fps = 1000 / 60;
let frameDeadline;
let pendingCallback;
const channel = new MessageChannel();
const timeRemaining = () => frameDeadline - window.performance.now();
const deadline = {
didTimeout: false,
timeRemaining,
};
channel.port2.onmessage = () => {
if (typeof pendingCallback === 'function') {
pendingCallback(deadline);
}
};
global.requestIdleCallback = (callback) => {
global.requestAnimationFrame((frameTime) => {
frameDeadline = frameTime + fps;
pendingCallback = callback;
channel.port1.postMessage(null);
});
return id;
};
})(window);
requestIdleCallback
表示回调函数会在浏览器空闲时执行;requestAnimationFrame
会在浏览器每一次重绘前执行。传递给回调函数的 frameTime
表示当前的时间戳,单位为毫秒,加上单帧绘制的时间,就得到了本次工作的截止时间(浏览器大多为 60Hz,也就是每秒刷新 60 帧,那么一帧的时间就是 1000 / 60 毫秒)。workLoop
每次都会检查下,只有剩余时间大于 1 毫秒才会继续执行任务。
const workLoop = (deadline) => {
while (nextUnitOfWork && deadline.timeRemaining() > 1) { // 剩余时间>1ms
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
window.requestIdleCallback(workLoop);
};
完整代码 & 总结
以上,就实现了一个 Mini React。
总结整个流程:
createElement
将待渲染的 JSX 转成一棵树;render
初始化根节点的 FiberNode;workLoop
检测到有待处理的工作,开始计算节点;performUnitOfWork
逐一计算节点,按照深度优先遍历树,为当前节点创建真实 DOM;reconcileChildren
为当前节点的子节点创建 FiberNode,并建立节点之间的上下级、邻里关系;commitRoot
提交所有最新节点,更新到真实 DOM,并清空wipRoot
;- 更新流程:
- 调用
setState
将新值推入队列 - 重新填充
wipRoot
workLoop
检测到有新的工作,开始新一轮的更新,期间使用setState
的新值和旧值合并或替换- ...
- 调用
完整代码如下,还可以从原作者这里 clone 整个项目进行调试。(原项目的示例代码比较复杂,我自己写了个简单的,附在文末了)
Mini React:
let wipRoot = null;
let nextUnitOfWork = null;
let currentRoot = null;
let deletions = [];
let wipFiber;
let hookIndex = 0;
const Fragment = Symbol.for('react.fragment');
((global) => {
const id = 1;
const fps = 1e3 / 60;
let frameDeadline;
let pendingCallback;
const channel = new MessageChannel();
const timeRemaining = () => frameDeadline - window.performance.now();
const deadline = {
didTimeout: false,
timeRemaining,
};
channel.port2.onmessage = () => {
if (typeof pendingCallback === 'function') {
pendingCallback(deadline);
}
};
global.requestIdleCallback = (callback) => {
global.requestAnimationFrame((frameTime) => {
frameDeadline = frameTime + fps;
pendingCallback = callback;
channel.port1.postMessage(null);
});
return id;
};
})(window);
const isDef = (param) => param !== void 0 && param !== null;
const isPlainObject = (val) =>
Object.prototype.toString.call(val) === '[object Object]' &&
[Object.prototype, null].includes(Object.getPrototypeOf(val));
const isVirtualElement = (e) => typeof e === 'object';
const createTextElement = (text) => ({
type: 'TEXT',
props: {
nodeValue: text,
},
});
const createElement = (type, props = {}, ...child) => {
const children = child.map((c) =>
isVirtualElement(c) ? c : createTextElement(String(c)),
);
return {
type,
props: {
...props,
children,
},
};
};
const updateDOM = (DOM, prevProps, nextProps) => {
const defaultPropKeys = 'children';
for (const [removePropKey, removePropValue] of Object.entries(prevProps)) {
if (removePropKey.startsWith('on')) {
DOM.removeEventListener(
removePropKey.slice(2).toLowerCase(),
removePropValue,
);
} else if (removePropKey !== defaultPropKeys) {
DOM[removePropKey] = '';
}
}
for (const [addPropKey, addPropValue] of Object.entries(nextProps)) {
if (addPropKey.startsWith('on')) {
DOM.addEventListener(addPropKey.slice(2).toLowerCase(), addPropValue);
} else if (addPropKey !== defaultPropKeys) {
DOM[addPropKey] = addPropValue;
}
}
};
const createDOM = (fiberNode) => {
const { type, props } = fiberNode;
let DOM = null;
if (type === 'TEXT') {
DOM = document.createTextNode('');
} else if (typeof type === 'string') {
DOM = document.createElement(type);
}
if (DOM !== null) {
updateDOM(DOM, {}, props);
}
return DOM;
};
const commitRoot = () => {
const findParentFiber = (fiberNode) => {
if (fiberNode) {
let parentFiber = fiberNode.return;
while (parentFiber && !parentFiber.dom) {
parentFiber = parentFiber.return;
}
return parentFiber;
}
return null;
};
const commitDeletion = (parentDOM, DOM) => {
if (isDef(parentDOM)) {
parentDOM.removeChild(DOM);
}
};
const commitReplacement = (parentDOM, DOM) => {
if (isDef(parentDOM)) {
parentDOM.appendChild(DOM);
}
};
const commitWork = (fiberNode) => {
if (fiberNode) {
if (fiberNode.dom) {
const parentFiber = findParentFiber(fiberNode);
const parentDOM = parentFiber?.dom;
switch (fiberNode.effectTag) {
case 'REPLACEMENT':
commitReplacement(parentDOM, fiberNode.dom);
break;
case 'UPDATE':
updateDOM(
fiberNode.dom,
fiberNode.alternate ? fiberNode.alternate.props : {},
fiberNode.props,
);
break;
default:
break;
}
}
commitWork(fiberNode.child);
commitWork(fiberNode.sibling);
}
};
for (const deletion of deletions) {
if (deletion.dom) {
const parentFiber = findParentFiber(deletion);
commitDeletion(parentFiber?.dom, deletion.dom);
}
}
if (wipRoot !== null) {
commitWork(wipRoot.child);
currentRoot = wipRoot;
}
wipRoot = null;
};
const reconcileChildren = (fiberNode, elements = []) => {
let index = 0;
let oldFiberNode = void 0;
let prevSibling = void 0;
const virtualElements = elements.flat(Infinity);
if (fiberNode.alternate && fiberNode.alternate.child) {
oldFiberNode = fiberNode.alternate.child;
}
while (
index < virtualElements.length ||
typeof oldFiberNode !== 'undefined'
) {
const virtualElement = virtualElements[index];
let newFiber = void 0;
const isSameType = Boolean(
oldFiberNode &&
virtualElement &&
oldFiberNode.type === virtualElement.type,
);
if (isSameType && oldFiberNode) {
newFiber = {
type: oldFiberNode.type,
dom: oldFiberNode.dom,
alternate: oldFiberNode,
props: virtualElement.props,
return: fiberNode,
effectTag: 'UPDATE',
};
}
if (!isSameType && Boolean(virtualElement)) {
newFiber = {
type: virtualElement.type,
dom: null,
alternate: null,
props: virtualElement.props,
return: fiberNode,
effectTag: 'REPLACEMENT',
};
}
if (!isSameType && oldFiberNode) {
deletions.push(oldFiberNode);
}
if (oldFiberNode) {
oldFiberNode = oldFiberNode.sibling;
}
if (index === 0) {
fiberNode.child = newFiber;
} else if (typeof prevSibling !== 'undefined') {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index += 1;
}
};
const performUnitOfWork = (fiberNode) => {
const { type } = fiberNode;
switch (typeof type) {
case 'function': {
wipFiber = fiberNode;
wipFiber.hooks = [];
hookIndex = 0;
let children;
if (Object.getPrototypeOf(type).REACT_COMPONENT) {
const C = type;
const component = new C(fiberNode.props);
const [state, setState] = useState(component.state);
component.props = fiberNode.props;
component.state = state;
component.setState = setState;
children = component.render?.bind(component)();
} else {
children = type(fiberNode.props);
}
reconcileChildren(fiberNode, [
isVirtualElement(children)
? children
: createTextElement(String(children)),
]);
break;
}
case 'number':
case 'string':
if (!fiberNode.dom) {
fiberNode.dom = createDOM(fiberNode);
}
reconcileChildren(fiberNode, fiberNode.props.children);
break;
case 'symbol':
if (type === Fragment) {
reconcileChildren(fiberNode, fiberNode.props.children);
}
break;
default:
if (typeof fiberNode.props !== 'undefined') {
reconcileChildren(fiberNode, fiberNode.props.children);
}
break;
}
if (fiberNode.child) {
return fiberNode.child;
}
let nextFiberNode = fiberNode;
while (typeof nextFiberNode !== 'undefined') {
if (nextFiberNode.sibling) {
return nextFiberNode.sibling;
}
nextFiberNode = nextFiberNode.return;
}
return null;
};
const workLoop = (deadline) => {
while (nextUnitOfWork && deadline.timeRemaining() > 1) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
window.requestIdleCallback(workLoop);
};
const render = (element, container) => {
currentRoot = null;
wipRoot = {
type: 'div',
dom: container,
props: {
children: [
{
...element,
},
],
},
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
};
function useState(initState) {
const hook = wipFiber?.alternate?.hooks
? wipFiber.alternate.hooks[hookIndex]
: {
state: initState,
queue: [],
};
while (hook.queue.length) {
let newState = hook.queue.shift();
if (isPlainObject(hook.state) && isPlainObject(newState)) {
newState = { ...hook.state, ...newState };
}
hook.state = newState;
}
if (typeof wipFiber.hooks === 'undefined') {
wipFiber.hooks = [];
}
wipFiber.hooks.push(hook);
hookIndex += 1;
const setState = (value) => {
hook.queue.push(value);
if (currentRoot) {
wipRoot = {
type: currentRoot.type,
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
currentRoot = null;
}
};
return [hook.state, setState];
}
class Component {
props;
constructor(props) {
this.props = props;
}
static REACT_COMPONENT = true;
}
void (function main() {
window.requestIdleCallback(workLoop);
})();
export default {
createElement,
render,
useState,
Component,
Fragment,
};
示例代码参考:
import React from './mini-react.js';
function Child() {
const [count, setCount] = React.useState(0)
const handleClick = () => {
setCount(count + 1);
}
return (
<div onClick={handleClick}>
点击了 {count} 次
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
title: 'hello'
}
}
render() {
const {title} = this.state;
return (
<div style={{background: '#eee'}}>
<div>{title}</div>
<Child />
</div>
)
}
}
React.render(<App />, document.getElementById('root'));
参考资料
转载自:https://juejin.cn/post/7173341317395644429