学习 Build your own React

跟着 https://pomb.us/build-your-own-react/ 尝试实现一个有基本功能的迷你 React。教程内容一流,交互效果更是一流。
作者的仓库:

以及感谢优秀的中译版: https://qcsite.gatsbyjs.io/build-your-own-react/

文章分成8个步骤:

  1. createElement 函数
  2. render 函数
  3. 并发模式
  4. 纤程 (Fiber)
  5. 渲染和提交阶段
  6. 协调 (Reconciliation)
  7. 函数式组件
  8. 钩子

1. createElement 函数

首先,我们有一个简单的 React app

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

首先,我们将它变成 JS:

const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)

createElement 函数需要做的就是创建一个带有 type 和 props 的对象。
例如:

createElement("div", null, a, b)
// 返回
{
  "type": "div",
  "props": { "children": [a, b] }
}

因此,函数如下:

const createElement = (type, props, ...children) => {
  return {
    type,
    props: {
      props,
      children,
    },
  };
};

children 数组中也可能有像 strings、numbers 这样的基本值。为了方便起见,我们对所有不是对象的值创建一个特殊类型 TEXT_ELEMENT,这和 React 有所差异。

export const createElement = (type, props, ...children) => {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        child instanceof Object ? child : createTextElement(child)
      ),
    },
  };
};

const createTextElement = (text) => {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
};

2. render 函数

接下来是模拟 ReactDOM.render 函数,首要解决的问题是:怎么往 DOM 里加东西。
首先,创建对应 DOM 节点,然后赋值属性,最后将节点添加到容器。
递归地处理每一个子节点。

export const render = (element, container) => {
  // 根据类型创建 DOM 节点
  const dom =
    element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);
  // 把 element 里的属性赋给 node
  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);
};

相关链接:
Array.prototype.filter() - MDN
Document.createTextNode() - MDNNode.nodeValue - MDN

至此,我们得到了一个简单的能将 JSX 渲染成 DOM 的库。
可以使用以下代码尝试效果,也可以在 原作者提供的 codesandbox 上尝试。

import * as MyReact from "./MyReact";

/** @jsx MyReact.createElement */
const element = (
  <div style="background: salmon">
    <h1>Hello World! MyReact!</h1>
    <h2 style="text-align:right">Powered by Didact</h2>
  </div>
);
const container = document.getElementById("root");
MyReact.render(element, container);

3. 并发模式

render 函数 一节中,我们需要递归地处理子节点。渲染一旦开始就无法停止,可能会阻塞主线程。
因此,我们将任务分成多个小块,每完成一个小块,将控制权交还浏览器,让浏览器判断是否有更高优先级的任务。
我们使用 requestIdleCallback 循环来代替 react/packages/scheduler,回调函数会在浏览器空闲的时候被调用。

let nextUnitOfWork = null;

const workLoop = (deadline) => {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    // 时间充足时执行任务,并返回下一个任务
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    // 判断离浏览器收回控制器的时间是否充足
    shouldYield = deadline.timeRemaining() < 1;
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

相关链接:
requestIdleCallback - MDN

4. 纤程(Fiber)

为了组织任务单元,我们需要一个数据结构: Fiber 树。每个 Fiber 节点对应一个 React element。

其中,处理顺序是:子节点 -> 兄弟节点 -> 叔节点(父节点的兄弟)
对应的处理函数则是 performUnitOfWork,它需要完成三件事:

  1. 把 element 添加到 DOM 上;
  2. 为当前 element 的子节点新建对应 fiber 节点;
  3. 挑选下一个任务单元

4.1 改进 render

由于原有的 render 函数仅用于插入,因此需要对其进行改进。
首先注释掉旧的 render 函数,并新建一个 render 函数,将 nextUnitOfWork 置为 Fiber 树的根节点。

export const render = (element, container) => {
  // 将 nextUnitOfWork 置为 fiber 树的根节点
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  };
};

4.2 将节点插入 DOM

首先我们需要创建节点,这里可以复用部分旧的 render 代码。
将旧的 render 改名为 createDom,删除递归执行和插入部分:

const createDom = (fiber) => {
  // 根据类型创建 DOM 节点
  const dom =
    fiber.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);
  // 把 fiber 里的属性赋给 node
  const isProperty = (key) => key !== "children";
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = fiber.props[name];
    });
  return dom;
};

创建 Fiber 对应的 DOM 节点,并插入到父节点里。

const performUnitOfWork = (fiber) => {
  // 创建节点并插入
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  // TODO create new fibers
  // TODO return next unit of work
};

4.3 为子节点创建 Fiber 节点

然后为子节点创建 Fiber 节点并且插入 Fiber 树。

const performUnitOfWork = (fiber) => {
  // 1. 创建节点并插入
  // ...
  // 2. 为 element 子节点创建 Fiber 节点
  const elements = fiber.props.children;
  let prevSibling = null;
  elements.forEach((element, index) => {
    // 新建 Fiber 节点
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    };
    if (index === 0) {
      // 父节点的 child 指向第一个子节点
      fiber.child = newFiber;
    } else {
      // sibling 指向兄弟节点
      prevSibling.sibling = newFiber;
    }
    prevSibling = newFiber;
  });
  // TODO return next unit of work
};

4.4 选择下一个任务单元

按照 子节点 -> 兄弟节点 -> 叔节点(父节点的兄弟)的顺序选择下一个任务单元

const performUnitOfWork = (fiber) => {
  // 1. 创建节点并插入
  // ...
  // 2. 为 element 子节点创建 Fiber 节点 
  // ...
  // 3. 选出下一个任务单元:子 -> 兄弟 -> 叔
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
};

相关链接:
Fiber的结构 - React技术揭秘

5. 渲染和提交阶段

目前,我们的方案会持续生成 DOM 节点并插入到父节点。在完成渲染之前,如果浏览器需要中断这个过程,用户可能会看到渲染不完整的界面。
因此我们在内存中完成修改,所有的任务都完成后再一起添加到 DOM。
我们用一棵叫做 wipRoot(work in progress root)的树来追踪修改。
首先修改 render 函数。在原来的基础上,将 wipRoot 置为 Fiber 树的根节点。

export const render = (element, container) => {
  // 将 nextUnitOfWork 和 wipRoot 置为 fiber 树的根节点
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  };
  nextUnitOfWork = wipRoot;
};

let nextUnitOfWork = null;
// work in progress root,用于追踪修改
let wipRoot = null;

然后,删除 performUnitOfWork 里插入 DOM 节点的部分。

const performUnitOfWork = (fiber) => {
  // 1. 创建节点
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  // 2. 为 element 子节点创建 Fiber 节点
  // ...
  // 3. 选出下一个任务单元:子 -> 兄弟 -> 叔
  // ...
};

修改 workLoop,当所有修改已经完成,由 commitRoot 将变化提交到实际 DOM。

const commitRoot = () => {
  // TODO
};

const workLoop = (deadline) => {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    // 时间充足时执行任务,并返回下一个任务
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    // 判断离浏览器收回控制器的时间是否充足
    shouldYield = deadline.timeRemaining() < 1;
  }
  // 判断所有任务已经完成
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  requestIdleCallback(workLoop);
};

最后,添加 commitRootcommitWork,递归将节点插入 DOM。

const commitRoot = () => {
  commitWork(fiber);
  wipRoot = null;
};

const commitWork = (fiber) => {
  if (!fiber) {
    return;
  }
  const parentDom = fiber.parent.dom;
  parentDom.appendChild(fiber.dom);
  // 递归添加节点
  commitWork(fiber.child);
  commitWork(fiber.sibling);
};

相关链接:
什么是“双缓存” - React技术揭秘

6. 协调 (Reconciliation)

这一步,需要在之前增加节点的基础上,实现更新和删除。

