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

前言

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

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

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

〇、基础回顾

本章节讲述了如何使用原生的JS代码替代React的语法及API,如果你对React的虚拟DOM对象及原生的DOM节点十分了解,那么你可以跳过这一章节。

举个例子,下方的代码可以实现一个最简单的React项目,我们试着用原生的JS代码替代React特殊的语法及API。

js 复制代码
    const element = <h1 className='label'>Hello React</h1>;
    const container = document.getElementById('root');
    ReactDOM.render(element, container);

首先,第一行代码等价于:

js 复制代码
    const element = React.createElement("h1", { title: "foo" }, "Hello");

这还是依赖于React的API,但我们可以依此将其构造成一个对象:

js 复制代码
    const element = {
        type: "h1",
        props: { title: "foo", children: "Hello" } 
    };

对象有两个属性,type是元素的类型,props是元素的属性,这其中包括子元素。这里的子元素是字符串,但在更多时候子元素会是其它的元素------想象一下DOM树的结构。

有了这个对象,我们就可以通过原生的JS来替代上述代码:

js 复制代码
    const element = {
        type: "h1",
        props: { title: "foo", children: "Hello" } 
    };
    
    const node = document.createElement(element.type);
    node["title"] = element.props.title;
    
    const childNode = document.createTextNode("");
    childNode["nodeValue"] = element.props.children;
    
    const container = document.getElementById('root');
    
    node.appendChild(childNode);
    container.appendChild(node);

在子元素的实现上我们选择用createTextNode来创建而不是用innerText属性,是为了统一创建元素的方式,后续能够更方便地将其封装成一个方法。

至此,我们实现了用原生的JS代码完全替代React。

一、实现createElement函数

从这一章节开始,我们将慢慢实现自己的React。第一步,从实现createElement函数开始。

首先,我们重新创建一个React项目,这次我们来制造一些简单的父子级关系

js 复制代码
    const container = document.getElementById("root");
    const element = (
      <div id="box">
        <a>Hello React</a>
        <b />
      </div>
    );
    ReactDOM.render(element, container);

同样的,我们先用React的createElement来重写这一部分代码,看看它做了些什么事情:

js 复制代码
    const container = document.getElementById("root");
    const element = React.createElement(
      "div",
      { id: "box" },
      React.createElement("a", null, "Hello React"),
      React.createElement("b")
    )
    ReactDOM.render(element, container);

我们可以看到,创建一个元素所需要的只是type, props和children,而children也只需要嵌套调用创建的函数即可。

在先前的步骤中,我们已经将元素抽象为了一个含有type和props属性的对象。那么,我们的createElement函数只需要创建这样的一个对象就行了。

我们可以据此写出一个雏形:

js 复制代码
    function createElement(type, props, ...children) {
      return {
        type,
        props: { ...props, children }
      }
    }

在函数的形参部分,我们用剩余参数语法 来传递children,这样就能确保返回的props中的children为一个数组

children数组也有可能包含着像string, number这样的基本类型数据 ,因此我们要对不同类型的child进行不同的处理

js 复制代码
    function createElement(type, props, ...children) {
      return {
        type,
        props: {
          ...props,
          children: children.map(child => 
            typeof child === "object" ? child : createTextElement(child)
          )
        }
      }
    }

    function createTextElement(value) {
      return {
        type: "TEXT_ELEMENT",
        props: { nodeValue: value, children: [] }
      }
    }

我们将基本类型数据 处理为一个type为TEXT_ELEMENT的对象,它的值在props中的nodeValue属性中,并且children属性为一个空数组。

注意:真正的React并不是这样处理的,我们这样处理是为了简化我们的代码。对于我们要实现的东西来说,简洁的代码比优异的性能更为重要。

至此,我们实现了一个自己的createElement函数。这个函数所创建的并不是真实的DOM节点,而是一个庞大的、树形结构的对象 ,可以把它想象成一个虚拟的DOM

我们的目的是替代React,我们可以给我们的库取个名字,比如说:Teact,并将createElement方法放在Teact对象中,让它看起来更像一个库。

还有一个问题需要解决:如何让babel在编译时使用Teact.createElement而不是React.createElement

我们可以加一行注解 来解决这个问题:/** @jsx Teact.createElement */

如果你的React版本在17以上,那么你可能会遇到跟我一样的另一个问题:

意为不能在runtime自动模式时使用这个注解。

