【react框架】如何手写一个超级mini的React,学完后对框架的理解也会更进一步

本文逐步介绍了如何从头开始构建一个迷你版React,包括创建虚拟DOM、createElement、render函数,以及引入Fiber优化、按需渲染和diff算法,帮助读者深入理解React的核心原理。
摘要由CSDN通过智能技术生成

前言

本篇文章只是对https://qcsite.gatsbyjs.io/build-your-own-react/网站内容,做的一个内容的压缩,结合上自己的理解与想法,告诉已经理解虚拟dom的人怎么去一步一步的写一个超级mini的React。

看完后,相信你对框架的理解也会更进一步。

有错误欢迎评论指出!以下“网站”一词指代上面的网站(绕口令哈哈)


起步工作

先自己手起一个初始的react项目,暂时不用管是什么版本的(我们起是18,网站里说的是16),不太重要。

npx create-react-app my-app

先看虚拟dom长啥样

在src下的index.js中打印:

const element = React.createElement(
  "div",
  { id: "box" },
  "盒子",
  React.createElement("a", { id: "link" }, "点击")
);

console.log("虚拟dom", element);

在这里插入图片描述

展开子项:

在这里插入图片描述

其实虚拟dom长啥样想必大家都已经知道了,我们只需要模拟出那个几个关键的属性即可。


写个createElement

可以看到通过React.createElement可以生成虚拟dom,那我们就模拟着写个简单的例子。

首先创建文件src/miniReact/createElement.js,然后写入:

function createElement(type, props, ...children) {
  return {
    type, // 节点类型
    props: {
      ...props, // 节点所有属性
      children: children.map((child) => // 这里你会疑问如果子级还有子级咋办?放心只是不在这里处理
        typeof child === "object" ? child : createTextElement(child)
      ),
    },
  };
}

// 专门用来创建普通标签元素的
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT", // 自己定个特殊字符标识
    props: {
      nodeValue: text, // 文本内容
      children: [],
    },
  };
}

export default createElement;

可以拿上面React.createElement输入案例来看,把入参都放到我们写的函数里,是不是也是一样的。


写个render

在src下的index.js中,我们可以这样渲染出真实的dom在页面上:

const element = React.createElement(
  "div",
  { id: "box" },
  "盒子",
  React.createElement("a", { id: "link" }, "点击")
);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(element);

可以看出是通过了render函数去做的真实dom转换,不过使用方式和网站上的16版本不太一样:

const container = document.getElementById("root");
ReactDOM.render(element, container);

咱们这里为了贴合网站的案例,也按照网站上的写一个render的简单实现。

创建文件src/miniReact/render.js,然后写入:

function render(element, container) {
  // 创建对应的真实dom对象
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);

  // 判断是否为非children属性
  const isProperty = (key) => key !== "children";
  // 把虚拟dom上的children属性赋值在真实dom对象上
  Object.keys(element.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = element.props[name];
    });

  // 这里对子级进行递归处理
  element.props.children.forEach((child) => render(child, dom));

  // 每一层级的dom节点挂载
  container.appendChild(dom);
}

export default render;

第一次试验

可以进行我们的第一个初步的试验了,看能不能替代原来的写法,首先创建src/miniReact/index.js,然后写入:

// 合并导出
import createElement from "./createElement";
import render from "./render";

export default { createElement, render };

这个文件就相当于ReactDOM。

好啦,我们在src下的index.js把我们写的miniReact替换掉原来的ReactDOM:

import miniReact from "./miniReact";

const element = miniReact.createElement(
  "div",
  { id: "box" },
  "盒子",
  miniReact.createElement("a", { id: "link" }, "点击")
);
const container = document.getElementById("root");
miniReact.render(element, container);

看看你的页面是不是正常渲染了!


了解Fiber

这个是为接下来的代码理解做的理论知识铺垫,一定要懂了!!!才好理解下面的代码!!!

这玩意网上讲的太复杂了,可以看我这篇大白话给你讲明白:【react框架】别把Fiber整得那么难理解,来参考下我是咋理解的,用大白话解释


写个Fiber化函数

知道fiber是个啥东东后,咱们可以写个fiber化的函数了,在src/miniReact/render.js里面加入:

// 创建真实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;
}

// 节点fiber化:把每一个虚拟dom阶段fiber化,最后还要返回下一个节点,因为下一个节点在本次处理中是没有完全fiber化的
function performUnitOfWork(fiber) {
  // 1 先给自己创建真实dom
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  // 2 接下来就是理清指向:父级、子级、兄弟

  // 如果有父节点,那么要把这个fiber对象插入父节点下,要不怎么知道每个fiber之间的关系,而且这种形式可以向下慢慢发展成真实的dom树
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }

  const elements = fiber.props.children; // 拿到所有子级们
  let index = 0;
  let prevSibling = null; // 上一个兄弟节点

  // 开始循环子级们
  while (index < elements.length) {
    const element = elements[index]; // 拿到每个子级

    const newFiber = { // 创建子级的fiber对象
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    };

    if (index === 0) { // 第一个子级的处理,正常写入到child属性中
      fiber.child = newFiber;
    } else { // 其他的子级就用每个fiber子级的sibling属性记录上一个兄弟节点的指向
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber; // 上一个兄弟节点不断移动至下一位
    index++;
  }

  // 3 这个函数要返回一个fiber出来,作为下一个单元小任务
  // 因为本节点已经完全fiber化了,所以可以看看有没有子节点,有的话继续fiber化
  if (fiber.child) {
    return fiber.child;
  }
  // 没有子节点了,就看看兄弟节点,不断的去找兄弟节点
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent; // 如果兄弟节点也没有了,返回到上一个节点
  }
}

