手写一个react,看透react运行机制

news2024/11/15 15:59:59

适合人群

本文适合0.5~3年的react开发人员的进阶。

讲讲废话:

react的源码,的确是比vue的难度要深一些,本文也是针对初中级,本意让博友们了解整个react的执行过程。

写源码之前的必备知识点

JSX

首先我们需要了解什么是JSX。

网络大神的解释:React 使用 JSX 来替代常规的 JavaScript。JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。

是的,JSX是一种js的语法扩展,表面上像HTML,本质上还是通过babel转换为js执行。再通俗的一点的说,jsx就是一段js,只是写成了html的样子,而我们读取他的时候,jsx会自动转换成vnode对象给我们,这里都由react-script的内置的babel帮助我们完成。

简单举个栗子:

return (
  <div>
    Hello  Word  </div>
)

实际上是:

return React.createElement(
  "div",
  null,
  "Hello"
)

JSX本质上就是转换为React.createElement在React内部构建虚拟Dom,最终渲染出页面。

虚拟Dom

这里说明一下react的虚拟dom。react的虚拟dom跟vue的大为不同。vue的虚拟dom是为了是提高渲染效率,而react的虚拟dom是一定需要。很好理解,vue的template本身就是html,可以直接显示。而jsx是js,需要转换成html,所以用到虚拟dom。

我们描述一下react的最简版的vnode:

function createElement(type, props, ...children) {
  props.children = children;
  return {
    type,
    props,
    children,
  };
}

这里的vnode也很好理解,
type表示类型,如div,span,
props表示属性,如{id: 1, style:{color:red}},
children表示子元素
下边会在createElement继续讲解。

原理简介

我们写一个react的最简单的源码:

import React from 'react'
import ReactDOM from 'react-dom'
function App(props){
     return <div>你好</div>
 </div>
}
ReactDOM.render(<App/>,  document.getElementById('root'))
  • React负责逻辑控制,数据 -> VDOM
    首先,我们可以看到每一个js文件中,都一定会引入import React from ‘react’。但是我们的代码里边,根本没有用到React。但是你不引入他就报错了。

为什么呢?可以这样理解,在我们上述的js文件中,我们使用了jsx。但是jsx并不能给编译,所以,报错了。这时候,需要引入react,而react的作用,就是把jsx转换为“虚拟dom”对象。

JSX本质上就是转换为React.createElement在React内部构建虚拟Dom,最终渲染出页面。而引入React,就是为了时限这个过程。

  • ReactDom渲染实际DOM,VDOM -> DOM

理解好这一步,我们再看ReactDOM。React将jsx转换为“虚拟dom”对象。我们再利用ReactDom的虚拟dom通过render函数,转换成dom。再通过插入到我们的真是页面中。

这就是整个mini react的一个简述过程。

手写react过程

1)基本架子的搭建

react的功能化问题,暂时不考虑。例如,启动react,怎么去识别JSX,实现热更新服务等等,我们的重点在于react自身。我们借用一下一下react-scripts插件。

有几种种方式创建我们的基本架子:

  • 利用 create-react-app zwz_react_origin快速搭建,然后删除原本的react,react-dom等文件。(zwz_react_origin是我的项目名称)

  • 第二种,复制下边代码。新建package.json

      {
        "name": "zwz_react_origin",
        "scripts": {
          "start": "react-scripts start"
        },
        "version": "0.1.0",
        "private": true,
        "dependencies": {
          "react-scripts": "3.4.1"
        },
      }
    

    然后新建public下边的index.html

      <!DOCTYPE html>
      <html lang="en">
        <head>
        </head>
        <body>
          <div id="root"></div>
        </body>
      </html>
    

    再新建src下边的index.js

    这时候react-scripts会快速的帮我们定为到index.html以及引入index.js

      import React from "react";
      import ReactDOM from "react-dom";
    
      let jsx = (
        <div>
          <div className="">react启动成功</div>
        </div>
      );
      ReactDOM.render(jsx, document.getElementById("root"));
    

    这样,一个可以写react源码的轮子就出来了。

2) React的源码

let obj = (
  <div>
    <div className="class_0">你好</div>
  </div>
);
console.log(`obj=${ JSON.stringify( obj) }`);

首先,我们上述代码,如果我们不import React处理的话,我们可以打印出:
‘React’ must be in scope when using JSX react/react-in-jsx-scope
是的,编译不下去,因为js文件再react-script,他已经识别到obj是jsx。该jsx却不能解析成虚拟dom, 此时我们的页面就会报错。通过资料的查阅,或者是源码的跟踪,我们可以知道,实际上,识别到jsx之后,会调用页面中的createElement转换为虚拟dom。

