当前位置: 首页 > article >正文

实现 Toy-React , 实现 JSX 渲染

一、简介

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

二、准备工作

目录结构

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

image.png

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.kler.cn/a/395773.html

相关文章:

  • 谷歌Gemini发布iOS版App,live语音聊天免费用!
  • 使用 Python 和 OpenCV 实现摄像头人脸检测并截图
  • 【算法】二分查找
  • 2、 家庭网络发展现状
  • 大模型在蓝鲸运维体系应用——蓝鲸运维开发智能助手
  • 修改数据库和表的字符集
  • 通过css的哪些方式可以实现隐藏页面上的元素?
  • spark的学习-05
  • Java中的集合类与线程安全的讨论
  • ETLCloud支持的数据处理类型包括哪些?
  • ubuntu docker里面安装Omniverse Launcher不能登陆
  • 【Elasticsearch】01-ES安装
  • node对接ChatGpt的流式输出的配置
  • Apache Doris:深度优化与最佳实践
  • Dev C++ 无法使用to_string方法的解决
  • shell编程(2)永久环境变量和字符串显位
  • 利用云计算实现高效的数据备份与恢复策略
  • 使用 DBSCAN(基于密度的聚类算法) 对二维数据进行聚类分析
  • Spring基础之——控制反转(IOC)、依赖注入(DI)与切面编程(AOP)概念详解(适合小白,初学者必看)
  • 问:数据库的六种锁机制实践总结?
  • C语言,用最小二乘法实现一个回归模型
  • (附项目源码)Java开发语言,211 springboot 在线问诊系统的设计与实现,计算机毕设程序开发+文案(LW+PPT)
  • 谷歌Gemini发布iOS版App,live语音聊天免费用!
  • 基于微信小程序的乡村研学游平台设计与实现,LW+源码+讲解
  • 科锐国际,蓝禾,汤臣倍健,三七互娱,GE医疗,得物,顺丰,快手,途游游戏25秋招内推
  • 14天Java基础学习——第6天:面向对象编程(类与对象)