手写react四

141 阅读3分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

实现

我们将分为8步,一步一步的实现一个小型的React

  • 实现createElement函数
  • 实现render函数
  • Currnet Mode模式
  • fibers
  • render和commit阶段
  • 协调器
  • function组件
  • hooks

fibers

将整个渲染任务分成一个一个小任务,我们需要一个种数据结构:fiber树,在react中同时只会存在两棵fiber树,一个是当前展示在屏幕的current tree,另一个是正在构建的workInProcess tree。当接收到更新任务时,就会根据传入的JSX构建fiber树,每个dom节点对应一个fiber,也就对应着一个小任务。

当在内存中构建workInProcess tree,构建完成时,将current tree等于workInProcess tree,就完成了一次页面更新。

假设我们要渲染的JSX如下:

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

在开始渲染的时候,首先会创建root fiber并把它赋值给nextUnitOfWork,当存在nextUnitOfWork就会执行performUnitOfWork方法,该方法会做三件事:

  1. 如果dom不存在就创建dom节点
  2. 对于每个子节点创建fiber节点
  3. 返回下一个任务

对于每个fiber节点,都会child指针,指向子节点;如果有兄弟节点,还会指sibling指针,指向兄弟节点。每个fiber节点还会有return指针,指向父节点。如下图:

1244.png

有了这个数据结构,我们就可以很容易找到下一个任务,当我们完成创建一个fiber任务,如果它存在child,那么child就是下一个任务。例如上面,当我们完成div fiber任务,下一个任务就是h1 fiber。

如果不存在child,那么就将sibling当做下一个任务。例如p fiber没有child,我们将a 做为下一个任务

如果即没有child也没有sibling,我们将返回到parent中寻找parent的sibling,直到返回到root fiber,此时就说明我们已经完成了构建fiber tree的工作。

下面我们来实现一个代码吧 我们把create dom单独抽成方法,后面还会用到,在render函数里,我们将root赋值给nextUnitOfWork,当浏览器有空闲时间,就会执行workLoop方法,然后从root开始第一个任务。

function createDom(fiber) {
    const dom =
    fiber.type === 'TEXT_ELEMENT'
            ? document.createTextNode('')
            : document.createElement(fiber.type);
    Object.keys(fiber.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = fiber.props[name]
        })

    return dom;
}
function render(element, container) {
    nextUnitOfWork = {
        dom: continer,
        props: {
            children: [element]
        },
    }
}
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) { // 存在下一个任务并且存在空闲时间
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

下面我们实现一下performUnitOfWork方法 第一步:如果fiber中不存在dom节点,就创建dom节点并将节点放入parent的dom节点中

function performUnitOfWork(fiber) {
    // 创建dom
    if (!fiber.dom) {
        fiber.dom = createDom(fiber);
    }
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom);
    }
    // TODO 为每个child创建fiber节点
    
    // TODO 返回下一个任务
    
}

第二步:为每个child创建fiber节点

function performUnitOfWork(fiber) {
    // 创建dom
    if (!fiber.dom) {
        fiber.dom = createDom(fiber);
    }
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom);
    }
    // 为每个child创建fiber节点
    const elements = fiber.props.children;
    let index = 0;
    let prevSibling = null;
    while (index < elements.length) {
        const element = elements[index];
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }
        
         if (index === 0) { // 如果是首个child,将赋值为child,后面的将赋值为sibling
            fiber.child = newFiber
        } else {
            prevSibling.sibling = newFiber
        }
        prevSibling = newFiber
        index++
    }
   
    // TODO 返回下一个任务
    
}

第三步:查找下一个任务,首先尝试fiber.child,然后是sibling,都没有就返回parent继续寻找

function performUnitOfWork(fiber) {
    // 创建dom
    if (!fiber.dom) {
        fiber.dom = createDom(fiber);
    }
    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom);
    }
    // 为每个child创建fiber节点
    const elements = fiber.props.children;
    let index = 0;
    let prevSibling = null;
    while (index < elements.length) {
        const element = elements[index];
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }
        
         if (index === 0) { // 如果是首个child,将赋值为child,后面的将赋值为sibling
            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;
    }
}

总结

  1. 要实现任务划分,依赖于fiber数据结构
  2. 开始渲染时会将root赋值给nextUnitOfWork,然后当浏览器有空闲时间时,开始执行performUnitOfWork,开始构建fiber tree
  3. performUnitOfWork做了三件事情:
    • 创建dom节点,并将节点append 到parent节点中;
    • 为每个child创建fiber节点;
    • 寻找下一个任务并返回