我们import React,看看打印出来什么?相关参考视频讲解:进入学习

+ import React from "react";
let obj = (
  <div>
    <div className="class_0">你好</div>
  </div>
);
console.log(`obj:${ JSON.stringify( obj) }`);

结果:
jsx={"type":"div","key":null,"ref":null,"props":{"children":{"type":"div","key":null,"ref":null,"props":{"className":"class_0","children":"你好"},"_owner":null,"_store":{}}},"_owner":null,"_store":{}}

由上边结论可以知道, babel会识别到我们的jsx,通过createElement并将其dom(html语法)转换为虚拟dom。从上述的过程,我们可以看到虚拟dom的组成,由type,key,ref,props组成。我们来模拟react的源码。

此时我们已经知道react中的createElement的作用是什么,我们可以尝试着自己来写一个createElement(新建react.js引入并手写下边代码):

function createElement() {
  console.log("createElement", arguments);
}

export default {
  createElement,
};

此时的打印结果:



我们可以看出对象传递的时候,dom的格式,先传入type, 然后props属性,我们根据原本react模拟一下这个对象转换的打印:

function createElement(type, props, ...children) {
  props.children = children;
  return {
    type,
    props,
  };
}

这样,我们已经把最简版的一个react实现,我们下边继续看看如何render到页面

3) ReactDom.render

import React from "react";
+ import ReactDOM from "react-dom";
let jsx = (
  <div>
    <div className="class_0">你好</div>
  </div>
);
// console.log(`jsx=${ JSON.stringify( jsx) }`);
+ ReactDOM.render(jsx, document.getElementById("root"));

如果此时,我们引入ReactDom,通过render到对应的元素,整个简版react的就已经完成,页面就会完成渲染。首先,jsx我们已经知道是一个vnode,而第二个元素即是渲染上页面的元素,假设我们的元素是一个html原生标签div。
我们新建一个reactDom.js引入。

function render(vnode, container) {
  mount(vnode, container);
}

function mount(vnode, container){
    const { type, props } = vnode;
    const node = document.createElement(type);//创建一个真实dom
    const { children, ...rest } = props;
    children.map(item => {//子元素递归
        if (Array.isArray(item)) {
          item.map(c => {
            mount(c, node);
          });
        } else {
          mount(item, node);
        }
    });
    container.appendChild(node);
}


//主页:
- import React from "react";
- import ReactDOM from "react-dom";
+ import React from "./myReact/index.js";
+ import ReactDOM from "./myReact/reactDom.js";
let jsx = (
  <div>
    <div className="class_0">你好</div>
  </div>
);
ReactDOM.render(jsx, document.getElementById("root"));

此时,我们可以看到页面,我们自己写的一个react渲染已经完成。我们优化一下。

首先,这个过程中, className="class_0"消失了。我们想办法渲染上页面。此时,虚拟dom的对象,没有办法,区分,哪些元素分别带有什么属性,我们在转义的时候优化一下mount。

 function mount(vnode, container){
    const { type, props } = vnode;
    const node = document.createElement(type);//创建一个真实dom
    const { children, ...rest } = props;
    children.map(item => {//子元素递归
        if (Array.isArray(item)) {
          item.map(c => {
            mount(c, node);
          });
        } else {
          mount(item, node);
        }
    });

    // +开始
    Object.keys(rest).map(item => {
        if (item === "className") {
          node.setAttribute("class", rest[item]);
        }
        if (item.slice(0, 2) === "on") {
          node.addEventListener("click", rest[item]);
        }
      });
    // +结束  

    container.appendChild(node);
}

4) ReactDom.Component

看到这里,整个字符串render到页面渲染的过程已完成。此时入口文件已经解决了。对于原始标签div, h1已经兼容。但是对于自定义标签呢?或者怎么完成组件化呢。

我们先看react16+的两种组件化模式,一种是function组件化,一种是class组件化。

首先,我们先看看demo.

import React, { Component } from "react";
import ReactDOM from "react-dom";
 class MyClassCmp extends React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    return (
    <div className="class_2" >MyClassCmp表示:{this.props.name}</div>
    );
  }

}

function MyFuncCmp(props) {
  return <div className="class_1" >MyFuncCmp表示:{props.name}</div>;
}
let jsx = (
  <div>
    <h1>你好</h1>
    <div className="class_0">前端小伙子</div>
    <MyFuncCmp />
    <MyClassCmp  />
  </div>
);
ReactDOM.render(jsx, document.getElementById("root"));

