Skip to content

自己动手实现 React

Updated: at 12:00 AM

这篇文章会实现一个极小版 React。它不会覆盖真实 React 的优化、调度细节和边界情况,但会保留一条非常重要的主线:JSX 如何变成 element,element 如何变成 DOM node,更新时如何通过 fiber tree 做协调(reconciliation),最后再支持函数组件和 Hooks。

我们会把这个小库叫作 MiniReact。最终它能完成这些能力:

整篇文章会按从 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 还有更多内部字段,但我们只关心 typeprops。如果把函数调用直接替换成结果,大概是这样:

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

这段代码里有三个术语需要固定下来:

到这里,我们已经知道自己要实现两件事:一个 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 只需要返回一个带 typeprops 的对象。为了让后面的渲染逻辑更简单,我们还会保证 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 既保存渲染所需的数据,也是一个工作单元。

Fiber Tree 0

假设要渲染这棵树:

MiniReact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
);

render 里先创建 root fiber,并把它设置为 nextUnitOfWork。之后 performUnitOfWork 会对每个 fiber 做三件事:

  1. 为当前 fiber 创建 DOM node
  2. 为 children 创建新的 fibers
  3. 返回下一个工作单元

Fiber Tree 1

为了快速找到下一个工作单元,每个 fiber 都有三个指针:childsiblingparent

Fiber Tree 2

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

Fiber Tree 3

没有 child 就处理 sibling

Fiber Tree 4

如果既没有 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 的两个阶段已经分开:

下一步处理更新。只有新增还不够,我们需要比较新旧树。

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++;
  }
}

判断规则很简化:

真实 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 流程。

函数组件有两个关键差异:

也就是说,普通 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,可以从这些点开始:

附注:本文实现路线参考了 Build your own React,但正文聚焦 MiniReact 的实现过程。

参考文献

  1. Build your own React