手写 Mini-React:构建一个简化版的 React

191 阅读3分钟

开场白

在现代前端开发中,React 已成为一种强大的工具,广泛应用于构建高效、可维护的用户界面。作为一名前端开发者,理解 React 的内部机制不仅能帮助你更好地使用它,还能为你提供对其他前端框架和库的深入理解。在这篇文章中,我们将深入探讨如何手写一个简化版的 React,带你体验构建一个微型 React 框架的全过程。

请大佬指教 抱拳了

React 的基本概念

  • 虚拟 DOM: React 使用虚拟 DOM 来表示 UI 的状态,通过比较新旧虚拟 DOM,找出差异,然后最小化地更新真实 DOM。

  • Fiber 架构: React 16 引入的架构,允许将渲染工作分解为多个单元,逐步执行,避免阻塞主线程,提高渲染效率。

创建虚拟 DOM

首先,我们需要实现一个函数来创建虚拟 DOM。虚拟 DOM 是一个描述 UI 结构的普通 JavaScript 对象,它不会直接操作真实 DOM,而是为后续的差异比较提供基础。

function createElement(type, props, ...children) {
  const { key, ref, children: propsChildren, ...restProps } = props || {};
  if (Array.isArray(propsChildren) && children.length > 0) {
    console.error("children和props.children同时存在优先使用children");
  }
  if (
    children.length === 0 &&
    Array.isArray(propsChildren) &&
    propsChildren.length > 0
  ) {
    children = propsChildren;
  }
  return {
    type,
    key: key || null,
    ref: ref || null,
    props: {
      ...restProps,
      children: children.map((child) => {
        if (typeof child === "object") {
          return child;
        } else {
          return createTextElement(child);
        }
      }),
    },
    $$typoef: Symbol.for("react.element"),
  };
}
function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: [],
    },
    $$typoef: Symbol.for("react.element"),
  };
}
  • createElement: 模仿 React.createElement,用于创建一个包含 type(标签类型)、props(属性)和 children(子元素)的对象。

  • createTextElement: 用于处理文本节点,返回一个特定类型的虚拟 DOM 对象。

到此,我们已经有了创建虚拟 DOM 的基础工具。

构建 Fiber 架构

Fiber 会在内存中维护两颗树

  • current 用户渲染
  • workInProgress 用户diff比对

每次更新时 都会在内存中 diff 比对workInProgress新的VDOM,进而构建workInProgress 完全修改完成workInProgress后 会将其值赋给current 调用渲染器渲染

let nextUnitOfWork = null;
let currentRoot = null; 
let wipRoot = null;
let deletions = null;
  • nextUnitOfWork当前处理的工作单元
  • currentRoot 相当于 Fiber的 currentTree 用于渲染
  • wipRoot 相当于Fiber的另一颗树 workInprogress 用于diff比对
  • deletions 用于存储需要从 DOM 中删除的 Fiber 节点
const render = (container, element) => {
  // 构建工作树
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  };
  deletions = [];
  nextUnitOfWork = wipRoot; // 将当前工作单元设置为根节点
};
  • dom 真实dom节点对象
  • props 属性值
  • props.children 子项列表
  • child 第一个子项
  • sibling 下一个同级节点
  • return 父级节点
  • alternate 上一次的老节点
  • key
  • ref
function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  if (!nextUnitOfWork && wipRoot) {
    // 没有需要更新的单元 且 有 workInProgress tree 则提交更新 替换树
    commitRoot();
  }

  requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);

  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.return;
  }
  return null;
}

function reconcileChildren(parentFiber, elements) {
  let index = 0;
  let oldFiber = parentFiber.alternate && parentFiber.alternate.child;
  let prevSibling = null;

  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    let newFiber = null;

    // 比对类型 类型相同
    const sameType = oldFiber && element && element.type === oldFiber.type;

    // 类型相同时 更新
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props, // 只换属性
        key: oldFiber.key,
        dom: oldFiber.dom,
        return: parentFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",
        $$typeof: Symbol.for("react.element"),
      };
    }
    // 类型不同且 有新节点时候 新增
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        key: element.key || null,
        dom: null,
        return: parentFiber,
        alternate: null,
        effectTag: "PLACEMENT",
        $$typeof: Symbol.for("react.element"),
      };
    }
    // 类型不同 且 没有新节点 有旧节点时 删除
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }
   
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    if (index === 0) {
      parentFiber.child = newFiber;
    } else if (element) {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
}

提交替换两颗树

function commitRoot() {
  deletions.forEach(commitWork);
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

function commitWork(fiber) {
  if (!fiber) {
    return;
  }

  let domParentFiber = fiber.return;
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.return;
  }
  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") {
    commitDeletion(fiber, domParent);
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

构建入口

// jsx最后会被babel编译成 React.createElement或jsx形式 该函数返回VDom
const element = createElement(
  "div",
  {
    class: "app",
    onClick: () => console.log("clicked"),
  },
  createElement(
    "h1",
    {
      onClick: (e) => {
        e.stopPropagation();
        console.log("clicked h1");
      },
    },
    "Hello"
  ),
  createElement("p", null, "React")
);


const container = document.getElementById("root");

render(container, element);

完整代码

结尾

通过手写这个简化版的 React,我们了解了 React 的基本原理和核心机制。希望这次实践能帮助你更好地理解 React,并在实际开发中更高效地使用它。今后,无论遇到什么样的前端挑战,都能以扎实的基础和清晰的思路去应对。继续探索,享受编程的乐趣吧!