先看简单点一些的Function组件。暂不考虑传递值等问题,Function其实跟原本组件不一样的地方,在于他是个函数,而原本的jsx,是一个字符串。我们可以根据这个特点,将函数转换为字符串,那么Function组件即跟普通标签同一性质。

我们写一个方法:

mountFunc(vnode, container);

function mountFunc(vnode, container) {
  const { type, props } = vnode;
  const node = new type(props);
  mount(node, container);
}

此时type即是函数体内容,我们只需要实例化一下,即可跟拿到对应的字符串,即是普通的vnode。再利用我们原来的vnode转换方法,即可实现。

按照这个思路,如果我们不考虑生命周期等相对复杂的东西。我们也相对简单,只需拿到类中的render函数即可。

mountFunc(vnode, container);

function mountClass(vnode, container) {
  const { type, props } = vnode;
  const node = new type(props);
  mount(node.render(), container);
}

这里可能需注意,class组件,需要继承React.Component。截图一下react自带的Component

可以看到,Component统一封装了,setState,forceUpdate方法,记录了props,state,refs等。我们模拟一份简版为栗子:

class Component {
  static isReactComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {};
  }
  setState = () => {};
}

再添加一个标识,isReactComponent表示是函数数组件化。这样的话,我们就可以区分出:普通标签,函数组件标签,类组件标签。

我们可以重构一下createElement方法,多定义一个vtype属性,分别表示

    1. 普通标签
    1. 函数组件标签
    1. 类组件标签

根据上述标记,我们可改造为:

function createElement(type, props, ...children) {
  props.children = children;
  let vtype;
  if (typeof type === "string") {
    vtype = 1;
  }
  if (typeof type === "function") {
    vtype = type.isReactComponent ? 2 : 3;
  }
  return {
    vtype,
    type,
    props,
};

那么,我们处理时:

function mount(vnode, container) {
  const { vtype } = vnode;
  if (vtype === 1) {
    mountHtml(vnode, container); //处理原生标签
  }

  if (vtype === 2) {
    //处理class组件
    mountClass(vnode, container);
  }

  if (vtype === 3) {
    //处理函数组件
    mountFunc(vnode, container);
  }

}

至此,我们已经完成一个简单可组件化的react源码。不过,此时有个bug,就是文本元素的时候异常,因为文本元素不带标签。我们优化一下。

function mount(vnode, container) {
  const { vtype } = vnode;
  if (!vtype) {
    mountTextNode(vnode, container); //处理文本节点
  }
  //vtype === 1
  //vtype === 2
  // ....
}

//处理文本节点
function mountTextNode(vnode, container) {
  const node = document.createTextNode(vnode);
  container.appendChild(node);
}

简单源码:

package.json:

{
  "name": "zwz_react_origin",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "react-scripts": "3.2.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",      "not dead",      "not op_mini all"    ],    "development": [      "last 1 chrome version",      "last 1 firefox version",      "last 1 safari version"    ]  }}

index.js

import React from "./wzReact/";
import ReactDOM from "./wzReact/ReactDOM";

class MyClassCmp extends React.Component {
  constructor(props) {
    super(props);
  }

render() {
    return (
    <div className="class_2" >MyClassCmp表示:{this.props.name}</div>
    );
  }
}

function MyFuncCmp(props) {
  return <div className="class_1" >MyFuncCmp表示:{props.name}</div>;
}

let jsx = (
  <div>
    <h1>你好</h1>
    <div className="class_0">前端小伙子</div>
    <MyFuncCmp name="真帅" />
    <MyClassCmp name="还有钱" />
  </div>
);

ReactDOM.render(jsx, document.getElementById("root"));

/wzReact/index.js

function createElement(type, props, ...children) {
  console.log("createElement", arguments);
  props.children = children;
  let vtype;
  if (typeof type === "string") {
    vtype = 1;
  }
  if (typeof type === "function") {
    vtype = type.isReactComponent ? 2 : 3;
  }
  return {
    vtype,
    type,
    props,
  };
}

class Component {
  static isReactComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {};
  }
  setState = () => {};
}

export default {
  Component,
  createElement,
};

/wzReact/ReactDOM.js

function render(vnode, container) {
  console.log("render", vnode);
  //vnode-> node
  mount(vnode, container);
  // container.appendChild(node)
}
// vnode-> node
function mount(vnode, container) {
  const { vtype } = vnode;
  if (!vtype) {
    mountTextNode(vnode, container); //处理文本节点
  }
  if (vtype === 1) {
    mountHtml(vnode, container); //处理原生标签
  }

  if (vtype === 3) {
    //处理函数组件
    mountFunc(vnode, container);
  }

  if (vtype === 2) {
    //处理class组件
    mountClass(vnode, container);
  }
}

