我的编程空间,编程开发者的网络收藏夹
学习永远不晚

教你如何从 html 实现一个 react

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

教你如何从 html 实现一个 react

什么是 React

React是一个简单的javascript UI库,用于构建高效、快速的用户界面。它是一个轻量级库,因此很受欢迎。它遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效。它使用虚拟DOM来有效地操作DOM。它遵循从高阶组件到低阶组件的单向数据流。

前言 📝

👉 我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。官网地址

react 的理念是在于对大型项目的快速响应,对于新版的 react 16.8 而言更是带来的全新的理念fiber去解决网页快速响应时所伴随的问题,即 CPU 的瓶颈,传统网页浏览受制于浏览器刷新率、js 执行时间过长等因素会造成页面掉帧,甚至卡顿

react 由于自身的底层设计从而规避这一问题的发生,所以 react16.8 的面世对于前端领域只办三件事:快速响应、快速响应、还是 Tmd 快速响应 !,这篇文章将会从一个 html 出发,跟随 react 的 fiber 理念,仿一个非常基础的 react

一开始的准备工作 🤖

html

我们需要一个 html 去撑起来整个页面,支撑 react 运行,页面中添加<div></div>,之后添加一个 script 标签,因为需要使用import进行模块化构建,所以需要为 script 添加 type 为module的属性


<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="root"></div>
  <script type="module" class="lazy" data-src="./index.js" ></script>
</body>

</html>

推荐安装一个 Live Server 插件,有助于我们对代码进行调试,接下来的操作也会用到

JavaScript

我们会仿写一个如下的 react,实现一个基础的操作,在 <input/> 绑定事件,将输入的值插入在 <h2/> 标签内:


...
function App() {
  return (
    <div>
      <input onInput={updateValue} value={value} />
      <h2>Hello {value}</h2>
      <hr />
    </div>
  );
}
...

在 react 进行 babel 编译的时候,会将 JSX 语法转化为 React.createElement() 的形式,如上被 retuen 的代码就会被转换成


...
React.createElement(
  "div",
  null,
  React.createElement("input", {
    onInput: updateValue,
    value: value,
  }),
  React.createElement("h2", null, "Hello ", value),
  React.createElement("hr", null)
);
...

在线地址

从转换后的代码我们可以看出 React.createElement 支持多个参数:

  • type,节点类型
  • config, 节点上的属性,比如 id 和 href
  • children, 子元素了,子元素可以有多个,类型可以是简单的文本,也可以还是 React.createElement,如果是 React.createElement,其实就是子节点了,子节点下面还可以有子节点。这样就用 React.createElement 的嵌套关系实现了 HTML 节点的树形结构。

我们可以按照 React.createElement 的形式仿写一个可以实现同样功能的 createElement 将 jsx 通过一种简单的数据结构展示出来即 虚拟DOM 这样在更新时,新旧节点的对比也可以转化为虚拟 DOM 的对比


{
  type:'节点标签',
  props:{
    props:'节点上的属性,包括事件、类...',
    children:'节点的子节点'
  }
}

这里我们可以写一个函数实现下列需求

  • 原则是将所有的参数返回到一个对象上
  • children 也要放到 props 里面去,这样我们在组件里面就能通过 props.children 拿到子元素
  • 当子组件是文本节点时,通过构造一种 type 为 TEXT_ELEMENT 的节点类型表示


const createElement = (type, props, ...children) => ({
  type,
  props: {
    ...props,
    children: children.map(child =>
      typeof child === "object"
        ? child
        : {
            type: "TEXT_ELEMENT",
            props: {
              nodeValue: child,
              children: [],
            },
          }
    ),
  },
});

react 中 createElement 源码实现

实现 createElement 之后我们可以拿到虚拟 DOM,但是还需要 render 将代码渲染到页面,此时我们需要对 index.js 进行处理,添加输入事件,将 createElementrender 通过 import 进行引入,render 时传入被编译后的虚拟 DOM 和页面的根元素 root, 最后再进行executeRender调用,页面被渲染,在页面更新的时候再次调用executeRender进行更新渲染


