手写 Fiber 架构的 React

AI 摘要从 JSX、虚拟 DOM 到任务调度,手写理解 React Fiber 的核心机制。

为了解决渲染阻塞的问题,React 团队提出了 Fiber 架构。我带你们手写一个带 Fiber 架构的 React,为理解这个所谓的 Fiber 架构。Talk is cheap, show me the code。

JSX 转虚拟 Dom

先来看现有的业务代码

function App() {
    return(
        <ul>
            <li>
                1
                <ul>
                    <li>2</li>
                    <li>3</li>
                    <li>4</li>
                </ul>
            </li>
            <li>
                5
                <ul>
                    <li>6</li>
                    <li>7</li>
                    <li>8</li>
                </ul>
            </li>
            <li>9</li>
        </ul>
    );
}

render(document.querySelector("#app"), App);

需要把 jsx 转换成虚拟 dom。

const vnode = {
  type: "ul",
  props: {
    children: [
      {
        type: "li",
        props: {
          children: [
            "1",
            {
              type: "ul",
              props: {
                children: [
                  {
                    type: "li",
                    props: { children: "2", key: null },
                    key: null,
                  },
                  {
                    type: "li",
                    props: { children: "3", key: null },
                    key: null,
                  },
                  {
                    type: "li",
                    props: { children: "4", key: null },
                    key: null,
                  },
                ],
                key: null,
              },
              key: null,
            },
          ],
          key: null,
        },
        key: null,
      },
      {
        type: "li",
        props: {
          children: [
            "5",
            {
              type: "ul",
              props: {
                children: [
                  {
                    type: "li",
                    props: { children: "6", key: null },
                    key: null,
                  },
                  {
                    type: "li",
                    props: { children: "7", key: null },
                    key: null,
                  },
                  {
                    type: "li",
                    props: { children: "8", key: null },
                    key: null,
                  },
                ],
                key: null,
              },
              key: null,
            },
          ],
          key: null,
        },
        key: null,
      },
      { type: "li", props: { children: "9", key: null }, key: null },
    ],
    key: null,
  },
  key: null,
  stateNode: document.querySelector("#root"),
};

实现细节 这一步实现很简单,首先通过 babel 将 jsx 转换成 react/jsx 的调用。

https://babeljs.io/repl

230cbb8384.png

然后 react/jsx 会通过相应的算法,将不同的对象构建成一个树的结构,最终我们得到一个类似于这样的结构。

70d1789df4.png

常规业务框架

我们拿到上面的这个数据结构,如果不考虑 Fiber,对这个树进行 dfs 就可以写一个简单的声明式框架了。

function createDomElement(vnode) {
  // 创建元素节点
  const element = document.createElement(vnode.type);

  // 如果有子节点,递归处理每个子节点
  const children = vnode.props.children;
  if (Array.isArray(children)) {
    children.forEach(child => {
      if (typeof child === 'object') {
        element.appendChild(createDomElement(child));  // 对象表示子元素节点
      } else if (typeof child === 'string') {
        element.appendChild(document.createTextNode(child));  // 字符串表示文本节点
      }
    });
  } else if (typeof children === 'string') {
    element.appendChild(document.createTextNode(children));
  }

  // 返回构建好的 DOM 元素
  return element;
}

// 获取根节点
const root = document.querySelector("#root");

// 清空原有内容
root.innerHTML = '';

// 创建 DOM 并挂载
const realDom = createDomElement(vnode);
root.appendChild(realDom);

如果再加上一些生命周期,很好,一个 FineUI 就这么设计好了。 如果在 createDomElement 里面加入 diff 算法,就是一个 Preact 了。 如果再加依赖收集,很好,我们得到了一个 Vue。

Fiber

现在我们的 React,虽然完成了声明式 UI ,显然它没有解决我们的问题 。如果这棵虚拟 Dom 太大了,就会使我们的交互进行阻塞。为了不阻塞交互,分成两步。第一步怎么把一个大的任务拆解成一个个小的任务,第二步,怎么一个个小的任务合并成一个个大的任务。 于是,我们引入了第一个概念:Fiber,一个 Fiber 就是一个最小的任务调度单元。 一棵 FiberTree 应该设计什么样的数据结构?我们现有的这棵树用的了“儿子表示法”即父结点保存了孩子结点。 如果不做调整,这么设计有个问题,最简单的就是一个例子就是下面这个例子中的不同组件的 componentDidMount 的生命周期设计与实现,我们知道组件挂载遵循树的后续遍历原则,第一个触发 componentDidMount 的结点是"结点1",那么结点 1 怎么找到它的 parent Fiber 所指代的引用的呢。

