手写一个react,看透react运行机制
适合人群
本文适合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);//创建一个真实domconst { 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);//创建一个真实domconst { 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属性,分别表示
-
- 普通标签
-
- 函数组件标签
-
- 类组件标签
根据上述标记,我们可改造为:
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-> nodemount(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等概念或者源码分析,将在新文章汇总出。如对你有用,关注期待后续文章。
相关文章:

手写一个react,看透react运行机制
适合人群 本文适合0.5~3年的react开发人员的进阶。 讲讲废话: react的源码,的确是比vue的难度要深一些,本文也是针对初中级,本意让博友们了解整个react的执行过程。 写源码之前的必备知识点 JSX 首先我们需要了解什么是JSX。…...
JS判断输入值是否为正整数,判断变量是否为数字
这篇文章将讨论如何确定一个变量是否代表 JavaScript 中的有效数字。 1.JS中的test是原来是JS中检测字符串中是否存在的一种模式,JS输入值是否为判断正整数代码: <script type”text/javascript”> function test() { var num document.getElem…...

Android开发八股文,Android也有自己的八股文了
前言别的行业都有自己的八股文,凭什么Android没有。2023春招即将来临,很多同学会问 Android开发的面试题有必要背吗?我的回答是:很有必要。你可以讨厌这种模式,但你一定要去背,因为不背你就进不了大厂。国内…...

你需要同款“Unreal项目自动化编译、打包和部署”方案吗?
在过往几期的UWA Pipeline最佳实践案例中,我们分享了如何通过Pipeline实现性能优化、性能管理、游戏内容验收和云真机系统的应用(实现批量真机设备的自动化测试,以及针对特效性能优化的方式),其实这些高效的方法并不局…...

电子技术——CMOS-AB类输出阶
电子技术——CMOS-AB类输出阶 本节我们研究CMOS-AB类输出阶。 经典配置 下图展示了一个经典的CMOS-AB类输出阶: 这个很像BJT二极管偏置版本的AB类输出阶,在这里二极管偏置变成了 Q1Q_1Q1 和 Q2Q_2Q2 偏置。不想BJT的情况,这里 QNQ_NQN…...

2023王道考研数据结构笔记第二章线性表
第二章 线性表 2.1 线性表的定义 2.1.1 线性表的基本概念 线性表是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n0时线性表是一个空表。若用L命名线性表,则其一般表示为: L(a1,a2,...,ai,ai1,...,an)L(a_1…...

[chapter 11][NR Physical Layer][Layer Mapping]
前言:这里参考Curious Being系列 ,简单介绍一下NR 5G 物理层核心技术层映射.我们主要讲了一下what is layer Mapping, why need layer Mapping, how layer Mapping 参考文档:3GPP 38.211- 6.3.1.3 Layer mapping《5G NR Physical Layer | Cha…...

什么是工业物联网(IIoT)?
什么是工业物联网(IIoT)?工业物联网(IIoT) 被定义为一组设备和应用,允许大企业创建从核心到边缘的端到端连接环境。其还包括传统的物理基础设施,如集装箱和物流卡车,以收集数据,对事件做出反应,并在智能设备的帮助下做…...

「TCG 规范解读」PC 平台相关规范(4)
可信计算组织(Ttrusted Computing Group,TCG)是一个非盈利的工业标准组织,它的宗旨是加强在相异计算机平台上的计算环境的安全性。TCG于2003年春成立,并采纳了由可信计算平台联盟(the Trusted Computing Platform Alli…...

CSS背景属性之颜色渐变
颜色渐变 颜色渐变其实在网页设计中并不是特别常见, 但也不可避免的会出现导航栏是渐变色这种情况或者别的不是单一颜色的情况, 例如:这样的设计解决方案并不是只可以使用颜色渐变,我们可以使用两个div拼接,将文字放…...

IPv4地址细讲
文章目录一、IPv4地址简介二、IPv4地址的表示方法点分十进制记法三、IP地址的分类四、特殊IPv4地址:全 “0” 和全 “1”五、常用的三类IP地址使用范围六、五类IP地址的范围一、IPv4地址简介 IPv4地址分5类,每一类地址都由固定长度的字段组成࿱…...
sql语句中exists用法详解
文章目录一、语法说明exists:not exists:二、常用示例说明1.查询a表在b表中存在数据2.查询a表在b表中不存在数据3.查询时间最新记录4.exists替代distinct剔除重复数据总结一、语法说明 exists: 括号内子查询sql语句返回结果不为空ÿ…...

思迅软件端口不通导致软件和软锁报错的问题
一、端口不通导致软件和软锁报错的问题 问题说明:打开软件提示到:xxx.xxx.xxx.xxx失败! 处理步骤1: 假设软锁服务器IP为192.168.0.1,分别在服务器本机和客户端电脑测试软锁服务: 在服务器的浏览器中访问地址: http:/…...

Docker之路(7.DockerFile文件编写、DockerFile 指令解释、CMD与ENTRYPOINT的区别)
1.DockerFile介绍 dockerfile 是用来构建docker镜像的文件!命令参数脚本! 构建步骤: 编写一个dockerfile文件docker build构建成为一个镜像docker run 运行镜像docker push发布镜像(DockerHub、阿里云镜像仓库) 2.Dock…...

[软件测试]如何使用Eclipse导入项目并打开
🧑🎓个人介绍:大二软件生,现学JAVA、Linux、MySQL、算法 💻博客主页:渡过晚枫渡过晚枫 👓系列专栏:[编程神域 C语言],[java/初学者],[蓝桥杯] Ὅ…...
emplace_back与push_back异同
vector的emplace_back与push_back 文章目录vector的emplace_back与push_back前言1.区别总览2.push_back支持右值引用不支持传入多个构造参数总是会进行拷贝构造3.emplace_backemplace_back可以接受多个构造参数支持原地构造前言 在vector中,通过push_back与emplace_…...

【C语言航路】第十五站:程序环境和预处理
目录 一、程序的翻译环境和执行环境 二、编译和链接 1.翻译环境 2.编译本身也分为几个阶段 3.运行环境 三、预处理 1.预定义符号 2.#define 1.#define定义标识符 2.#define定义宏 3.#define 替换规则 4.#和## 5.带副作用的宏参数 6.宏和函数的对比 7.命名约定 …...

Vue3 - 获取 Proxy 对象代理中包裹的 “真实数据“,解决对象或数组打印后是 Proxy 对象无法拿到原始数据的问题(提供 2 种详细解决方案)
前言 在 Vue3 中很多数据都被 Proxy 代理对象 “包裹”(无法拿到其真正的原始数据),另外就是请求回来的数据,例如通过 res.data.data 拿到了一个数组对象格式的数据。但是这个数据被 Proxy 包裹,你根本拿不到值无法进行处理。 本文实现了 Vue3 取到被 proxy 对象包裹的原始…...

ESP32设备驱动-ML8511紫外线传感器驱动
ML8511紫外线传感器驱动 1、ML8511介绍 ML8511 是一款紫外线传感器,适用于室内或室外获取紫外线强度。 ML8511 配备了一个内部放大器,可根据紫外线强度将光电流转换为电压。 这种独特的功能提供了与 ADC 等外部电路的简单接口。 在掉电模式下,典型的待机电流为 0.1 μ \mu…...

SC12B触摸感应芯片评测方案(1)
MM32F0160SC12B Touch Application Evaluation 文章目录MM32F0160SC12B Touch Application EvaluationIntroduction & RequirementHardwareSC12B & SC12B Sample Demo boardMini-F0160 boardSoftwareMCU Software - MM32F0160PC Tool - FreeMASTERSummaryIntroduction …...

19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...

关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...

【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...