简单实现 React 的渲染流程(2)

前言

众所周知,React是一个数据驱动视图的框架,在视图渲染更新时,就会涉及到 虚拟DOM、Fiber、Diff算法 之类的内容,这些内容的原理作为前端面试八股文已经屡见不鲜了,但我还从未想过如何从代码层面实现。

最近遇到了一个很有趣的网站:Build your own React,这个网站用简洁的代码讲述、实现了React是如何让视图进行渲染更新的。

我将以我的理解大致翻译该网站的内容,并进行代码层面的实现。

这篇文章,我们接着上次的内容继续,实现对函数式组件的渲染

上一篇文章指路:简单实现 React 的渲染流程(1)

七、函数式组件

函数式组件有两个明显的不同点

1、函数式组件对应的Fiber节点没有DOM节点属性

2、Fiber节点的children属性不通过props直接获取,而是通过运行函数获取

那么我们就需要根据这两点对代码进行修改。

注意:没有DOM节点属性仅是指函数式组件对应的Fiber节点,其子级节点仍有DOM节点属性。也就是说仅type为Function的Fiber节点不存在DOM节点属性,它是用来运行获取子级节点们的。

首先,判断当前的Fiber节点是否为函数式组件,对performUnitOfWork函数进行修改:

js 复制代码
......
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
  updateFunctionComponent(fiber);
} else {
  updateHostComponent(fiber);
}
......

原先的逻辑我们原封不动地封装到updateHostComponent函数中:

js 复制代码
function updateHostComponent(fiber) {
  // 将元素添加到真实DOM结构中
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  // 创建当前元素的子级Fiber节点们,构建Fiber Tree
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
}

对于函数式组件我们进行特殊处理:

js 复制代码
function updateFunctionComponent(fiber) {
  const elements = [fiber.type(fiber.props)];
  reconcileChildren(fiber, elements);
}

其实就是上面说的第1点,通过运行函数获取子级元素。获取到子级元素后的逻辑与之前无异。

根据函数式组件Fiber节点没有DOM节点属性的特性,在commitWork函数中,添加和删除操作依赖于DOM节点 ,因此我们要对其进行处理,对于没有DOM节点属性的Fiber节点作跳过处理,继续往上/下寻找存在DOM节点属性的父/子级节点

js 复制代码
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 === "DELETION") {
    commitDeletion(fiber, domParent);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom !== null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

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

删除操作中寻找子节点 的逻辑我们将其封装到commitDeletion函数中:

js 复制代码
function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

至此,我们成功地实现了函数式组件的渲染流程。

八、Hooks

函数式组件的一大特点就是hooks,取代了类式组件中的state和生命周期函数。其中最常用 的hook有useStateuseEffect,这一章节我们将实现useState的底层逻辑

首先,我们初始化一些全局变量 ,将hooks相关的属性挂载在函数式组件对应的Fiber节点上

js 复制代码
let wipFiber = null;
let hookIndex = null;
function updateFunctionComponent(fiber) {
  // 初始化hooks相关全局变量,将其挂载在函数式组件Fiber节点上
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = [];

  // 创建当前元素的子级Fiber节点们,构建Fiber Tree
  // 函数式组件的子级节点通过运行函数获取
  const elements = [fiber.type(fiber.props)];
  reconcileChildren(fiber, elements);
}

一个函数式组件中可能有多个state ,涉及到多个useState的初始化,所以我们通过一个全局的数组 来保存各个state的值 ,并且追踪各个state的索引值,以此来获取对应的值。

因为函数式组件每一次渲染都会重新执行 ,但state的值不应该每次都初始化,为了获取最新的值,我们将每一次渲染时的state值挂载在函数式组件对应的Fiber节点上。在useState函数中,我们先判断上一次渲染的Fiber节点上是否存在hooks属性,如果有的话就将数据继承下来,否则进行初始化。

js 复制代码
function useState(initial) {
  const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
  const hook = { state: oldHook ? oldHook.state : initial };

  wipFiber.hooks.push(hook);
  hookIndex++;

  return [hook.state];
}

useState还应该返回一个setState函数用于修改state 。另外,在state更新时 会触发组件的重新渲染 ,所以在setState中我们还要给wipRoot赋值,触发渲染流程。

js 复制代码
function useState(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.map(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];
}

setState接收一个参数action,由于一次渲染可能会执行多个action ,所以我们用一个数组 来保存actions,并在下一次渲染时依次进行调用,返回更新后的state值。

这样我们就实现了useState,我们来调用试试看:

js 复制代码
function App(props) {
  const [count, setCount] = Teact.useState(0);
  const [showName, setShowName] = Teact.useState(true);

  return (
    <div id="1" onClick={() => {
      setCount(prevState => prevState + 1);
      setShowName(prevState => !prevState);
    }}>
      <h1>Hi {showName ? props.name : ""} {count}</h1>
    </div>
  );
}

const element = <App name="Toby" />;
const container = document.getElementById("root");
Teact.render(element, container);

设置了两个state,可以正常运行!

至此,我们实现了对函数式组件中useState这个hook的实现。

九、查漏补缺

问题1:函数式组件重新渲染时不会清空上一次的渲染结果

由于函数式组件对应的Fiber节点没有dom节点属性的特殊性,在添加真实DOM节点时,会向上寻找其父级节点 ,这就导致了本该被删除的函数式组件节点的子级元素们被添加到向上一级的父级节点上,所以出现了这个问题(正常情况是子级元素被添加到已被删除的节点中,所以没有影响)。

所以我们对effectTagDELETION的函数式组件节点进行特殊处理,不进行后续真实DOM的构造:

js 复制代码
if (fiber.effectTag === "DELETION" && fiber.type instanceof Function) return;

问题2:新增的节点没有添加监听事件

新增的节点没有添加监听事件,仅更新的节点添加了监听事件。

js 复制代码
function updateListener(dom, props) {
  // 添加监听事件
  Object.keys(props).filter(isEvent).map(name => {
    const eventType = name.toLowerCase().substring(2);
    dom.addEventListener(eventType, props[name]);
  });
}

简单补一下。

小结

在这篇文章中,我们实现了函数式组件的渲染流程 ,并对函数式组件中的hooks 有了一定的了解,实现了useState这个hook。

Git项目地址:Build-Your-Own-React

原文链接:Build your own React

相关推荐
Eamonno10 小时前
深入理解React性能优化:掌握useCallback与useMemo的黄金法则
react.js·性能优化
goldenocean12 小时前
React之旅-02 创建项目
前端·react.js·前端框架
一路向前的月光12 小时前
React(8)
前端·react.js·前端框架
林啾啾12 小时前
常用的 React Hooks 的介绍和示例
前端·javascript·react.js
goldenocean14 小时前
React之旅-01 初识
前端·javascript·react.js
power-辰南14 小时前
AI Agent架构深度解析:从ReAct到AutoGPT,自主智能体的技术演进与工程实践
人工智能·react.js·架构·ai agent
开发者每周简报19 小时前
React:UI开发的革新者
javascript·react native·react.js
engchina1 天前
使用 Vite + React 19 集成 Tailwind CSS 与 shadcn/ui 组件库完整指南
css·react.js·ui·vite·tailwind·react 19·shadcn
祈澈菇凉2 天前
React 的 context 是什么?
前端·javascript·react.js
Au_ust2 天前
千峰React:脚手架准备+JSX基础
前端·javascript·react.js