cf3559b9ea.png
所以需要把这棵 VirtualDomTree 转换成 FiberTree,FiberTree 结下所示,一个结点有三个指针,分别是 child、return、sibling。
5398af0106.png
每一个结点的含义如下:

  • child 第一个孩子结点
  • sibling 相邻的右兄弟结点
  • return 父结点

我们使用深度优先完成这个算法。

function performUnitOfWork(fiber) {
    let children = fiber.props.children || [];
    let previousSibling = null;

    // 仅当非文本节点时处理子节点
    if (fiber.type !== "txt") {
        for (let i = 0; i < children.length; i++) {
            let child = children[i];

            let newFiber = {
                type: typeof child === "string" ? "txt" : child.type,
                props:
                    typeof child === "string"
                        ? { nodeValue: child }
                        : child.props || {},
                child: null,
                sibling: null,
                return: fiber,
                stateNode: null,
                effectTag: "PLACEMENT"
            };

            if (i === 0) {
                fiber.child = newFiber;
            } else {
                previousSibling.sibling = newFiber;
            }
            previousSibling = newFiber;
        }
    }

    // 继续深度优先遍历
    if (fiber.child) {
        return fiber.child;
    }

    let nextFiber = fiber;
    while (nextFiber) {
        if (nextFiber.sibling) {
            return nextFiber.sibling;
        }
        nextFiber = nextFiber.return;
    }

    return null;
}

这样,我们就成功的从虚拟 DOM 转换成了 FiberTree。

Concurrency

注意哈,这里用的是 concurrency,而不是 parallel。concurrency 的核心点在于怎么切片。我们可以使用 requestIdleCallback,完成这个目的。

let nextUnitOfWork = { text: "根结点" };

function workLoop(deadline) {
    let shouldYield = false;
    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        shouldYield = deadline.timeRemaining() < 1;
    }
    requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(nextUnitOfWork) {
    const next = { text: "下一个结点" };
    console.log(next);
    return next;
}

我们来模拟一下,加入一个 setInterval 模拟用户操作。

<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div></div>

        <script>
            let nextUnitOfWork = { text: "根结点" };

            function workLoop(deadline) {
                let shouldYield = false;
                while (nextUnitOfWork && !shouldYield) {
                    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
                    shouldYield = deadline.timeRemaining() < 1;
                }
                requestIdleCallback(workLoop);
            }

            requestIdleCallback(workLoop);

            function performUnitOfWork(nextUnitOfWork) {
                const next = { text: "下一个结点" };
                console.log(next);
                return next;
            }

            let i = 1;
            const dom = document.querySelector("div");
            setInterval(() => {
                dom.innerHTML = i++;
            }, 20);
        </script>
    </body>
</html>

可以发现即使我们的 FiberTree 非常复杂(无限长),也能正确处理用户响应,同时自己也能同时更新。

Commit

下面,我们需要把 Fiber 和 Concurrency 两个阶段结合起来,就完成了基础的 Fiber 架构设计。在 React 设计中,Fiber 的拆分以及 diff 的对比称为 Reconcile 阶段,而最终渲染到 Dom 上的阶段称为 Commit 阶段。注意哈,Reconcile 阶段是可以被打段的,Commit 阶段是不可以被打断的。