import {createElement,render} from "./mini/index.js";
const updateValue = e => executeRender(e.target.value);
const executeRender = (value = "World") => {
  const element = createElement(
    "div",
    null,
    createElement("input", {
      onInput: updateValue,
      value: value,
    }),
    createElement("h2", null, "Hello ", value),
    createElement("hr", null)
  );
  render(element, document.getElementById("root"));
};

executeRender();

render 的时候做了什么 🥔

before 版本

render 函数帮助我们将 element 添加至真实节点中,首先它接受两个参数:

根组件,其实是一个 JSX 组件,也就是一个 createElement 返回的虚拟 DOM

父节点,也就是我们要将这个虚拟 DOM 渲染的位置

在 react 16.8 之前,渲染的方法是通过一下几步进行的

  • 创建 element.type 类型的 dom 节点,并添加到 root 元素下(文本节点特殊处理)
  • 将 element 的 props 添加到对应的 DOM 上,事件进行特殊处理,挂载到 document 上(react17 调整为挂在到 container 上)
  • 将 element.children 循环添加至 dom 节点中;

拿到虚拟 dom 进行如上三步的递归调用,渲染出页面 类似于如下流程



const render = (element, container) => {
  let dom;
  
  if (typeof element !== "object") {
    dom = document.createTextNode(element);
  } else {
    dom = document.createElement(element.type);
  }
  
  if (element.props) {
    Object.keys(element.props)
      .filter((key) => key != "children")
      .forEach((item) => {
        dom[item] = element.props[item];
      });
    Object.keys(element.props)
      .filter((key) => key.startsWith("on"))
      .forEach((name) => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, nextProps[name]);
      });
  }
  if (
    element.props &&
    element.props.children &&
    element.props.children.length
  ) {
    
    element.props.children.forEach((child) => render(child, dom));
  }
  container.appendChild(dom);
};

after 版本(fiber)

当我们写完如上的代码,会发现这个递归调用是有问题的

如上这部分工作被 React 官方称为 renderer,renderer 是第三方可以自己实现的一个模块,还有个核心模块叫做 reconsiler,reconsiler 的一大功能就是 diff 算法,他会计算出应该更新哪些页面节点,然后将需要更新的节点虚拟 DOM 传递给 renderer,renderer 负责将这些节点渲染到页面上,但是但是他却是同步的,一旦开始渲染,就会将所有节点及其子节点全部渲染完成这个进程才会结束。

React 的官方演讲中有个例子,可以很明显的看到这种同步计算造成的卡顿:

当 dom tree 很大的情况下,JS 线程的运行时间可能会比较长,在这段时间浏览器是不会响应其他事件的,因为 JS 线程和 GUI 线程是互斥的,JS 运行时页面就不会响应,这个时间太长了,用户就可能看到卡顿,

此时我们可以分为两步解决这个问题

  • 允许中断渲染工作,如果有优先级更高的工作插入,则暂时中断浏览器渲染,待完成该工作后,恢复浏览器渲染;
  • 将渲染工作进行分解,分解成一个个小单元;

solution I 引入一个新的 Api

requestIdleCallback 接收一个回调,这个回调会在浏览器空闲时调用,每次调用会传入一个 IdleDeadline,可以拿到当前还空余多久, options 可以传入参数最多等多久,等到了时间浏览器还不空就强制执行了。

window.requestIdleCallback 将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件

但是这个 API 还在实验中,兼容性不好,所以 React 官方自己实现了一套。本文会继续使用 requestIdleCallback 来进行任务调度


// 下一个工作单元
let nextUnitOfWork = null

function workLoop(deadline) {
  // 是否应该停止工作循环函数
  let shouldYield = false

  // 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )

    // 如果截止时间快到了,停止工作循环函数
    shouldYield = deadline.timeRemaining() < 1
  }

  // 通知浏览器,空闲时间应该执行 workLoop
  requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)

// 执行单元事件,并返回下一个单元事件
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

solution II 创建 fiber 的数据结构