第一次看可能有点懵逼,沉住气,慢慢看,你会有收获的。


改造render

知道要有fiber化的过程后,就是要改造render函数了,在src/miniReact/render.js里面改造。

首先我们要驱动fiber化函数,让它自动去遍历我们的虚拟dom,把每个阶段都fiber化:

let nextUnitOfWork = null; // 记录下一个单元任务,单元任务就是performUnitOfWork函数做的事情

// 单元任务轮番触发机制
function workLoop(deadline) { // 默认requestIdleCallback执行的时候会有参数传入
  let shouldYield = false; // 主线程是否空闲
  // 如果有下一个单元任务要执行并且主线程空闲
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 执行当前单元任务并且拿到下一个单元任务
    shouldYield = deadline.timeRemaining() < 1; // 这个方式可以知道主线程是否空闲
  }
  requestIdleCallback(workLoop); // 当主线程空闲时触发,这个api不懂的话去MDN查下,这里就不赘述了
}

requestIdleCallback(workLoop); // 第一次启动

这里方便实现就用requestIdleCallback这个api了。

ok,这里就差不多了,我们接下来就只需要把第一个节点任务塞入到nextUnitOfWork变量里即可(这样workLoop就能正常工作了),这个事情就交给render:

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  };
}

做完这一切,在页面上看是否一切正常,正常就表示改造成功!


按需渲染换成一次性渲染

按照我们之前的方式,最终呈现的结果是按需渲染,也就是先fiber化后的节点会先渲染出来。

问题来了,如果主线程非空闲时间过长,会有一个问题,页面会出现渲染一半的内容,等主线程空闲了才继续渲染完成。

要是大组件这样子处理还有按需加载那味,但是我们这个fiber是颗粒度很小的,显然这样是不行,我们还是要换成一次性完整的将视图渲染上去。

继续改造src/miniReact/render.js

首先在performUnitOfWork函数里,咱们不需要向下不断发展真实dom树了,所以把相关代码剔除:

function performUnitOfWork(fiber) {
  // 1 先给自己创建真实dom
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  // 2 接下来就是理清指向:父级、子级、兄弟

  // 如果有父节点,那么要把这个fiber对象插入父节点下,要不怎么知道每个fiber之间的关系,而且这种形式可以向下慢慢发展成真实的dom树
  //   if (fiber.parent) {
  //     fiber.parent.dom.appendChild(fiber.dom);
  //   }

// ...

没有父级指向了咋办,这里先不管

然后我们要能够知道什么时候虚拟树完全fiber化,并且能够拿到fiber节点树的根节点,就可以递归去把这颗树一次性渲染成真实的页面了。

怎么去拿到fiber节点树的根节点?可以创建一个变量,通过对象引用类型的特性去追踪当前fiber的节点对象:

let wipRoot = null; // 记录fiber化进度,也就是当前fiber节点

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  };
  nextUnitOfWork = wipRoot;
}

然后我们要写个递归渲染真实dom 的函数,一般叫commit

// 从根节点开始渲染dom
function commitRoot() {
  commitWork(wipRoot.child);
  wipRoot = null;
}

// 递归渲染真实dom
function commitWork(fiber) {
  if (!fiber) {
    return;
  }
  const domParent = fiber.parent.dom;
  domParent.appendChild(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

也就是说我们知道知道在完成fiber化后,调用commitRoot即可从根节点开始渲染dom,所以可以在workLoop里添加上:

// 单元任务轮番触发机制
function workLoop(deadline) {
  // 默认requestIdleCallback执行的时候会有参数传入
  let shouldYield = false; // 主线程是否空闲
  // 如果有下一个单元任务要执行并且主线程空闲
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 执行当前单元任务并且拿到下一个单元任务
    shouldYield = deadline.timeRemaining() < 1; // 这个方式可以知道主线程是否空闲
  }

  // 如果没有单元任务了,开始一次性渲染真实dom ++++++++++++++=
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }

  requestIdleCallback(workLoop); // 当主线程空闲时触发,这个api不懂的话去MDN查下,这里就不赘述了
}

做完后再去跑跑代码,看看页面是否正常显示!


加入diff算法

做完了以上事情,我们不难发现,每次更新页面的时候,都要重新向下遍历一个新的真实dom树,不断的创建,添加dom,对性能很不友好。所以要加入一个简单的diff算法,只更改变换的地方。

不知道diff算法是啥的,建议先去了解下哈哈

未完待续…

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值