function commitWork(fiber) {
    if (!fiber) return;
    if (fiber.effectTag === "PLACEMENT" && fiber.stateNode) {
        fiber.return.stateNode.append(fiber.stateNode);
    }

    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

综上所述,一个基础的 FiberReact 就已经实现了,下面是代码的完整实现:

<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>React Fiber</title>
    </head>
    <body>
        <div id="root"></div>

        <script>
            let nextUnitOfWork = null;
            let wipRoot = null;

            function workLoop(deadline) {
                let shouldYield = false;

                while (nextUnitOfWork && !shouldYield) {
                    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
                    shouldYield = deadline.timeRemaining() < 1;
                }

                if (!nextUnitOfWork && wipRoot) {
                    commitRoot();
                    clearInterval(timerId);
                    console.log("渲染完成");
                }

                requestIdleCallback(workLoop);
            }

            function performUnitOfWork(fiber) {
                console.log(`处理 fiber: ${fiber.type}`);

                if (!fiber.stateNode) {
                    // 判断是否为文本节点,并特别处理
                    if (fiber.type === "txt") {
                        fiber.stateNode = document.createTextNode(
                            fiber.props.nodeValue
                        );
                    } else {
                        fiber.stateNode = document.createElement(fiber.type);
                    }
                }

                let children = fiber.props.children || [];
                let previousSibling = null;

                // 仅当非文本节点时处理子节点
                if (fiber.type !== "txt") {
                    for (let i = 0; i < children.length; i++) {
                        let child = children[i];

                        let newFiber = {
                            type:
                                typeof child === "string" ? "txt" : child.type,
                            props:
                                typeof child === "string"
                                    ? { nodeValue: child }
                                    : child.props || {},
                            child: null,
                            sibling: null,
                            return: fiber,
                            stateNode: null,
                            effectTag: "PLACEMENT"
                        };

                        if (i === 0) {
                            fiber.child = newFiber;
                        } else {
                            previousSibling.sibling = newFiber;
                        }
                        previousSibling = newFiber;
                    }
                }

                // 继续深度优先遍历
                if (fiber.child) {
                    console.log("一个 fiber 处理完成,可以被用户打断\n");
                    return fiber.child;
                }

                // 查找下一个工作单元
                let nextFiber = fiber;
                while (nextFiber) {
                    if (nextFiber.sibling) {
                        console.log("一个 fiber 处理完成,可以被用户打断\n");
                        return nextFiber.sibling;
                    }
                    nextFiber = nextFiber.return;
                }

                console.log("一个 fiber 处理完成,可以被用户打断\n");
                return null;
            }

            function commitRoot() {
                console.log("==========\n\n");
                console.log("Commit phase\n\n");

                commitWork(wipRoot.child);
                wipRoot = null;
            }

            function commitWork(fiber) {
                if (!fiber) return;
                if (fiber.effectTag === "PLACEMENT" && fiber.stateNode) {
                    fiber.return.stateNode.append(fiber.stateNode);
                }

                commitWork(fiber.child);
                commitWork(fiber.sibling);
            }

            const vnode = {
                type: "ul",
                props: {
                    children: [
                        {
                            type: "li",
                            props: {
                                children: [
                                    "1",
                                    {
                                        type: "ul",
                                        props: {
                                            children: [
                                                {
                                                    type: "li",
                                                    props: {
                                                        children: "2",
                                                        key: null
                                                    },
                                                    key: null
                                                },
                                                {
                                                    type: "li",
                                                    props: {
                                                        children: "3",
                                                        key: null
                                                    },
                                                    key: null
                                                },
                                                {
                                                    type: "li",
                                                    props: {
                                                        children: "4",
                                                        key: null
                                                    },
                                                    key: null
                                                }
                                            ],
                                            key: null
                                        },
                                        key: null
                                    }
                                ],
                                key: null
                            },
                            key: null
                        },
                        {
                            type: "li",
                            props: {
                                children: [
                                    "5",
                                    {
                                        type: "ul",
                                        props: {
                                            children: [
                                                {
                                                    type: "li",
                                                    props: {
                                                        children: "6",
                                                        key: null
                                                    },
                                                    key: null
                                                },
                                                {
                                                    type: "li",
                                                    props: {
                                                        children: "7",
                                                        key: null
                                                    },
                                                    key: null
                                                },
                                                {
                                                    type: "li",
                                                    props: {
                                                        children: "8",
                                                        key: null
                                                    },
                                                    key: null
                                                }
                                            ],
                                            key: null
                                        },
                                        key: null
                                    }
                                ],
                                key: null
                            },
                            key: null
                        },
                        {
                            type: "li",
                            props: { children: "9", key: null },
                            key: null
                        }
                    ],
                    key: null
                },
                key: null,
                stateNode: document.querySelector("#root")
            };

            nextUnitOfWork = vnode;
            wipRoot = vnode;
            requestIdleCallback(workLoop);
        </script>
    </body>
</html>

总结

注意:上面为了更容易理解和代码实现,会有少量错误在里面。 首先并不存在 VirtualDomTree 先生成,然后转换成 FiberTree 这一过程。我们直接在控制台打印一个 jsx 结点可以看到,它并不是一个完整的 VirtualDomTree 结点。在 Reconcile 阶段完成以后,VirualDomTree 和 FiberTree 是同时生成的,并且他们在 Fiber 架构中其实已经被融合到了一起,FiberTree 是对 VirtualDomTree 的扩展。而生成的过程中,需要执行我们的业务代码,也就是我们的一个个组件的 render 函数,或者整个函数组件。 从宏观角度来看,整个过程分为了 Reconcile 阶段和 Commit 阶段,Reconcile 阶段因为包含了用户代码,也正是这个原因,为什么 performUnitOfWork 设计在 Reconcile 阶段。 React Fiber 并不存在什么魔法,根据 Run-to-completion。一个最终对业务侧有意义的一个启发是:在我们的 React 的业务代码中,一个组件应该做更少的事情,而不是一个组件把整个页面全部渲染完成。