Fiber 之前的数据结构是一棵树,父节点的 children 指向了子节点,但是只有这一个指针是不能实现中断继续的。比如我现在有一个父节点 A,A 有三个子节点 B,C,D,当我遍历到 C 的时候中断了,重新开始的时候,其实我是不知道 C 下面该执行哪个的,因为只知道 C,并没有指针指向他的父节点,也没有指针指向他的兄弟。

Fiber 就是改造了这样一个结构,加上了指向父节点和兄弟节点的指针:

  • child 指向子组件
  • sibling 指向兄弟组件
  • return 指向父组件

每个 fiber 都有一个链接指向它的第一个子节点、下一个兄弟节点和它的父节点。这种数据结构可以让我们更方便的查找下一个工作单元,假定 A 是挂在 root 上的节点 fiber 的渲染顺序也如下步骤

  • 从 root 开始,找到第一个子节点 A;
  • 找到 A 的第一个子节点 B
  • 找到 B 的第一个子节点 E
  • 找 E 的第一个子节点,如无子节点,则找下一个兄弟节点,找到 E 的兄弟节点 F
  • 找 F 的第一个子节点,如无子节点,也无兄弟节点,则找它的父节点的下一个兄弟节点,找到 F 的 父节点的兄弟节点 C;
  • 找 C 的第一个子节点,找不到,找兄弟节点,D
  • 找 D 的第一个子节点,G
  • 找 G 的第一个子节点,找不到,找兄弟节点,找不到,找父节点 D 的兄弟节点,也找不到,继续找 D 的父节点的兄弟节点,找到 root;
  • 上一步已经找到了 root 节点,渲染已全部完成。

我们通过这个数据结构实现一个 fiber


//创建最初的根fiber
 wipRoot = {
  dom: container,
  props: { children: [element] },
};
performUnitOfWork(wipRoot);

随后调用performUnitOfWork自上而下构造整个 fiber 树



const  performUnitOfWork = fiber => {
  if (!fiber.dom) fiber.dom = createDom(fiber); // 创建一个DOM挂载上去
  const elements = fiber.props.children; //当前元素下的所有同级节点
  // 如果有父节点,将当前节点挂载到父节点上
  if (fiber.return) {
    fiber.return.dom.appendChild(fiber.dom);
  }

  let prevSibling = null;
  
  if (elements && elements.length) {
    elements.forEach((element, index) => {
      const newFiber = {
        type: element.type,
        props: element.props,
        return: fiber,
        dom: null,
      };
      // 父级的child指向第一个子元素
      if (index === 0) {
        fiber.child = newFiber;
      } else {
        // 每个子元素拥有指向下一个子元素的指针
        prevSibling.sibling = newFiber;
      }
      prevSibling = fiber;
    });
  }
  // 先找子元素,没有子元素了就找兄弟元素
  // 兄弟元素也没有了就返回父元素
  // 最后到根节点结束
  // 这个遍历的顺序是从上到下,从左到右
  if (fiber.child) {
    return fiber.child;
  } else {
    let nextFiber = fiber;
    while (nextFiber) {
      if (nextFiber.sibling) {
        return nextFiber.sibling;
      }
      nextFiber = nextFiber.return;
    }
  }
}

after 版本(reconcile)

currentRoot

reconcile 其实就是虚拟 DOM 树的 diff 操作,将更新前的 fiber tree 和更新后的 fiber tree 进行比较,得到比较结果后,仅对有变化的 fiber 对应的 dom 节点进行更新。

  • 删除不需要的节点
  • 更新修改过的节点
  • 添加新的节点

新增 currentRoot 变量,保存根节点更新前的 fiber tree,为 fiber 新增 alternate 属性,保存 fiber 更新前的 fiber tree


let currentRoot = null
function render (element, container) {
    wipRoot = {
        // 省略
        alternate: currentRoot
    }
}
function commitRoot () {
    commitWork(wipRoot.child)
    
    currentRoot = wipRoot
    wipRoot = null
}
  • 如果新老节点类型一样,复用老节点 DOM,更新 props
  • 如果类型不一样,而且新的节点存在,创建新节点替换老节点
  • 如果类型不一样,没有新节点,有老节点,删除老节点