//处理文本节点
function mountTextNode(vnode, container) {
  const node = document.createTextNode(vnode);
  container.appendChild(node);
}

//处理原生标签
function mountHtml(vnode, container) {
  const { type, props } = vnode;
  const node = document.createElement(type);

  const { children, ...rest } = props;
  children.map(item => {
    if (Array.isArray(item)) {
      item.map(c => {
        mount(c, node);
      });
    } else {
      mount(item, node);
    }
  });

  Object.keys(rest).map(item => {
    if (item === "className") {
      node.setAttribute("class", rest[item]);
    }
    if (item.slice(0, 2) === "on") {
      node.addEventListener("click", rest[item]);
    }
  });

  container.appendChild(node);
}

function mountFunc(vnode, container) {
  const { type, props } = vnode;
  const node = new type(props);
  mount(node, container);
}

function mountClass(vnode, container) {
  const { type, props } = vnode;
  const cmp = new type(props);
  const node = cmp.render();
  mount(node, container);
}

export default {
  render,
};

至此,本文mini简单版本源码结束,代码将在文章最后段送出。
因本文定位初中级, 没有涉及react全家桶。
下一篇,fiber,redux, hooks等概念或者源码分析,将在新文章汇总出。如对你有用,关注期待后续文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/30812.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flutter高仿微信-第19篇-支付-我的零钱

Flutter高仿微信系列共59篇&#xff0c;从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。 详情请查看 效果图&#xff1a; 实现代码&#xff1a; /*** Author : wangning* Email : maoning20080809163.…

Unity DOTS学习 前置知识(一)

DOTS是什么 Data-Oriented Technology Stack(面向数据的技术栈) Unity 使用的5个核心包&#xff1a; The C# job system 提供快速安全的多线程操作The Burst compiler 优化C#代码的编译器&#xff0c;能够编译生成比mono或者L2CPP更快的代码。可以编译Unity中的任何代码Unit…

解决传统难题,WMS系统实现信息数据实时追踪

随着社会经济的发展&#xff0c;传统仓库的存储和做工难以适应当下市场经济的需求。仓库需要进行转型升级&#xff0c;从而适应当下的环境。在仓库的转型升级过程当中&#xff0c;WMS系统是不可或缺的一部分内容。 而WMS系统的应用会从多方面支持仓库的转型升级&#xff0c;其带…

uniapp之最新获取用户昵称以及头像

前言 在uniapp登录时候最开始想的就是手机号登录之后&#xff0c;就获取用户的昵称以及头像&#xff0c;存储起来&#xff0c;登录的时候直接显示在我的页面&#xff0c;最开始使用的是 uniapp官网自带的uni.getUserProfile的方法&#xff0c;就可以获取用户的头像跟昵称&…

Prometheus Operator与kube-prometheus之二-如何监控1.23+ kubeadm集群

简介 系列文章: 标签 - Prometheus - 东风微鸣技术博客 (ewhisper.cn)Prometheus Operator 的上一篇: Prometheus Operator 与 kube-prometheus 之一 - 简介 - 东风微鸣技术博客 (ewhisper.cn) kube-prometheus-stack捆绑了监控Kubernetes 集群所需的Prometheus Operator、Ex…

Web(二)html5基础-表格基本结构

第1关_网页表格的基本概念 第2关_创建简单的表格 本关任务&#xff1a;创建一个两行两列的表格。 相关知识&#xff1a;为了完成本关任务&#xff0c;你需要掌握&#xff1a;1.表格的结构及对应的标签&#xff0c;2.表格标签的属性。 表格的结构及对应的标签。一个基本的表格是…

Netty系列(二):Netty拆包/沾包问题的解决方案

上一篇说到Netty系列&#xff08;一&#xff09;&#xff1a;Springboot整合Netty&#xff0c;自定义协议实现&#xff0c;本文聊一些拆包/沾包问题。 拆包/沾包问题 TCP是面向字节流的协议&#xff0c;在发送方发送的若干包数据到接收方接收时&#xff0c;这些数据包可能会被…

Linux笔记

一。基础思想 一切皆文件。 两条权限原则&#xff1a; 权限分组原则权限最小原则 su是切换用户&#xff0c;而sudo则是用root权限执行某操作&#xff08; 普通用户sudo安全&#xff09; Linux目录 系统只存在一颗文件树、从/开始&#xff0c;所有的文件都挂载在这个节点上。…

