《手写mini React》Fiber渲染

93 阅读4分钟

先上代码

import React from 'react';

// 创建虚拟 DOM
function createDom(fiber) {
  const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type);

  // 更新 DOM 属性和事件监听
  updateDom(dom, {}, fiber.props);
  return dom;
}

let nextUnitOfWork = null;
let wipRoot = null; // 保存对根 Fiber 的引用
let currentRoot = null; // 保存当前页面的 Fiber 树
let deletions = null;
let wipFiber = null;
let hookIndex = null;

// 提交更新到 DOM
function commitRoot() {
  deletions.forEach(commitWork);
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

// 判断是否为事件属性
const isEvent = key => key.startsWith("on");
// 判断是否为普通属性
const isProperty = key => key !== "children" && !isEvent(key);
// 判断属性是否更新
const isNew = (prev, next) => key => prev[key] !== next[key];
// 判断属性是否被移除
const isGone = (prev, next) => key => !(key in next);

// 更新 DOM 元素的属性和事件监听
function updateDom(dom, prevProps, nextProps) {
  // 移除旧的或已更改的事件监听
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key =>
        !(key in nextProps) ||
        isNew(prevProps, nextProps)(key)
    )
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2);
      dom.removeEventListener(
        eventType,
        prevProps[name]
      );
    });

  // 移除旧的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = "";
    });

  // 设置新的或更改的属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name];
    });

  // 添加事件监听
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2);
      dom.addEventListener(
        eventType,
        nextProps[name]
      );
    });
}

// 提交工作到 DOM
function commitWork(fiber) {
  if (!fiber) {
    return;
  }
  let domParentFiber = fiber.parent;
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.dom;
  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === "DELETION") {
    // domParent.removeChild(fiber.dom);
    commitDeletion(fiber, domParent);
  }
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

// 提交删除到 DOM
function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

// 工作循环,处理 Fiber
function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

// 协调子元素
function reconcileChildren(wipFiber, elements) {  // 定义一个函数,接收两个参数:当前工作纤维(wipFiber)和元素数组(elements)
    let index = 0;  // 初始化索引变量为0,用于遍历元素数组。
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child;  // 获取当前工作纤维的备用纤维(alternate fiber)以及其子纤维(child fiber)。
    let prevSibling = null;  // 初始化前一个兄弟纤维(sibling fiber)为null。
  
    while (index < elements.length || oldFiber != null) {  // 当索引小于元素数组长度或者备用纤维不为null时,执行循环。
      const element = elements[index];  // 获取当前索引位置的元素。
      let newFiber = null;  // 初始化新的纤维为null。
      const sameType = oldFiber && element && element.type == oldFiber.type;  // 判断元素和备用纤维是否具有相同的类型。
  
      if (sameType) {  // 如果元素和备用纤维具有相同的类型。
        newFiber = {  // 创建一个新的纤维对象。
          type: oldFiber.type,  // 类型与备用纤维相同。
          props: element.props,  // 属性与元素相同。
          dom: oldFiber.dom,  // DOM节点与备用纤维相同。
          parent: wipFiber,  // 父级纤维为当前工作纤维。
          alternate: oldFiber,  // 备用纤维为旧的备用纤维。
          effectTag: "UPDATE"  // 标记为更新。
        };
      }
  
      if (element && !sameType) {  // 如果元素存在并且类型与备用纤维不同。
        newFiber = {  // 创建一个新的纤维对象。
          type: element.type,  // 类型与元素相同。
          props: element.props,  // 属性与元素相同。
          dom: null,  // DOM节点为null。
          parent: wipFiber,  // 父级纤维为当前工作纤维。
          alternate: null,  // 没有备用纤维。
          effectTag: "PLACEMENT"  // 标记为插入。
        };
      }
  
      if (oldFiber && !sameType) {  // 如果存在备用纤维并且类型与元素不同。
        oldFiber.effectTag = "DELETION";  // 将备用纤维的标记设为删除。
        deletions.push(oldFiber);  // 将备用纤维添加到删除列表中。
      }
  
      if (oldFiber) {  // 如果存在备用纤维。
        oldFiber = oldFiber.sibling;  // 获取备用纤维的兄弟纤维,准备处理下一个兄弟纤维。
      }
  
      if (index === 0) {  // 如果索引为0,即处理第一个元素和对应的第一个备用纤维。
        wipFiber.child = newFiber;  // 将新的纤维设为当前工作纤维的子纤维。
      } else if (element) {  // 如果不是第一个元素,即处理后续的元素和对应的备用纤维。
        prevSibling.sibling = newFiber;  // 将新的纤维设为前一个兄弟纤维的兄弟纤维。
      }
  
      prevSibling = newFiber;  // 将新的纤维设为前一个兄弟纤维,准备处理下一个兄弟纤维。
      index++;  // 索引加1,准备处理下一个元素。
    }
  }

