实现自己的mini-react

209 阅读4分钟

之前学习react就是简单的看了看文档,就学习了几个hook的使用。然后就是上手项目,面试中经常会问到react的一些设计原理(虽然没几个面试),手写一个mini-react有利于我们去理解,写下这篇文章以此记录。

开始

首先,需要了解React渲染大概分为三个阶段:

  • beginWork,开始处理,将JSX转化成React Element
  • reconcile,调和阶段,将React Element转化成Fiber结构,形成Fiber链表
  • commit,提交,将Fiber转化成真实DOM,呈现在页面上,并且处理一些useEffect传入的回调

首先是创建React Element,我感觉应该就是常说的Vnode。JSX经过bable编译成以下形式:

image.png

function createElement(type, props, ...children) {
    return {
      type,
      props: {
        ...props,
        children: children
          .map((child) => {
            const isTextNode =
              typeof child === "string" || typeof child === "number";
            return isTextNode ? createTextNode(child) : child;
          })
          .flat(Infinity), // 这里是因为map传入的数组会导致形成一个二维数组,所以粗暴的使用flat展开了。。
      },
    };
  }

其中的MiniReact是我们自定义的JSX工厂函数,可以配置tsconfig文件里的jsxFactory属性,默认是React.createElement

image.png

从render函数出发,传入我们的入口组件,一般是App,以及需要挂载的容器。一般项目里都有个index.html的文件,里面有一个id为app或者root的div。

image.png

看下render的实现,wipRoot就是当前处理的树,React采用了双缓存树的结构,wipRoot就是根节点,也就是当前处理的树的根节点,与之对应的是currentRoot,表示当前在页面展示的树的根节点。两棵树,使用alternate进行关联,下面的代码中就表示了wipRoot使用alternate指向了currentRoot。

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

然后开启我们的渲染工作work loop,由于页面的内容是由 JS 动态生成的,JS的执行和浏览器渲染进程是互斥的,所以react使用分片,将JS的执行分为一个个任务,在浏览器每一帧渲染后的空余时间来执行这些任务,这样既可以提升性能,也不会因为JS执行时间过长导致的阻塞,而且依据fiber的设计实现了可中断渲染的特性,随时可以从中断的的地方继续,因为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);

这里使用了requestIdleCallback来实现上述讲到的利用空闲时间来执行JS。

在performUnitOfWork中,我们主要是针对fiber的类型做不同的处理,是原生元素,还是函数组件或者类组件这样,然后处理完之后就返回child,没有就返回sibling,否则就是向上找父节点,这里return代表父节点

/** 处理fiber,工作单元*/
  function performUnitOfWork(fiber) {
    const isFunctionComponent = fiber.type instanceof Function;
    // 根据type来处理不同的fiber节点
    if (isFunctionComponent) {
      updateFunctionComponent(fiber);
    } else {
      updateHostComponent(fiber);
    }
    // 处理完成后,先处理child
    if (fiber.child) {
      return fiber.child;
    }
    let nextFiber = fiber;
    // 如果child节点不存在,那么优先处理sibling节点,也就是兄弟节点
    // 兄弟节点也没有就回到return代表父节点,向上找父节点处理sibling节点
    // 按照child,sibling,return(parent)顺序遍历处理,串成链表结构
    while (nextFiber) {
      if (nextFiber.sibling) {
        return nextFiber.sibling;
      }
      nextFiber = nextFiber.return;
    }
  }

如果当前fiber节点的类型是函数组件的话,就会传入props调用函数形成拿到Vnode,然后进入调和将其转化成fiber

const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, fiber.props.children);

原生元素就会创建按DOM,然后计入调和处理children

 if (!fiber.dom) {
      fiber.dom = createDom(fiber); // 判断是否是文本节点,创建DOM,然后进行一个更新操作,重新绑定事件以及属性
    }
    // 进入调和,形成fiber
 reconcileChildren(fiber, fiber.props.children);

在调和阶段,对两个树的新旧元素进行比较,然后给fiber节点打上tag在后续进行增删改的操作

当调和完成后,就是我们的commit阶段,开始处理我们的Fiber树,将其渲染到网页上,也就是渲染真实DOM了,在这个阶段就是处理fiber,根据tag处理fiber节点,然后执行我们传入的副作用函数,也就是传入的useEffect回调以及清理函数等。

总结

React利用时间分片,在每帧空余的时间执行渲染任务,先将jsx编译成ReactElement,然后进入调和阶段将其转化成fiber链表,然后在commit阶段更新到页面上,处理副作用函数等。