6.1 保存上次提交

  1. 新增 currentRoot 用来保存上次提交到 DOM 节点的 Fiber 树,并修改 commitRoot
  2. Fiber 节点内增加 alternate,用于记录上一个 commit 阶段使用的 fiber 节点的引用
    const commitRoot = () => {
      commitWork(wipRoot.child);
      currentRoot = wipRoot;
      wipRoot = null;
    };
    
    export const render = (element, container) => {
      // 将 nextUnitOfWork 和 wipRoot 置为 fiber 树的根节点
      wipRoot = {
        dom: container,
        props: {
          children: [element],
        },
        // 记录上一个 commit 阶段使用的 fiber 节点的引用
        alternate: currentRoot,
      };
      nextUnitOfWork = wipRoot;
    };
    
    // 上次提交到 DOM 节点的 fiber 树
    let currentRoot = null;

6.2 同时迭代

将为子节点创建 Fiber 节点的步骤抽取成 reconcileChildren

const performUnitOfWork = (fiber) => {
  // 1. 创建节点
  // ...
  // 2. 为 element 子节点创建 Fiber 节点
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
  // 3. 选出下一个任务单元:子 -> 兄弟 -> 叔
  // ...
};

const reconcileChildren = (wipFiber, elements) => {
  // ...
};

修改循环条件,迭代 elements 的同时也迭代 oldFiber

const reconcileChildren = (wipFiber, elements) => {
  let index = 0;
  // 任一是 undefined 时返回 undefined,都存在时返回后者
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;
  // 同时迭代 elements 和 oldFiber
  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    // 新建 Fiber 节点
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: wipFiber,
      dom: null,
    };
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }
    if (index === 0) {
      // 父节点的 child 指向第一个子节点
      wipFiber.child = newFiber;
    } else {
      // sibling 指向兄弟节点
      prevSibling.sibling = newFiber;
    }
    index++;
    prevSibling = newFiber;
  }
};

6.3 比较区别

通过比较 React element 和上一次提交的 Fiber node 我们可以找到需要在 DOM 上进行哪些改动。
比较的方法:

  • 旧的 Fiber 和新的 React element 类型相同,则复用节点,仅修改属性
  • 类型不同且有新的 element,需要增加 DOM 节点
  • 类型不同且有旧 Fiber,需要删除 DOM 节点

分类设计 newFiber

const reconcileChildren = (wipFiber, elements) => {
  let index = 0;
  // 任一是 undefined 时返回 undefined,都存在时返回后者
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;
  // 同时迭代 elements 和 oldFiber
  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    let newFiber = null;
    const sameType = element && oldFiber && element.type === oldFiber.type;
    if (sameType) {
      // 类型相同,复用
    }
    if (element && !sameType) {
      // 需要新增
    }
    if (oldFiber && !sameType) {
      // 需要删除
    }
    if (index === 0) {
      // 父节点的 child 指向第一个子节点
      wipFiber.child = newFiber;
    } else {
      // sibling 指向兄弟节点
      prevSibling.sibling = newFiber;
    }
    index++;
    prevSibling = newFiber;
  }
};

后面针对各种情况继续修改 reconcileChildren

6.3.1 复用

复用旧的 DOM 节点,仅将 React element 的属性置换进去。effectTag 设置为 "UPDATE",后面会用到。

if (element && !sameType) {
  // 需要新增
  newFiber = {
    type: element.type,
    props: element.props,
    dom: null,
    parent: wipFiber,
    alternate: null,
    effectTag: "PLACEMENT",
  };
}

6.3.2 新增

effectTag 设置为 "PLACEMENT"

if (element && !sameType) {
  // 需要新增
  newFiber = {
    type: element.type,
    props: element.type,
    dom: null,
    parent: wipFiber,
    alternate: null,
    effectTag: "PLACEMENT",
  };
}

6.3.3 删除

删除时,不需要创建新 Fiber 节点,因此没有 newFiber,就只修改旧节点的 effectTag。同时由于我们 commit 的时候,并不会再遍历旧的 Fiber 树,所以需要使用一个数组标记哪些节点被删除。
新建数组

let nextUnitOfWork = null;
// 上次提交到 DOM 节点的 fiber 树
let currentRoot = null;
// work in progress root,用于追踪修改
let wipRoot = null;
// 需要删除的 DOM 节点
let deletions = null;

修改 reconcileChildren

