动手实现mini React (三)

404 阅读3分钟

别急着划走,相信我,你一定有收获!

文章基于React 16.8

我们将从头开始一步一步重写React。遵循真实的React代码中的架构,但没有所有的优化和非必要的功能

目录

  1. createElement
  2. render
  3. Concurrent Mode
  4. Fibers
  5. Render 和 Commit 阶段
  6. Reconciliation
  7. Function Components
  8. Hooks

Fibers

通过上一篇文章,我们了解到我们需要把下一个需要处理的单元存储在nextUnitOfWork中,在浏览器空闲的时候再继续处理,我想你心里一定有一个问题,怎么知道下一个处理的单元呢?

我们来看看它的数据结构

const newFiber = {
    type: element.type,  // 存储 string->表签名|function
    props: element.props, // 存储属性
    parent: fiber, // 指向父节点 源码中为 return 属性
    sibling: fiber, // 指向兄弟节点
    child: fiber, // 指向孩子节点
    dom: null, // 存储 dom 元素
}

通过上面你一定发现,笔者发现fiberConcurrent Mode模块下,主要有以下作用:1. 存储中间状态;2.指向明确【parent/sibling/child

接下来,通过举例子的方式来让你加深对fiber的理解

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

fibers.png

通过图片,是不是有一种豁然开朗的感觉?把JSX转换为fiber树。我们都知道Concurrent Mode是把我们第一讲中的不可中断的递归变为异步可中断方式去处理每一个节点。那你肯定有一个疑问,如果同时有childsibling怎么处理呢?

二话不说直接上图:

fiber-process.jpg

实现

  1. render函数中,我们将nextUnitOfWork设置Fiber根节点 【容器节点】
function render(element, container) {
    nextUnitOfWork = {
        dom: container,
        props: {
            children: [element],
        },
    }
}
  1. performUnitOfWork中需要实现以下功能:
  • 创建dom节点,并添加到dom树中
  • children创建新的Fiber
  • 选择下一个Fiber节点
// 创建dom节点
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
}
function performUnitOfWork(fiber) {
        // 创建 dom 节点
        if (!fiber.dom) {
                fiber.dom = createDom(fiber)
        }
         //添加到父级节点
        if (fiber.parent) {
                fiber.parent.dom.appendChild(fiber.dom)
        }

        // 为孩子创建新的fiber节点 并添加到fiber树中
        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 {
                        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
        }
}

阶段代码:github.com/sunsmile-ls…

Render 和 Commit 阶段

我们在之前的Concurrent Mode中知道浏览器会打断我们代码的执行,再根据上面的代码,你一定会发现有问题在完成渲染整个树之前,浏览器可能会中断我们的工作。 在这种情况下,用户将看到不完整的UI。

不卖关子,直接上解决方案。React会把构造fiber树和渲染分开 -- Render 和 Commit 阶段

render 阶段只创建了fiber树,在commit 阶段根据已经创建的fiber 树 渲染到页面上面。我想你心里一定在想,怎么分开呢?要遍历两次

  1. 我们需要移除performUnitOfWork函数中的以下代码:
if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
}
  1. 因为要遍历两次,所以我们需要记住根fiber节点
let wipRoot = null
function render(element, container) {
   wipRoot = {
       dom: container,
       props: {
       children: [element],
       },
   }
   nextUnitOfWork = wipRoot
}
  1. 在完成fiber树的创建之后我们执行commit阶段
function workLoop(deadline) {
    ...
    // 表示没有下一个处理单元,并且有根节点
    if (!nextUnitOfWork && wipRoot) {
    commitRoot()
    }
    requestIdleCallback(workLoop)
}
  1. 最后我们来揭示以下commit的真面目吧
function commitRoot() {
      commitWork(wipRoot.child)
      wipRoot = null
}

function commitWork(fiber) {
      if (!fiber) {
        return
      }
      // 添加 dom 到页面中
      const domParent = fiber.parent.dom
      domParent.appendChild(fiber.dom)
      commitWork(fiber.child)
      commitWork(fiber.sibling)
}

至此,我们Render 和 Commit 阶段已经完成。

源码请看 github.com/sunsmile-ls…

未完待续...... 看都看完了,点赞吧,为你的努力点赞,哈哈!防止走失,请关注作者。