JaCoCo增量覆盖率的基本实现原理

什么是增量覆盖率 如图所示&#xff0c;在master分支提交了HelloController&#xff0c;然后从master拉了个新分支test&#xff1b;提交了第1次代码&#xff0c;增加了WorldController&#xff1b;提交了第2次代码&#xff0c;增加了DonController。增量的获取方式有两种&#…

报表工具使用教程-FineReport决策报表导出Plus

前言 通过决策报表导出插件&#xff0c;用户可以将单张决策报表导出为 Excel &#xff0c;PDF&#xff0c;Word 格式文件。 那么用户如何将决策报表导出为 PPT 或 Image 格式文件呢&#xff1f;如何将多张决策报表合并导出至一个文件呢&#xff1f; 1.实现思路 用户通过安装…

静态时序分析简明教程(七)]端口延迟

端口延迟一、写在前面1.1 快速导航链接二、端口延迟2.1 输入有效2.2 输出有效2.3 set_input_delay2.3.1 -clock clock_name2.3.2 -clock_fall2.3.3 -level_sensitive2.3.4 -rise/fall2.3.5 min/max2.3.6 -add_delay2.3.7 时钟延迟2.4 set_output_delay三、总结一、写在前面 一…

点击化学FAM荧光素:6-FAM-alkyne,FAM alkyne 6-isomer,6-炔基-羧基荧光素

【中文名称】6-炔基-羧基荧光素 【英文名称】 FAM alkyne,6-isomer&#xff0c;6-FAM-alkyne 【CAS】478801-49-9 【分子式】C24H15NO6 【分子量】413.39 【纯度标准】95% 【包装规格】25mg&#xff0c;50mg&#xff0c;100mg 【是否接受定制】可进行定制&#xff0c;定制时间周…

Kubernetes安装可视化界面

安装可视化界面编写配置文件安装kubernetes-dashboard创建访问账号访问可视化界面dashboard是kubernetes官方提供的可视化界面。 https://github.com/kubernetes/dashboard编写配置文件 创建配置文件存放目录并切换到其中&#xff1a; mkdir /usr/local/kubernetes-dashboard…

java面试强基(10)

Exception 和 Error 有什么区别&#xff1f; 在 Java 中&#xff0c;所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类: Exception :程序本身可以处理的异常&#xff0c;可以通过 catch 来进行捕获。Exception 又可以分为 Checke…

Twitter网红账号营销,一定不能做的事

做社媒运营&#xff0c;我们都会创建一个官方账号与粉丝进行互动沟通&#xff0c;及时通知我们的新活动、产品&#xff0c;也是我们与粉丝建立联系的一个渠道方法。 推特群推王提示&#xff0c;虽然有这么多的好处&#xff0c;但是&#xff0c;也是有很多事项需要注意的&#…

服务器抓包简介

1、微服务服务器上抓包 2、在nginx服务器上抓包 1、服务器安装抓包软件 yum install -y tcpdump 2、服务器抓包命令 tcpdump -i any -s 0 -vvv -w /opt/qqgh.cap port 8080&#xff08;本服务器该服务的实际ip地址&#xff09; tcpdump -i eth0 host 10.30.224.170 -w result.…

14.函数的使用

函数的概念 函数是c语言的功能单位&#xff0c;实现一个功能可以封装成一个函数来实现。 定义函数的时候一切以功能为目的&#xff0c;根据功能去定函数的参数和返回值。 函数的分类 1.从定义角度分类&#xff08;即函数是谁实现的&#xff09; 库函数&#xff08;c库实现的…

Fedora怎么设置主菜单快捷键? Fedora快捷键的设置方法

Fedora主菜单可以设置打开快捷键&#xff0c;该怎么设置呢&#xff1f;下面我们就来看看Fedora快捷键的操作方法。 同时按【ALTF2】&#xff0c;输入gnome-terminal&#xff0c;打开终端。 单击右上角的主菜单按钮。 单击【配置文件首选项】。 单击【快捷键】。 单击【显示主菜…

使用DIV+CSS进行网页布局设计【HTML节日介绍网站——二十四节气】

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

【linux】进程概念

文章目录前言进程状态一、普遍的操作系统1、运行状态2、阻塞状态小结&#xff08;重要知识点&#xff09;3、新建/就绪状态4、挂起状态小结二、linux操作系统Linux内核源代码1、运行状态&#xff08;R&#xff09;2、&#xff08;浅度&#xff09;睡眠状态&#xff08;S&#x…