if (oldFiber && !sameType) {
  // 需要删除
  oldFiber.effectTag = "DELETION";
  deletions.push(oldFiber);
}

同时,由于删除不产生新的 fiber,所以需要修改连接兄弟的条件

if (index === 0) {
  // 父节点的 child 指向第一个子节点
  wipFiber.child = newFiber;
  // 原来是 else
} else if (element) {
  // sibling 指向兄弟节点
  prevSibling.sibling = newFiber;
}

修改 render

export const render = (element, container) => {
  // 将 nextUnitOfWork 和 wipRoot 置为 fiber 树的根节点
  wipRoot = {
    // ...
  };
  nextUnitOfWork = wipRoot;
  // 初始化储存需要删除的 DOM 节点的数组
  deletions = [];
};

修改 commitRoot,提交需要删除的节点

const commitRoot = () => {
  // 需要删除的节点也需要提交
  deletions.forEach(commitWork);
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
};

6.4 处理 effectTag

我们需要对 commitWork 进行修改,以处理各种条件下 DOM 的变化。
遇到 PLACEMENTDELETION 的情况比较简单,直接插入或删除即可。

const commitWork = (fiber) => {
  if (!fiber) {
    return;
  }
  const parentDom = fiber.parent.dom;
  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    // 需要增加
    parentDom.appendChild(fiber.dom);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    // 需要复用
  } else if (fiber.effectTag === "DELETION") {
    // 需要删除
    parentDom.removeChild(fiber.dom);
  }
  // parentDom.appendChild(fiber.dom);
  // 递归添加节点
  commitWork(fiber.child);
  commitWork(fiber.sibling);
};

下面考虑复用情况:
新建 updateDom 函数用于属性的新增和更新

const updateDom = (dom, prevProps, nextProps) => {
  const isProperty = (key) => key !== "children";
  const isNew = (prev, next) => (key) => prev[key] !== next[key];
  const isGone = (prev, next) => (key) => !(key in next);
  // 移除旧属性
  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];
    });
};

修改 commitWork

else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    // 需要复用
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

同时,发现这个函数也可以用在 createDom

const createDom = (fiber) => {
  // 根据类型创建 DOM 节点
  const dom =
    fiber.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);
  // 把 fiber 里的属性赋给 node
  updateDom(dom, {}, fiber.props);
  return dom;
};

6.5 处理事件监听

updateDom 中,当我们遇到事件监听时,需要特殊处理。

const updateDom = (dom, prevProps, nextProps) => {
  // 判断是否时事件监听 (on 开头)
  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);
  // 移除旧的和发生改变的事件监听
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      // 旧的和改变的都要移除
      (key) =>
        isGone(prevProps, nextProps)(key) || isNew(prevProps, nextProps)(key)
    )
    .forEach((name) => {
      // 得到事件类型:onClick => click
      const eventType = name.toLowerCase().substring(2);
      dom.removeEventListener(eventType, prevProps[name]);
    });
  // 移除旧属性
  // ...
  // 新增或更新属性
  // ...
  // 新增事件监听
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach((name) => {
      const eventType = name.toLowerCase().substring(2);
      dom.addEventListener(eventType, nextProps[name]);
    });
};

相关链接:
Reconciliation - React
事件处理 - React
事件列表 - MDN

至此,我们完成了增删改功能。可以使用以下代码或者在原作者提供的 codesandbox尝试。

/** @jsx MyReact.createElement */
const container = document.getElementById("root");

const updateValue = (e) => {
  rerender(e.target.value);
};

const rerender = (value) => {
  const element = (
    <div>
      <input onInput={updateValue} value={value} />
      <h2>Hello {value}</h2>
    </div>
  );
  MyReact.render(element, container);
};

rerender("World");

7. 函数式组件

下面是支持函数式组件。当遇到函数式组件时,jsx 转换成 js 的情况如下:

// jsx
function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
// js
function App(props) {
  return MyReact.createElement(
    "h1",
    null,
    "Hi ",
    props.name
  )
}
const element = MyReact.createElement(App, {
  name: "foo",
})

函数式组件有以下两个不同点:

  • 函数式组件的 Fiber 没有 DOM 节点
  • 子节点来自函数的运行而不是直接从 props 读取。

因此,当 Fiber 的类型是函数的时候,需要分类处理。
关键就是如何获得子节点。参考上面的例子,我们可以发现,运行函数即可。

const performUnitOfWork = (fiber) => {
  // 判断是否为函数式组件
  const isFunctionComponent = fiber.type instanceof Function;
  // 为 element 子节点创建 Fiber 节点
  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;
  }
};

const updateFunctionComponent = (fiber) => {
  // 函数式组件不能从 props 读子组件
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
};

const updateHostComponent = (fiber) => {
  // 1. 创建节点
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  // 2. 为 element 子节点创建 Fiber 节点
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
};

同时,由于函数式组件没有 DOM 节点,所以在寻找父子节点的时候,需要跳过并找到离它最近的上(下)一个节点。
修改 commitWork,增加 commitDeletion

const commitWork = (fiber) => {
  if (!fiber) {
    return;
  }
  let domParentFiber = fiber.parent;
  // 找父节点,需要跳过函数式组件,找到上面第一个有 DOM 节点的
  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);
};

const commitDeletion = (fiber, domParent) => {
  // 函数式组件没有 DOM 节点,需要找到 fiber 下第一个有 DOM 节点的
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
};

8. 钩子 (Hooks)

首先加入一些全局变量,以及在 Fiber 中加入 hook 数组。

let wipFiber = null;
let hookIndex = null;

const updateFunctionComponent = (fiber) => {
  wipFiber = fiber;
  hookIndex = 0;
  // 支持同一组件多次使用 useState
  wipFiber.hooks = [];
  // 函数式组件不能从 props 读子组件
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
};

下一步是实现 useState 函数。

export const useState = (initial) => {
  // 判断是否存在旧的 hook,存在则取出旧值
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  // 旧 hook 存在则复制一份,不存在则初始化
  const hook = { state: oldHook ? oldHook.state : initial };
  // 将 hook 放入 Fiber
  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state];
};

下一步就是实现 setState

export const useState = (initial) => {
  // 判断是否存在旧的 hook,存在则取出旧值
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  // 旧 hook 存在则复制一份,不存在则初始化
  const hook = {
    state: oldHook ? oldHook.state : initial,
    // 存放 setState 的 action
    queue: [],
  };
  const setState = (action) => {
    hook.queue.push(action);
    // 设置 wipRoot 以及 nextUnitOfWork,workLoop 被2调用时会开启新一轮的渲染
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
  // 将 hook 放入 Fiber
  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
};

最后,执行 actions,更新状态。

export const useState = (initial) => {
  // 判断是否存在旧的 hook,存在则取出旧值
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  // 旧 hook 存在则复制一份,不存在则初始化
  const hook = {
    state: oldHook ? oldHook.state : initial,
    // 存放 setState 的 action
    queue: [],
  };
  // 执行 setState 里的 action
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach((action) => {
    hook.state = action(hook.state);
  });
  const setState = (action) => {
    hook.queue.push(action);
    // 设置 wipRoot 以及 nextUnitOfWork,workLoop 被2调用时会开启新一轮的渲染
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
  // 将 hook 放入 Fiber
  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
};

相关链接:
Introducing Hooks - React

最终,我们构建了属于我们自己的 React。可以使用以下代码或者在原作者提供的 codesandbox尝试。

import * as MyReact from "./MyReact";

/** @jsx MyReact.createElement */
const Counter = () => {
  const [state, setState] = MyReact.useState(0);
  return (
    <div>
      <h1>Count: {state}</h1>
      <button onClick={() => setState((c) => c + 1)}>+</button>
      <button onClick={() => setState((c) => c - 1)}>-</button>
    </div>
  );
};
const App = () => {
  return (
    <div>
      <h1>This is a counter</h1>
      <Counter />
    </div>
  );
};
const container = document.getElementById("root");
MyReact.render(<App />, container);

学习 Build your own React
https://blog.loststar.tech/posts/633c953d/
作者
loststar
发布于
2023年1月24日
许可协议