简单实现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

相关推荐
bysking16 分钟前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
September_ning5 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人5 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0015 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking7 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫8 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull12 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
FinGet1 天前
那总结下来,react就是落后了
前端·react.js
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语2 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts