本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
React框架fiber树实战应用
zhuxiaoqiang · 133浏览 · 发布于2023-06-29 +关注

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

原理剖析

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

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

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

在render函数中,我们将创建根fiber并将其赋值给nexttunitofwork。剩下的任务在performUnitOfWork函数中执行。

image.png

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

  • 将元素添加到DOM中

  • 为元素的子元素创建Fiber

  • 返回下一个工作单元

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

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

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

同理,如果一个fiber节点没有子节点也没有下一个兄弟节点,则会指向父节点(原文为uncle节点,个人以为不妥),比如说上面示例中的a节点和h2节点分别指向父级h1节点和div节点。

image.png

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

image.png

代码实现

旧递归逻辑

在前面的章节中,我们讲了普通的递归来实现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。

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

写在后面

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

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




相关推荐

PHP实现部分字符隐藏

沙雕mars · 1325浏览 · 2019-04-28 09:47:56
Java中ArrayList和LinkedList区别

kenrry1992 · 908浏览 · 2019-05-08 21:14:54
Tomcat 下载及安装配置

manongba · 970浏览 · 2019-05-13 21:03:56
JAVA变量介绍

manongba · 963浏览 · 2019-05-13 21:05:52
什么是SpringBoot

iamitnan · 1086浏览 · 2019-05-14 22:20:36
加载中

0评论

评论
我爱编程,我爱工作,更爱生活
小鸟云服务器
扫码进入手机网页