reconcileChildren

  • 将 performUnitOfWork 中关于新建 fiber 的逻辑,抽离到 reconcileChildren 函数
  • 在 reconcileChildren 中对比新旧 fiber;

在对比 fiber tree 时

  • 当新旧 fiber 类型相同时 保留 dom,仅更新 props,设置 effectTag 为 UPDATE
  • 当新旧 fiber 类型不同,且有新元素时 创建一个新的 dom 节点,设置 effectTag 为 PLACEMENT
  • 当新旧 fiber 类型不同,且有旧 fiber 时 删除旧 fiber,设置 effectTag 为 DELETION


function reconcileChildren(wipFiber, elements) {
  let index = 0;// 用于统计子节点的索引值
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //更新时才会产生
  let prevSibling;// 上一个兄弟节点
  while (index < elements.length || oldFiber) {
    
    let newFiber;
    const element = elements[index];
    const sameType = oldFiber && element && element.type == oldFiber.type; // fiber 类型是否相同点
    
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props, //只更新属性
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",
      };
    }
    
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      };
    }
    
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }

    if (oldFiber) oldFiber = oldFiber.sibling;
    // 父级的child指向第一个子元素
    if (index === 0) {
      // fiber的第一个子节点是它的子节点
      wipFiber.child = newFiber;
    } else {
      // fiber 的其他子节点,是它第一个子节点的兄弟节点
      prevSibling.sibling = newFiber;
    }
    // 把新建的 newFiber 赋值给 prevSibling,这样就方便为 newFiber 添加兄弟节点了
    prevSibling = newFiber;
    //  索引值 + 1
    index++;
  }
}

在 commit 时,根据 fiber 节点上effectTag的属性执行不同的渲染操作

after 版本(commit)

在 commitWork 中对 fiber 的 effectTag 进行判断,处理真正的 DOM 操作。

  • 当 fiber 的 effectTag 为 PLACEMENT 时,表示是新增 fiber,将该节点新增至父节点中。
  • 当 fiber 的 effectTag 为 DELETION 时,表示是删除 fiber,将父节点的该节点删除。
  • 当 fiber 的 effectTag 为 UPDATE 时,表示是更新 fiber,更新 props 属性。


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

  // 递归操作子元素和兄弟元素
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

此时我们着重来看updateDom发生了什么,我们拿到 dom 上被改变的新旧属性,进行操作



const isEvent = key => key.startsWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key => prev[key] !== next[key];



function updateDom(dom, prevProps, nextProps) {
  
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(key => !(key in nextProps))
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2);
      dom.removeEventListener(eventType, prevProps[name]);
    });

  
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(key => !(key in nextProps))
    .forEach(key => delete dom[key]);

  
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name];
    });

  
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2);
      dom.addEventListener(eventType, nextProps[name]);
    });
}

完成了一系列对 dom 的操作,我们将新改变的 dom 渲染到页面,当 input 事件执行时,页面又会进行渲染,但此时会进入更新 fiber 树的逻辑,
alternate 指向之前的 fiber 节点进行复用,更快的执行 Update 操作,如图:

大功告成!

完整代码可以看我github。

结论与总结 💢

结论

  1. 我们写的 JSX 代码被 babel 转化成了 React.createElement。
  2. React.createElement 返回的其实就是虚拟 DOM 结构。
  3. 虚拟 DOM 的调和和渲染可以简单粗暴的递归,但是这个过程是同步的,如果需要处理的节点过多,可能会阻塞用户输入和动画播放,造成卡顿。
  4. Fiber 是 16.x 引入的新特性,用处是将同步的调和变成异步的。
  5. Fiber 改造了虚拟 DOM 的结构,具有 父->第一个子, 子->兄, 子->父这几个指针,有了这几个指针,可以从任意一个 Fiber 节点找到其他节点。
  6. Fiber 将整棵树的同步任务拆分成了每个节点可以单独执行的异步执行结构。
  7. Fiber 可以从任意一个节点开始遍历,遍历是深度优先遍历,顺序是 父->子->兄->父,也就是从上往下,从左往右。
  8. Fiber 的调和阶段可以是异步的小任务,但是提交阶段( commit)必须是同步的。因为异步的 commit 可能让用户看到节点一个一个接连出现,体验不好。

