实现 Toy-React , 实现 JSX 渲染

devtools/2024/11/16 20:45:31/

一、简介

JSX 是属于 React 中的一大特性,因此,本文将实现自定义 JSX 渲染功能,同时也会实现部分 React 中拥有的功能,以便加深理解.

二、准备工作

目录结构

目录结构比较简单,就不详细说明了

image.png

webpack__8">webpack 配置

  • 由于我们需要在 .js 或者 .jsx 文件中编写 jsx 语法,同时,也为了我们可以使用一些 js 新特性,因此需要通过 webpack 中的 loader 配置进行编译.
  • 这里我们需要用到的 loader 如下:
    • babel-loader
    • @babel/core
    • @babel/preset-env:js 转换为运行环境能识别的语法
    • @babel/plugin-transform-react-jsx:JSX 语法转换为对应内容的输出结果
  • 为了避免多次手动执行 webpack 编译命令,这里是使用了 webpack-dev-server 来监听文件变化,自动执行编译命令

image.png

  • 配置文件内容如下
const path =  require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = {mode: "development",entry: {main: "./main.jsx",},module: {rules: [{test: /\.jsx$/,use: {loader: "babel-loader",options: {presets: ["@babel/preset-env"],plugins: [["@babel/plugin-transform-react-jsx",{ pragma: "createElement" },],],},},exclude: /node_modules/,},],},output: {path: path.resolve(__dirname, 'dist'),filename: '[name].js',},plugins: [new HtmlWebpackPlugin({title: "My App",template: "public/index.html",}),],optimization: {minimize: false,},
};

三、编写 JSX

1. 首先在 main.jsx 中编写一段简单的 JSX 内容

image.png

2. 观察被编译的结果

image.png

  • 从以上结果可以看到,最终 JSX 语法被 @babel/plugin-transform-react-jsx 被编译成了 React.createElement 方法,由此可见,要实现 JSX 渲染的关键就是要实现 createElement
  • 这里我们要调整一下编译后的结果,我们需要 jsx 被编译为我们自定义的 createElement 方法,而不是 React.createElement,因此我们修改 webpack 配置文件中与 “@babel/plugin-transform-react-jsx” 相关的配置为