// 执行工作单元
function performUnitOfWork(fiber) {
  // 1. 函数组件对应的 Fiber 节点没有真实 DOM 元素
  // 2. 函数组件需要运行函数获取子元素
  const isFunctionComponent = fiber.type instanceof Function;
  if (!isFunctionComponent && !fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  const children = isFunctionComponent ? updateFunctionComponent(fiber) : fiber.props.children;

  // 第二步,为每一个新的 React 元素节点创建对应的 Fiber 节点,并判断旧的 Fiber 节点上的真实 DOM 元素是否可以复用,从而节省创建真实 DOM 元素的开销
  reconcileChildren(fiber, children);

  // 第三步,查找下一个工作单元
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

// 更新函数组件
function updateFunctionComponent(fiber) {
  // 更新工作单元纤维
  wipFiber = fiber;
  // 初始化钩子索引为0
  hookIndex = 0;
  // 清空当前工作单元纤维的钩子数组
  wipFiber.hooks = [];
  // 返回更新后的纤维类型和属性
  return [fiber.type(fiber.props)];
}

// 简化的 React 库
const MiniReact = {
  createElement: (type, props, ...children) => {
    return {
      type,
      props: {
        ...props,
        children: children.map(child => {
          if (typeof child === 'object') {
            return child;
          }
          return {
            type: 'TEXT_ELEMENT',
            props: {
              nodeValue: child,
              children: [],
            }
          };
        })
      }
    };
  },
  render: function(element, container) {
    wipRoot = {
      dom: container,
      props: {
        children: [element], // 这里的 element 是使用 MiniReact.createElement 创建的虚拟 DOM 树
      },
      alternate: currentRoot,
    };
    deletions = [];
    nextUnitOfWork = wipRoot;
  },
  useState: function(initial) {
    const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
    const hook = {
      state: oldHook ? oldHook.state : initial,
      queue: [],
    };
    const actions = oldHook ? oldHook.queue : [];
    actions.forEach(action => {
      hook.state = action(hook.state);
    });
    const setState = action => {
      hook.queue.push(action);
      wipRoot = {
        dom: currentRoot.dom,
        props: currentRoot.props,
        alternate: currentRoot,
      };
      nextUnitOfWork = wipRoot;
      deletions = [];
    };
    wipFiber.hooks.push(hook);
    hookIndex++;
    return [hook.state, setState];
  },
};

// 使用 JSX 语法
/** @jsx MiniReact.createElement */
const container = document.getElementById("root");

// 示例组件 Counter
function Counter() {
  const [state, setState] = MiniReact.useState(1);
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  );
}

const element = <Counter />;
MiniReact.render(element, container);

这是一个简化的 React 实现,它实现了一个虚拟 DOM 和一些 React 的核心功能,包括元素的创建、渲染、组件的状态管理等。以下是对代码的详细解释:

  1. 创建虚拟 DOM(createDom函数) :根据给定的 Fiber 节点类型创建相应的虚拟 DOM。如果类型是文本元素('TEXT_ELEMENT'),则创建一个文本节点;否则,创建一个具有相应类型的 DOM 元素,并调用 updateDom 函数来更新 DOM 属性。
  2. 更新 DOM 元素(updateDom函数) :根据前后属性的变化,更新 DOM 元素的属性和事件监听。首先,移除不再需要的事件监听,然后移除不再需要的属性,接着设置新的或更改的属性,最后添加新的事件监听。
  3. 提交工作到 DOM(commitWork函数) :在完成 Fiber 树的构建后,将生成的 Fiber 树(称为 wipRoot)的变化提交到真实的 DOM。根据 Fiber 节点的 effectTag 属性,执行插入(PLACEMENT)、更新(UPDATE)和删除(DELETION)等操作。
  4. 工作循环(workLoop函数) :通过 requestIdleCallback 触发工作循环,持续执行工作直到时间片用尽。在工作循环中,执行 performUnitOfWork 函数来处理每一个工作单元(Fiber 节点),直至没有待处理的工作单元。
  5. 协调子元素(reconcileChildren函数) :为当前工作纤维(wipFiber)和传入的元素数组协调子元素的变化。对比新旧元素,根据类型、属性等判断是否需要插入、更新或删除 DOM 元素。
  6. 执行工作单元(performUnitOfWork函数) :处理一个工作单元的逻辑。它首先根据 Fiber 节点类型和是否为函数组件,执行相应的更新逻辑,然后继续处理子元素。
  7. 更新函数组件(updateFunctionComponent函数) :用于更新函数组件的状态。它初始化钩子索引、清空钩子数组,并执行函数组件逻辑,获取返回的元素数组,进而协调子元素的变化。
  8. 简化的 React 库(MiniReact对象) :提供了一些简化的 React API,包括创建元素、渲染、状态管理等。它使用 createElement 函数创建虚拟 DOM 元素,并使用 render 函数将虚拟 DOM 渲染到真实的 DOM。
  9. 使用 JSX 语法(jsx注释和组件) :使用 JSX 语法来创建组件并渲染到指定的容器中。在示例中,创建了一个名为 Counter 的组件,使用 MiniReact.useState 来管理状态,以及使用 onClick 事件来更新状态并重新渲染。

这段代码演示了一个简化的 React 实现,可以帮助你更好地理解 React 的工作原理,尤其是虚拟 DOM、协调和状态管理的概念。请注意,实际的 React 实现更加复杂和优化,但这个示例可以作为入门的参考。