总结

  • react hook 实现 ✖
  • react 合成事件 ✖
  • 还有很多没有实现 😤...

至此,谢谢各位在百忙之中点开这篇文章,希望对你们能有所帮助,如有问题欢迎各位大佬指正。工作原因这篇文章大概断断续续写了有一个月,工作上在忙一个基于 腾讯云TRTC+websocket 的小程序电话功能,有时间也会写成文章分享一下,当然 react 的实现文章也会继续

👋:跳转 github 欢迎给个 star,谢谢大家了

参考文献

🍑:手写系列-实现一个铂金段位的 React

🍑:build-your-own-react(强烈推荐)

🍑:手写 React 的 Fiber 架构,深入理解其原理

🍑:手写一个简单的 React

🍑:妙味课堂大圣老师 手写 react 的 fiber 和 hooks 架构

🍑:React Fiber 架构

🍑:手写一个简单的 React

到此这篇关于教你如何从 html 实现一个 react的文章就介绍到这了,更多相关 html 实现react内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

教你如何从 html 实现一个 react

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

教你如何实现在react项目中嵌入Blazor

这篇文章主要介绍了如何实现在react现有项目中嵌入Blazor,通过这个案例我们可以知道blazor也可以像react那样嵌入在任何的现有项目中,并且使用方便,需要的朋友可以参考下
2023-01-28

教你使用PHP实现一个轻量级HTML模板引擎

🏆作者简介,黑夜开发者,全栈领域新星创作者✌,2023年6月csdn上海赛道top4。多年电商行业从业经验,对系统架构,数据分析处理等大规模应用场景有丰富经验。 🏆本文已收录于PHP专栏:PHP进阶实战
2023-08-16

如何使用react实现一个tab组件

这篇“如何使用react实现一个tab组件”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“如何使用react实现一个tab组件
2023-07-04

一文教你掌握Java如何实现判空

实际项目中我们会有很多地方需要判空校验,如果不做判空校验则可能产生NullPointerException异常。所以本文小编为大家整理了Java中几个常见的判空方法,希望对大家有所帮助
2023-05-17

一文教你JavaScript如何实现分支优化

本篇文章给大家带来了关于JavaScript的相关知识,其中主要介绍了关于分支优化的相关内容,如果有许多个判断条件,使用大量的if分支会使整个代码的可读性和可维护都大大降低,下面一起来看一下,希望对大家有帮助。
2022-11-22

HTML中如何实现一个canvas智绘画板

这篇文章主要介绍了HTML中如何实现一个canvas智绘画板,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。 一、项目介绍名称:智绘画板技术栈:HTML5,CSS3,JavaS
2023-06-09

最新【从零单排】系列流出,教你如何实现字

昨天在星球的【从零单排】系列分享了一篇【字典存储结构的实现方式】,我觉得这篇文章写的还是蛮好的,就分享给大家了。但由于篇幅太长了,微信没办法全部传上来,如果你觉得内容还不错,想获得全部内容的话,关注公众号「Python专栏」,后台直接回复:
2023-01-31

如何在html中实现一个轮播图效果

这期内容当中小编将会给大家带来有关如何在html中实现一个轮播图效果,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。html是什么html的全称为超文本标记语言,它是一种标记语言,包含了一系列标签.通过这些
2023-06-06

一步步教你如何创建第一个C#项目

这篇文章主要给大家介绍了关于如何创建第一个C#项目的相关资料,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
2022-12-08

一文教会你在Docker容器中实现Mysql主从复制

目录一 主从复制概念二 主从复制的优势三 docker容器实现mysql主从复制3.1 创建主服务器容器3.2 新建主配置文件 my.cnf3.3 重启master容器实例3.4 进入mysql-master容器3.5 在主服务器中创建用户
2022-11-29

编程热搜

目录