likes
comments
collection
share

自己造轮子系列——React框架fiber树实战应用

作者站长头像
站长
· 阅读数 62

写在前面

前一篇我们讲React为了递归更新DOM树不阻塞主线程,而引入了并发模型,把整个递归渲染DOM操作拆分成若干个任务单元执行,这节我们就来讲讲React是怎么拆分任务单元的。

原理剖析

为了组织任务单元,我们需要学习一个新的数据结构——fiber树。每个组件对应一个fiber,而每个fiber节点对应一个任务单元

为了加深理解,我们来看一个简单的例子:

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

发挥想象:在render函数中,我们将创建根fiber并将其赋值给nextunitofwork。剩下的任务在performUnitOfWork函数中执行。

自己造轮子系列——React框架fiber树实战应用

实际上,我们将为每个fiber做下面三件事:

  • 将元素添加到DOM中
  • 为元素的子元素创建Fiber
  • 返回下一个任务单元

其实,fiber这种数据结构的目标之一,就是方便查找下一个任务单元。所以每个fiber节点都同时指向它的第一个子节点(child)、下一个兄弟节点(sibling)和它的父节点(parent)的。

而当一个fiber节点任务单元完成时:

  • 如果它有子节点,那子节点就是下一个执行单元。在我们的示例中,当div这个任务单元完成时,下一个任务执行单元就是H1节点。

  • 如果一个fiber节点没有子节点,就会执行下一个兄弟节点的任务单元。示例中的p任务单元执行完成,a是下一个任务执行单元。

  • 如果一个fiber节点没有子节点没有下一个兄弟节点,则会指向父节点的兄弟节点(uncle节点),比如说上面示例中的a任务单元的下一个任务单元是h2任务单元。

自己造轮子系列——React框架fiber树实战应用

如果父 parent 节点没有兄弟节点silbing,就继续找父节点的父节点,直到该节点有兄弟节点sibling,或者直到达到根节点。到达根节点意味着完成了整个树的 render

综上,每个fiber节点都可以指向第一个子节点(child)、下一个兄弟节点(sibling)和它的父节点(parent),并且至少有其中一个指向:

自己造轮子系列——React框架fiber树实战应用

代码实现

旧递归逻辑

在前面的章节中,我们讲了普通的递归来实现render方法,具体代码如下:

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

fiber改造

接下来我们应用前面原理剖析的理论知识,来把DOM递归渲染的render方法改造成fiber渲染的方式:

1. 将 render 中创建 DOM 节点的部分抽离为 creactDOM 函数;
/**
 * createDom 创建 DOM 节点
 * @param {fiber} fiber 节点
 * @return {dom} dom 节点
 */
function createDom (fiber) {
    // 如果是文本类型,创建空的文本节点,如果不是文本类型,按 type 类型创建节点
    const dom = fiber.type === 'TEXT_ELEMENT'
        ? document.createTextNode("")
        : document.createElement(fiber.type)

    // isProperty 表示不是 children 的属性
    const isProperty = key => key !== "children"

    // 遍历 props,为 dom 添加属性
    Object.keys(fiber.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = fiber.props[name]
        })

    // 返回 dom
    return dom
}

createDom中,创建DOM节点,该函数接收一个fiber作为参数,处理不同类型的节点,生成新的DOM结构,并返回该DOM节点。

2. 在 render 中设置第一个任务单元为 fiber 根节点;

fiber 根节点仅包含 children 属性,值为长度为1的fiber数组。

// 下一个任务单元
let nextUnitOfWork = null
/**
 * 将 fiber 添加至真实 DOM
 * @param {element} fiber
 * @param {container} 真实 DOM
 */
function render (element, container) {
    nextUnitOfWork = {
        dom: container,
        props: {
            children: [element]
        }
    }
}
3. 通过 requestIdleCallback 在浏览器空闲时,渲染 fiber;
/**
 * workLoop 工作循环函数
 * @param {deadline} 截止时间
 */
function workLoop(deadline) {
  // 是否应该停止工作循环函数
  let shouldYield = false

  // 如果存在下一个任务单元,且没有优先级更高的其他工作时,循环执行
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )

    // 如果截止时间快到了,停止工作循环函数
    shouldYield = deadline.timeRemaining() < 1
  }

  // 通知浏览器,空闲时间应该执行 workLoop
  requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)

workLoop类似任务栈,不停地处理一个又一个任务单元,并且添加了是否中止的判断,同时还有继续下一个任务单元的逻辑。这样可以有效地避免主线程被阻塞,这也是React采用fiber数据结构的最重要的目的。

4. 渲染 fiber 的函数 performUnitOfWork;

performUnitOfWork主要实现前面提到的三个功能:

  • 添加 dom 节点
    1. 如果 fiber 没有 dom 节点,为它创建一个 dom 节点;
    2. 如果 fiber 有父节点,将 fiber.dom 添加至父节点;
  • 新建 filber
    1. 遍历子节点;
    2. 创建 fiber;
    3. 将第一个子节点设置为 fiber 的子节点;
    4. 第一个之外的子节点设置为该节点的兄弟节点;
  • 返回下一个任务单元(fiber)
    1. 如果有子节点,返回子节点;
    2. 如果有兄弟节点,返回兄弟节点;
    3. 否则继续走 while 循环,直到找到 root;
/**
 * performUnitOfWork 处理任务单元
 * @param {fiber} fiber
 * @return {nextUnitOfWork} 下一个任务单元
 */
function performUnitOfWork(fiber) {
    //----------------------1. 添加 dom 节点----------------------
    // 如果 fiber 没有 dom 节点,为它创建一个 dom 节点
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    
    // 如果 fiber 有父节点,将 fiber.dom 添加至父节点
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom)
    }
    //----------------------2 新建 filber----------------------
    // 子节点
    const elements = fiber.props.children
    // 索引
    let index = 0
    // 上一个兄弟节点
    let prevSibling = null
    // 遍历子节点
    while (index < elements.length) {
        const element = elements[index]

        // 创建 fiber
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

        // 将第一个子节点设置为 fiber 的子节点
        if (index === 0) {
            fiber.child = newFiber
        } else if (element) {
        // 第一个之外的子节点设置为该节点的兄弟节点
            prevSibling.sibling = newFiber
        }

        prevSibling = newFiber
        index++
    }
    //----------------------3 返回下一个任务单元(fiber)----------------------
    // 如果有子节点,返回子节点
    if (fiber.child) {
        return fiber.child
    }
    let nextFiber = fiber
    while (nextFiber) {
        // 如果有兄弟节点,返回兄弟节点
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }

        // 否则继续走 while 循环,直到找到 root。
        nextFiber = nextFiber.parent
    }
}

performUnitOfWork函数,我们接受一个fiber作为入参,然后处理当前任务单元,并返回下一个任务单元。

以上我们实现了将 fiber树渲染到页面的功能,并且渲染过程是可中断的。

写在后面

今天就到这里,fiber的内容还是挺多的,由于代码内容比较多,功能集中,所以多以注释的方式,解释了实现细节,可以慢慢消化。

不过重要的是懂得原理,然后再学习下具体的实现细节

转载自:https://juejin.cn/post/7249286903207804986
评论
请登录