我们只需要将runtime设置为传统模式 即可解决:/** @jsxRuntime classic */

具体可以查阅这篇文章:介绍全新的 JSX 转换

这样一来,babel在编译代码时,就会使用我们定义的createElement来创建元素了。

意料之外的问题

除此之外,我还遇到了另一个问题:

我创建出来的元素并不被ReactDOM.render认可,无法将它渲染出来。我检查了代码并对比了React.createElement创建出来的元素的结构,并没有发现什么问题。

我尝试将React的版本降到16.8.6 (与沙盒 中的版本保持一致),但这个问题仍然存在。由于沙盒中的代码并没有这个问题,并且后续我们自行实现render方法后也就不会遇到这个问题了,所以我暂时不打算深究它。

二、实现render函数

这一章节,我们将来实现自己的render函数,以取代ReactDOM.render函数。

首先,我们来实现添加节点的功能,更新和删除的功能之后再进行完善。

参考ReactDOM.render函数,接收两个参数,一个是之前通过createElement创建的虚拟DOM对象 ,一个是承载DOM结构的容器

有了思路那么很容易就能写出一个雏形:

js 复制代码
    function render(element, container) {
      const dom = element.type === "TEXT_ELEMENT" ? 
        document.createTextNode(element.props.nodeValue) : 
        document.createElement(element.type);

      element.props.children.map(child => render(child, dom));

      container.appendChild(dom);
    }

其实就是一个循环递归 的过程,根据type创建真实的DOM节点,再添加到父级容器中。同时,我们还要对TEXT_ELEMENT这个特殊的类型进行处理。

调用Teact.render取代React.render,我们成功地将内容显示在了页面上。

我们要做的最后一件事情,就是将元素的props属性添加到节点上去:

js 复制代码
function render(element, container) {
  const dom = element.type === "TEXT_ELEMENT" ? 
    document.createTextNode(element.props.nodeValue) : 
    document.createElement(element.type);

  const isProperty = key => key !== "children";
  Object.keys(element.props).filter(isProperty).map(name => {
    dom[name] = element.props[name];
  });

  element.props.children.map(child => render(child, dom));

  container.appendChild(dom);
}

至此,我们就实现了一个能够将JSX转换成DOM的库了。

三、并行模式

在上一章节,我们实现了render方法,但存在一个问题:一旦我们开始渲染DOM树,就只有到渲染完成时才会停止。如果DOM树很复杂,那么渲染的过程有可能会阻塞主线程 (JS是一个单线程的语言)。这也会影响浏览器执行一些优先级更高的操作,例如用户的输入、保持帧动画的流畅。

所以接下来,我们要将渲染工作分割成更小的片段 ,每当一个片段完成时,我们将给到浏览器去中断渲染并执行优先级更高的操作的机会。

为了实现上述调度工作 的特性,我们选择使用requestIdleCallback这个API,它类似于setTimeout,不同的是它执行的时机是在浏览器某一帧存在空闲时,也即是我们可以在主线程空闲时去执行渲染任务。

js 复制代码
function workLoop(deadLine) {
  ......
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

大致上就是这样一个循环调用workLoop的结构。另外,requestIdleCallback的回调函数提供一个参数deadLine,我们可以通过它得知当前帧还有多少空闲时间,当空闲时间结束时,我们需要中断渲染操作,将控制权交给浏览器去执行优先级更高的操作。

js 复制代码
function workLoop(deadLine) {
  let shouldYield = false;
  while (!shouldYield) {
    ......
    shouldYield = deadLine.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

接下来我们处理workLoop中执行渲染工作的逻辑:

js 复制代码
let nextUnitOfWork = null;

function workLoop(deadLine) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

    shouldYield = deadLine.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(nextUnitOfWork) {
  ......
}

我们编写一个performUnitOfWork函数,这个函数的作用是执行当前的渲染工作片段,并且返回下一个渲染工作片段。顺带完善一下其它逻辑,用一个变量来保存下一个渲染工作的片段,并在每次执行渲染工作时更新它。

本章节实现了一个大致的代码框架(主要是思路),具体performUnitOfWork函数中执行渲染工作的代码将在后续章节中介绍。

四、Fibers

这一章节,我们将介绍一个耳熟能详的数据结构:Fiber Tree,用这个数据结构来管理各个片段的渲染工作。

Fiber Tree的本质是一个链表 ,下文将提到的Fiber可以理解为是链表中的一个节点。

我们先来看一个例子,假设我们要渲染这样一个DOM结构:

js 复制代码
Teact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
);

那么在渲染过程中,我们首先需要创建根Fiber节点 ,并将其设置为初始的nextUnitOfWork。剩余的渲染工作将放在performUnitOfWork函数中。

也就是说,我们在performUnitOfWork函数中需要做3件事情:

1、将元素添加到真实DOM结构中;

2、创建当前元素的子级Fiber节点们;

3、返回下一个渲染工作片段。

了解 Fiber Tree 的结构

在编写代码之前,我们先来熟悉一下Fiber Tree的结构,并看看它是如何进行下一个渲染工作片段的选取的。

从上图可以看出,Fiber Tree的结构与DOM结构有明显的不同:每一个节点只会有一个子节点(child) ,其余节点则被划分为了兄弟节点(sibling) ,并且子节点和兄弟节点都有指向父级节点(parent) 的箭头。

当我们完成一个Fiber节点的渲染工作,那么就去寻找它的子节点 作为下一个渲染工作片段:例如渲染完div节点 后,下一个渲染工作片段应该是p节点

如果没有子节点那么就去寻找它的兄弟节点 作为下一个渲染工作片段:例如渲染完p节点 后,下一个渲染工作片段应该是a节点

如果也没有兄弟节点,那么就去寻找它的 "uncle节点"(父级节点的兄弟节点) :例如渲染完a节点 后,下一个渲染工作片段应该是h2节点

如果也没有,那么就去寻找再上一级的父级节点,选取它的兄弟节点。

以此类推......

当我们再一次回到根节点时,意味着这一次渲染的所有工作均已完成。

接下来,我们开始编写代码,实现上述逻辑。

重构 render 函数

我们先重构 一下之前实现的render函数,现在,我们只需要保留它创建DOM节点的部分舍弃 递归执行render以及添加到真实DOM的部分:

js 复制代码
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).map(name => {
    dom[name] = fiber.props[name];
  });

  return dom;
}

另外,我们将原先的element替换成了fiber,所以其实fiber节点的结构就是在element的基础上增加了指向其它节点的指针,基本的节点属性仍保持一致。

在render函数中,我们去设置初始的nextUnitOfWork

js 复制代码
function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element]
    }
  }
}

初始的nextUnitOfWorkFiber Tree的根节点,其实就是承载DOM结构的容器 ,因此它不需要有type属性,并且props属性中也只有children属性(另外还有指向子节点的指针将在performUnitOfWork中添加)。根节点我们可以直接创建,除此以外其它节点的创建将在performUnitOfWork中进行。

实现 performUnitOfWork 函数

接下来,我们分3步实现performUnitOfWork函数的具体逻辑。

1、将元素添加到真实DOM结构中:

js 复制代码
if (!fiber.dom) {
  fiber.dom = createDom(fiber);
}

if (fiber.parent) {
  fiber.parent.dom.appendChild(fiber.dom);
}

在每一个Fiber节点中,增加了一个dom属性,用于构建真实DOM结构,添加子节点等。

2、创建当前元素的子级Fiber节点们:

js 复制代码
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;

while (index < elements.length) {
  const element = elements[index];

  const newFiber = {
    type: element.type,
    props: element.props,
    parent: fiber,
    dom: null
  }

  // 设置指向子节点和兄弟节点的指针
  if (index === 0) {
    fiber.child = newFiber;
  } else {
    prevSibling.sibling = newFiber;
  }

  prevSibling = newFiber;
  index++;
}

遍历当前Fiber节点的所有子级节点,创建对应的Fiber节点,并将其划分为子节点和兄弟节点 ,并且每个Fiber节点会有指向父/子/兄弟节点的指针 ,以此完成Fiber Tree的构建。

每个Fiber节点也和之前一样保留了typeprops属性用于将Fiber节点创建为真实DOM节点

3、返回下一个渲染工作片段:

js 复制代码
if (fiber.child) {
  return fiber.child;
}

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

  nextFiber = nextFiber.parent;
}

按照之前描述的寻找下一个渲染工作片段 的顺序返回对应的Fiber节点。

至此,我们实现了一个完整的performUnitOfWork函数:

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

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }

  // 创建当前元素的子级Fiber节点们,构建Fiber Tree
  const elements = fiber.props.children;
  let index = 0;
  let prevSibling = null;

  while (index < elements.length) {
    const element = elements[index];

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null
    }

    // 设置指向子节点和兄弟节点的指针
    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }

  // 返回下一个渲染工作片段
  if (fiber.child) {
    return fiber.child;
  }

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

    nextFiber = nextFiber.parent;
  }
}

