实现一个 Mini React

1,111 阅读12分钟

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'));

这里我们需要实现 rendercreateElement 方法。

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 表示元素的类型,目前也就是元素标签 divprops 整合了自身传入的 propschildrenchildren 如果是纯文本节点,则进行标准化,否则原封不动地放进数组。

例如对于:

<div id="test">
  <h1>hello</h1>
  world
</div>

则会先转为:

React.createElement('div', { id: 'test' }, [React.createElement('h1', {}, 'hello'), 'world'])

调用 createElement 后生成一个 React element,也可以理解为一颗描述了 UI 的虚拟 DOM 树(在 React 中 virtual DOM 是个笼统的概念,React elements 和后文即将提到的 fibers 都是它的部分实现):

{
  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 第一个参数为要渲染的 element,即上文得到的虚拟 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 节点都有着相似的结构。还注意到 wipRoot 的值赋给了 nextUnitOfWorknextUnitOfWork 是个非常重要的角色,它表示当前正在处理的节点。在 React 中,每次都是一个一个节点来处理,节点与节点之间是可以打断的,这里的处理包括创建 Fiber 节点,新旧节点对比更新、状态计算等,最终得到一个最新的虚拟 DOM 树(Fiber 树),然后将其一并提交一口气更新到真实 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 里列出了需要干哪些活:

  1. 如果当前有需要处理的节点 nextUnitOfWork,且还有剩余时间(这部分后文会详细叙述),就处理该节点,并返回下一个待处理的节点;
  2. 如果所有节点都处理完毕,就可以提交整个虚拟 DOM 来一次性完成真实 DOM 的渲染;
  3. 进入下一轮巡逻,如果有任何变动将会进行下一轮更新。

第 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。有了这些指针,就能通过深度优先遍历,来挨个处理树上的所有节点了。

image.png

节点计算

根据传入的节点类型不同,分情况处理,上文我们举的例子中,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 方法,在这个方法中,将会对节点树中的节点建立链接,以便可以进行节点树的遍历。最后返回下一个待处理的节点,按照以下顺序:

  1. 如果有子节点则返回子节点;
  2. 如果有相邻节点则返回相邻节点,如果没有相邻节点则向上一层,查找上层的相邻节点;
  3. 如果都没有,则返回 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

commitRootcommitWork 主要进行 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 调用必须要遵循两个规则:

  1. 必须在函数第一层,不允许嵌套;
  2. 必须每次都存在,不得使用条件判断。

因为始终要保持相同的顺序,才能通过 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。

总结整个流程:

  1. createElement 将待渲染的 JSX 转成一棵树;
  2. render 初始化根节点的 FiberNode;
  3. workLoop 检测到有待处理的工作,开始计算节点;
  4. performUnitOfWork 逐一计算节点,按照深度优先遍历树,为当前节点创建真实 DOM;
  5. reconcileChildren 为当前节点的子节点创建 FiberNode,并建立节点之间的上下级、邻里关系;
  6. commitRoot 提交所有最新节点,更新到真实 DOM,并清空 wipRoot
  7. 更新流程:
    1. 调用 setState 将新值推入队列
    2. 重新填充 wipRoot
    3. workLoop 检测到有新的工作,开始新一轮的更新,期间使用 setState 的新值和旧值合并或替换
    4. ...

完整代码如下,还可以从原作者这里 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'));

参考资料

  1. React 18 Has Been Released. Implement Mini-React in 400 Lines of Code