module: {rules: [{test: /\.jsx$/,use: {loader: "babel-loader",options: {presets: ["@babel/preset-env"],plugins: [["@babel/plugin-transform-react-jsx",{ pragma: "createElement" }, // 这里就是控制 jsx 语法被编译后要调用的方法名],],},},exclude: /node_modules/,},],

3. 自定义实现 createElement 方法

从编译后的结果来看 createElement 方法具有三个参数:

  • type —— 当前元素的类型:HTML标签名、Class 组件、Function 组件
  • attributes —— 当前元素上的拥有的属性:{ } || null
  • children —— 除了前两个参数,默认后面的参数全部为当前元素的子节点:[ ]
function createElement(type, attributes,...children){// 创建 dom 实例const currentElement = document.createElement(type);// 处理属性if(attributes){for (const name in attributes) {currentElement.setAttribute(name, attributes[name]);}}// 处理子节点if(children.length){for (let child of children) {// 处理文本节点if(typeof child === "string"){child = document.createTextNode(child);}currentElement.appendChild(child);}}return currentElement;
}const JSX = (<div class="jsx"><h1>i am Jsx</h1>
</div>);document.body.appendChild(JSX);

到这里,现在已经可以将简单的 JSX 渲染成了视图

image.png

四、升级改造 createElement

  1. 虽然现在我们已经可以渲染简单的 JSX 内容了,但是如果要渲染 Class 组件或者 Function 组件的话,createElement 方法明显还无法做到,于是我们需要对其进行升级改造.
  2. 同样,我们先观察如果使用 Class 组件,那么最终会被编译为什么呢?
class MyComponent {render() {return (<div><h1>i am MyComponent</h1></div>);}
}const JSX = (<div id="jsx"><h1>i am Jsx</h1><MyComponent id="MyComponent"><h1>i am MyComponent child</h1></MyComponent></div>
);

image.png

  1. 可以看到 createElement 的第一个参数已经不再是 string ,而是我们定义的 Class 类,于是可以进行第一步改造,根据 type 进行对应的处理
function createElement(type, attributes, ...children) {let currentElement;if (typeof type === "string") {// 创建 dom 实例currentElement = document.createElement(type);}else {// 获取对应的 dom 实例currentElement = new type().render();}// 处理属性if (attributes) {for (const name in attributes) {currentElement.setAttribute(name, attributes[name]);}}// 处理子节点if (children.length) {for (let child of children) {// 处理文本节点if (typeof child === "string") {child = document.createTextNode(child);}// 往当前元素中插入子节点currentElement.appendChild(child);}}return currentElement;
}

这样一来,我们就可以成功渲染 Class 组件

image.png

五、抽离逻辑实现 Toy-React

尽管上面我们实现了对 JSX 的渲染,但所有操作都在 main.jsx 中进行,包括 createElement 方法也是直接在该文件中声明和实现的,既然我们要实现 Toy-React , 那么我们应该要保证其在使用上要和 React 保持一致.

  • 1. createElement 中要实现的功能有:
    • 获取或创建 dom 实例
    • 为 dom 实例设置 attribute
    • 创建文本节点
    • 为 dom 实例添加子节点
    • 返回最终的 dom 实例
    1. 为了让 createElement 中所有的 type 都能拥有正常调用 DOM API 的能力,我们需要给所有的 type 定义一个通用 ElmentWrapper,同时也为文本节点定义一个对应的 TextWrapper.
    1. 同样的,为了让所有的 Class 组件拥有共同的一些功能特性,我们需要实现 Component 这个类,来保证所有 Class 组件拥有统一性
    1. main.jsx 中最后是通过 document.body.appendChild(JSX) 的方式,把 JSX 转换后的结果最终渲染在页面上的,因此,在这里我们要实现 render 方法去替换这种方式.

toy-react.js 最终实现如下:

// ElementWrapper
class ElementWrapper {constructor(type) {this.root = document.createElement(type);}setAttribute(name, value) {this.root.setAttribute(name, value);}appendChild(component) {this.root.appendChild(component.root);}
}// TextWrapper
class TextWrapper {constructor(content) {this.root = document.createTextNode(content);}
}// Component
export class Component {constructor() {this._root = null;this.props = {};this.children = [];}setAttribute(name, value) {this.props[name] = value;}appendChild(component) {this.children.push(component);}get root() {if (!this._root) {this._root = this.render().root;}return this._root;}
}// createElement
export function createElement(type, attributes, ...children) {// 1. 获取 dom 实例let currentElement;if (typeof type === "string") {currentElement = new ElementWrapper(type);} else {currentElement = new type();}// 2. 处理 dom 实例属性if (attributes) {for (const name in attributes) {currentElement.setAttribute(name, attributes[name]);}}// 3. 处理子节点const insertChildren = (children) => {if (children.length) {for (let child of children) {// 处理文本节点if (typeof child === "string") {child = new TextWrapper(child);}// 当子节点拥有子节点时,递归处理// 即在组件中使用了 { this.children } 表达式if (typeof child === "object" && child instanceof Array) {insertChildren(child);} else {currentElement.appendChild(child);}}}};// 初始化调用insertChildren(children);return currentElement;
}// render
export function render(component, parentElement) {parentElement.appendChild(component.root);
}

在 main.jsx 中使用如下:

import { createElement, render, Component } from './toy-react'; class MyComponent extends Component {render() {return (<div id="MyComponent"><h1>i am MyComponent</h1>{ this.children }</div>);}
}const JSX = (<div id="jsx"><h1>i am Jsx</h1><MyComponent><h1>i am MyComponent child</h1></MyComponent></div>
);render(JSX, document.querySelector("#app"));

渲染结果

image.png


http://www.ppmy.cn/devtools/134517.html

相关文章

ESP32-C3 开发笔记 之 arduino 正常上传 串口乱码2024/11/15

ESP32-C3 开发笔记 之 arduino 正常上传 串口乱码 ESP32-C3 开发笔记 之 arduino 正常上传程序 但是打开串口,串口快速刷新 芯片一直处于重启状态 找了很久的原因没找到,用Mixly 上传就正常 最后看到这篇 文章https://blog.csdn.net/luooove/article/details/132351398修改了Fl…

2024 CCF中国开源大会“开源科学计算与系统建模openSCS”分论坛成功举办

11月9日&#xff0c;2024 中国计算机学会&#xff08;CCF&#xff09;中国开源大会“开源科学计算与系统建模openSCS”分论坛在深圳落下帷幕。本次论坛由开源科学计算与系统建模工作委员会、苏州同元软控信息技术有限公司&#xff08;简称“同元软控”&#xff09;、深圳景元数…

ffmpeg编程入门

文章目录 ffmpeg流程常用的音视频术语常用概念复用器编解码器ffmpeg的整体结构注册组件相关封装格式相关函数的调用流程 相关的ffpmeg数据结构简介数据结构之间的关系 ffmpeg流程 图中的函数 以及结构体都是ffmpeg自带提供的 ffmpeg打开的时候 和其他io操作差不多 有一个类似句…

智能科技赋能金融决策:中阳科技的数据分析解决方案

在金融市场中&#xff0c;智能科技的崛起为投资策略提供了更全面的支持。中阳科技通过先进的数据分析技术和精准的算法&#xff0c;帮助投资者在充满变数的市场中做出更理性的决策。本文将探讨中阳科技如何通过数据驱动的方式帮助客户应对市场挑战&#xff0c;实现稳健的资产增…

Java集合(Collection+Map)

Java集合&#xff08;CollectionMap&#xff09; 为什么要使用集合&#xff1f;泛型 <>集合框架单列集合CollectionCollection遍历方式List&#xff1a;有序、可重复、有索引ArrayListLinkedListVector&#xff08;已经淘汰&#xff0c;不会再用&#xff09; Set&#xf…

layui.all.js:2 Uncaught Error: Syntax error, unrecognized expression

报错内容&#xff1a; layui.all.js:2 Uncaught Error: Syntax error, unrecognized expression: input[name"image1UploadTime"language] 错误代码&#xff1a; $(input[name"imagejUploadTime"language]).val(currentDateTime); 因为 language 是个变…

工作时发现自己手写SQL能力很低,特此再来学习一遍SQL

SQL语法 ①常用的数据库本身的操作 # 显示数据库列表 show databases;# 使用某个数据库 use twbpm_dev;# 创建一个数据库 create database db_test;# 删除一个数据库 drop database if exists db_test;# 显示数据库中所有的表 show tables;# 查看MySQL的版本 select version();…

docker使用,docker图形化界面+docker详细命令

DockerUI进入 docker container run --rm --name docker.ui -v /var/run/docker.sock:/var/run/docker.sock -p 8999:8999 joinsunsoft/docker.ui访问8999端口就行&#xff0c;就可以图形化管理Docker了 常规使用 搭建 sudo docker-compose build #有一些需要这条命令 su…