用它替换掉原先的render函数,依旧可以正常运行!

五、渲染与提交

现在,我们遇到了一个新的问题:由于渲染过程是会被浏览器打断的,而我们目前的渲染逻辑会直接将完成渲染的部分添加到真实DOM上,这样可能会导致页面上展示的内容为半成品。这一章节,我们就要解决这个问题。

首先,我们将添加到真实DOM结构上的代码注释掉:

js 复制代码
// if (fiber.parent) {
//   fiber.parent.dom.appendChild(fiber.dom);
// }

然后,我们用一个变量(wipRoot - work in progress root )来保存整个Fiber Tree,当其全部完成渲染时再一次性添加到真实DOM中:

js 复制代码
let wipRoot = null;
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    }
  };

  nextUnitOfWork = wipRoot;
}
js 复制代码
function workLoop(deadLine) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

    shouldYield = deadLine.timeRemaining() < 1;
  }

  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }

  requestIdleCallback(workLoop);
}

nextUnitOfWork为空时就认为完成了全部的渲染工作。

commitRoot函数中,我们用递归 的方法将所有节点添加到真实DOM上,就像我们一开始在render函数中做的那样:

js 复制代码
function commitRoot() {
  commitWork(wipRoot.child);
  wipRoot = null;
}

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

  const domParent = fiber.parent.dom;
  domParent.appendChild(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

这样一来,我们就解决了章节开头提到的那个问题。

六、调和(Reconciliation)

目前,我们只实现了向真实DOM树中添加节点,接下来我们要实现更新删除的功能。

要实现这两个功能,我们需要将上一次渲染的结果保存下来,与当前渲染的元素进行比较

我们用currentRoot来保存上一次渲染的结果,另外,我们为每个Fiber节点 添加一个新的属性:alternate指向上一次渲染中对应的Fiber节点,便于两者进行比较。

那么,render函数中的根节点就变成了这样:

js 复制代码
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
    alternate: currentRoot
  };

  nextUnitOfWork = wipRoot;
}

在commit函数中将提交渲染的结果保存下来:

js 复制代码
function commitRoot() {
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

alternate在第一次渲染时为null,但在后续的渲染中便是上一次渲染的结果。

重构 performUnitOfWork 函数

接下来,我们修改performUnitOfWork中创建Fiber节点构建Fiber Tree的部分,对每一个Fiber节点进行比较

我们将具体逻辑抽离到reconcileChildren函数中:

js 复制代码
function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let prevSibling = null;

  while (index < elements.length) {
    const element = elements[index];

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: wipFiber,
      dom: null
    }

    // 设置指向子节点和兄弟节点的指针
    if (index === 0) {
      wipFiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
}

接下来开始增加新的逻辑。

首先我们需要获取上一次渲染的节点,与当前要渲染的元素进行比较,可以通过alternate属性:

js 复制代码
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;

注意:这里的oldFiber是上一次渲染中对应的子级节点 ,因为我们要比较的元素也是子级元素,需要对应起来。

另外,我们增加一个循环条件:oldFiber !== null,处理上一次渲染中存在而当前渲染不存在的节点。

接下来我们需要比较oldFiberelementtype,根据比较的结果进行不同的处理:

js 复制代码
function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let prevSibling = null;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;

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

    let newFiber = null;

    // 当前要渲染的element与上一次commit的Fiber Tree节点进行比较
    const sameType = oldFiber && element && element.type === oldFiber.type;

    if (sameType) { // update
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE"
      }
    }
    if (element && !sameType) { // add
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT"
      }
    }
    if (oldFiber && !sameType) { // delete
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    // 设置指向子节点和兄弟节点的指针
    if (index === 0) {
      wipFiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
}

我们在每个Fiber节点 上增加了一个新的属性:effectTag,后续在渲染到真实DOM时需要根据effectTag的值进行不同的处理。

新旧节点的比较有3种情况:

1、type属性相同,判定为UPDATE。保留oldFiber的type和dom,仅更新props属性。

2、type属性不同,存在element,判定为PLACEMENT新增 的Fiber节点,和之前一样,dom属性暂时为null。

3、type属性不同,存在oldFiber,判定为DELETION删除 的Fiber节点,不需要创建新的Fiber节点,只需要在oldFiber上增加一个值为DELETION的effectTag属性即可。另外,因为提交时是根据当前的Fiber Tree进行渲染的,在那上面并不存在上一次渲染结果中需要删除的节点,所以我们需要用一个数组被删除的Fiber节点保存下来

注意:如果一个节点的type发生了变化,会被认为是旧节点的删除 以及新节点的新增;。

以上就是重构后的构建Fiber Tree的内容了,接下来我们进行commitWork的重构,让它能够处理effectTag的逻辑。

重构 commitWork 函数

js 复制代码
function commitWork(fiber) {
  if (!fiber) return;

  const domParent = fiber.parent.dom;
  if (fiber.effectTag === "PLACEMENT" && fiber.dom !== null) {
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom);
  } else if (fiber.effectTag === "UDPATE" && fiber.dom !== null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

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

如果effectTagPLACEMENT,那么和原先一样将其添加到DOM结构中;如果是DELETION,那么正好相反,将其从DOM结构中移除。

比较复杂的是UPDATE的情况,我们将更新 的逻辑抽离出来,放在updateDom函数中。

我们保持dom节点不变,更新dom节点上的props属性,将先前的props移除,添加新增以及变动的props:

js 复制代码
const isProperty = key => key !== "children";
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = next => key => !(key in next);
function updateDom(dom, prevProps, nextProps) {
  // 移除变更的props
  Object.keys(prevProps).filter(isProperty).
    filter(isGone(nextProps)).map(name => dom[name] = "");

  // 添加新增或变化的props
  Object.keys(nextProps).filter(isProperty).
    filter(isNew(prevProps, nextProps)).map(name => dom[name] = nextProps[name]);
}

还有一种特殊情况是props属性中有监听事件,对于这一类props我们需要特殊处理:

js 复制代码
const isEvent = key => key.startsWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = next => key => !(key in next);
function updateDom(dom, prevProps, nextProps) {
  // 移除变更的监听事件
  Object.keys(prevProps).filter(isEvent).filter(key => isGone(nextProps)(key) || isNew(prevProps, nextProps)(key)).map(name => {
    const eventType = name.toLowerCase().substring(2);
    dom.removeEventListener(eventType, prevProps[name]);
  });

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

  // 移除变更的props
  Object.keys(prevProps).filter(isProperty).filter(isGone(nextProps)).map(name => dom[name] = "");

  // 添加新增或变化的props
  Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).map(name => dom[name] = nextProps[name]);
}

至此,我们就成功地实现了更新DOM树的功能!

理解与讨论

js 复制代码
deletions.map(item => commitWork(item));
......
else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom);
}
......

删除 的操作部分我一开始有这样的疑惑:要删除的元素并不在当前要渲染的DOM结构中,而是在上一次渲染的DOM结构中,为什么要去处理它呢?

实际上的逻辑是这样的:当前要渲染的DOM结构是基于 上一次渲染的DOM结构的。也就是说,我对div节点进行了修改,并不会重新创建一个新的div节点,而是在上一次渲染的基础上进行更新 ,上一次渲染的div节点是包含子节点的,所以我们要通过上一次渲染的DOM结构去将其删除

小结

在这篇文章中,我们了解了React渲染的底层逻辑以及Fiber的结构与原理,实现了一个简易版的React ,它可以实现页面的渲染和更新,不过还只是基于类式组件 的。对于函数式组件的兼容适配,我将在下一篇文章进行介绍,有兴趣的朋友们可以关注一下。

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

原文链接:Build your own React

相关推荐
光头程序员7 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me8 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者8 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
VillanelleS11 小时前
React进阶之高阶组件HOC、react hooks、自定义hooks
前端·react.js·前端框架
傻小胖11 小时前
React 中hooks之useInsertionEffect用法总结
前端·javascript·react.js
flying robot21 小时前
React的响应式
前端·javascript·react.js
GISer_Jing1 天前
React+AntDesign实现类似Chatgpt交互界面
前端·javascript·react.js·前端框架
智界工具库1 天前
【探索前端技术之 React Three.js—— 简单的人脸动捕与 3D 模型表情同步应用】
前端·javascript·react.js
我是前端小学生1 天前
我们应该在什么场景下使用 useMemo 和 useCallback ?
react.js
我是前端小学生1 天前
讲讲 React.memo 和 JS 的 memorize 函数的区别
react.js