【react原理实践】使用babel手搓探索下jsx的原理

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

本系列我们将探寻react的实现原理,实现一些小demo,让我们能够更直观的理解原理,最终实现一个mini react。

🥑 你能学到什么?

希望在你阅读本篇文章之后,不会觉得浪费了时间。如果你跟着读下来,你将会学到:

  • jsx语法、基本使用、方法集
  • jsx是如何转化为DOM的
  • jsx的转换流程
  • jsx为什么被提到了运行时
  • ReactElement的数据结构
  • 源码createElement解析
  • 源码ReactElement解析

🍑 一、jsx的几大问题你都知道吗?

  • JSX的本质是什么?他和js的关系是什么?
  • JSX 语法是如何在 js 中生效的?
  • 既然jsx最终会转换为React.createElement,我们为啥不直接用React.createElement
  • JSX是如何映射为DOM的,转换流程是什么样的?【转换为DOM我们后续单开一节介绍】

1.JSX的本质是什么?他和js的关系是什么?

JSX是js 的一种语法扩展,它和模板语言很接近,但是它充分具备js 的能力,也就是说你可以把jsx放在各种运算中,或者是函数参数中,甚至在函数中返回。

本质是JS的扩展,是生成createElement的语法糖,通过调用createElement去处理key,ref,等等,调用ReactElement函数去生成ReactElement

2.一句话总结createElement的作用

createElement仅仅是格式化数据 ,让数据符合ReactElement函数的入参【看完文章的后续,你会豁然开朗】

3.JSX 语法是如何在 js 中生效的?

都说jsx会被编译成React.createElement,那这个编译过程是如何做的?谁做的,为什么浏览器不直接支持他?

从jsx的本质上来看,它的定位是js的语法扩展,都不是语法标准,那谁会支持他?

那我们如何编译jsx呢?这就引入了babel,babel 是一个工具链,主要用于将 es6+版本的代码转换为向后兼容的 js 语法,以便能够运行在当前和旧饭本的浏览器或其他环境中,他也可以做一些其他的语法转换之类的,比如ts转js,jsx转js等等,后续我们会有一个篇文章专门介绍babel

4.既然jsx最终会转换为React.createElement,我们为啥不直接用React.createElement

一张图就能看明白,就是为了开发体验,如果嵌套很深,React.createElement能写吐我们,我们去babel转换一下,之前搭建的项目里面的app.tsx,代码如下 记得选择这个,后续的react版本中jsx已经被提到了运行时中

tsx 复制代码
import React from 'react';
import img from '../javascript.png';
import './app.scss';
import {Button} from '@components/Button/Button';
function App() {
	return (
		<>
			<Button label='Button' onClick={() => {}} primary />
			<div className='App'>React18 + Ts5 + webpack5 开发模板搭建</div>;
			<img src={img}></img>
		</>
	);
}

export default App;

5.jsx被提到运行时的原因

在React 17及以后的版本中,引入了一项新的JSX转换方式,称为"新的JSX转换"(New JSX Transform)。主要就是将JSX的转换逻辑从编译时(通过Babel插件转换)更多地移动到运行时。原因如下:

  1. 减少import React的需求 :在旧的转换方式中,每个使用JSX的文件都需要在文件顶部导入React(import React from 'react';),因为转换后的代码需要React.createElement函数。新的JSX转换方式使得这种导入变得不必要,因为转换后的代码不再依赖于React.createElement。相反,它可以直接使用从React 17开始引入的新入口来创建JSX元素。
  2. 自动导入 :新的转换方式意味着Babel(或其他编译工具)会自动插入所需的JSX创建函数的导入声明。这可以让代码看起来更简洁,避免了冗余的import语句,并且降低了新手可能遇到的"忘记导入React"的问题。
  3. 更好的打包和树摇(Tree-shaking) :通过将JSX创建函数的处理推迟到运行时,React库可以更好地支持模块化和树摇优化。这意味着,如果你的应用中有些React特性没有被用到,那些代码可以在最终打包时被更有效地排除掉,减少应用的体积。
  4. 为将来的优化打下基础:新的JSX转换方式还允许React团队在不破坏现有应用的情况下引入新特性和性能优化。通过在运行时而不是编译时处理JSX,React可以更灵活地更新和优化元素的创建逻辑。

在旧的版本中,转换结果依赖于React全局变量的存在(因此需要导入React)。转换完成的代码在运行时不需要执行额外的转换,直接被执行即可。

新版中,提到运行时不是说在react运行的时候才去编译jsx,而是将转换为React.createElement的代码封装到模块中导出,babel会将这些模块自动导入,然后将我们的jsx转换为模块中的导出函数,这个过程不需要我们关注,导出的函数也不是依赖于React全局变量的,这样自然不再需要我们去手动引入React,具体我们看官网的转换

、

🥑 二、用babel手搓模拟一下jsx的转换

1.简单介绍下

在babel中,将JSX转为js使用的是一个插件预设@babel/preset-react,主要是以下几个插件:

  1. @babel/plugin-syntax-jsx: 仅允许Babel解析(但不转换)JSX语法。
  2. @babel/plugin-transform-react-jsx : 转换JSX为React.createElement调用。这是实现JSX的主要插件。
  3. @babel/plugin-transform-react-display-name : 自动添加displayName属性到React组件,有助于调试。
  4. @babel/plugin-transform-react-jsx-source (开发模式下可用): 添加源文件和行号信息到JSX元素,增强调试能力。
  5. @babel/plugin-transform-react-jsx-self (开发模式下可用): 添加对this的引用到JSX元素,以便在React的开发者工具中使用。

2.手搓模拟

编写test.js文件

js 复制代码
import React from 'react';
import img from '../javascript.png';
import './app.scss';
import {Button} from '@components/Button/Button';
function App() {
	return (
		<>
			<Button label='Button' onClick={() => {}} primary />
			<div className='App'>React18 + Ts5 + webpack5 开发模板搭建</div>;
			<img src={img}></img>
		</>
	);
}

export default App;

2.编写转换代码jsx.js

就是读取文件,然后调用babel插件模拟转换一下。

js 复制代码
const fs = require('fs');

const babel = require('@babel/core');

fs.readFile('./test.js', (e, data) => {
	// 读取数据
	const code = data.toString('utf-8');

	// 转换jsx文件
	const result = babel.transformSync(code, {
		plugins: ['@babel/plugin-transform-react-jsx'],
	});
	// 写入文件
	fs.writeFile('./result.js', result.code, function () {});
});

3.转换结果result.js

js 复制代码
import React from 'react';
import img from '../javascript.png';
import './app.scss';
import { Button } from '@components/Button/Button';
function App() {
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Button, {
    label: "Button",
    onClick: () => {},
    primary: true
  }), /*#__PURE__*/React.createElement("div", {
    className: "App"
  }, "React18 + Ts5 + webpack5 \u5F00\u53D1\u6A21\u677F\u642D\u5EFA"), ";", /*#__PURE__*/React.createElement("img", {
    src: img
  }));
}
export default App;

🍉 三、源码探究

1.createElement

createElement仅仅是做格式化数据 ,让数据符合ReactElement函数的入参

  • 处理ref、key、self、source等
  • 处理children
  • 将属性提取到props中
js 复制代码
/**
 * createElement
 */
export function createElement(type, config, children) {
  // propName 变量用于储存后面需要用到的元素属性
  let propName;
  // props 变量用于储存元素属性的键值对集合
  const props = {};
  // key、ref、self、source reac属性,key、ref应该很熟悉了吧
  let key = null;
  let ref = null;
  let self = null;
  let source = null;
  // config 对象中存储的是元素的属性
  if (config != null) {
    // ref赋值
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    // 此处将 key 值字符串化
    if (hasValidKey(config)) {
      key = "" + config.key;
    }
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 属性提取到props中去
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }
  // 处理children
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 处理 defaultProps
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  );
}

2.ReactElement

$$typeof眼熟吗?我们打印下之前搭建的项目里面的app.tsx的返回值,控制台查看一下,这不就是jsx返回的一个一个ReactElement吗?react就是根据这个去构建fiber树的,fiber我们后续会说的。

js 复制代码
const ReactElement = function (type, key, ref, self, source, owner, props) {
  element = {
    // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  //
  if (__DEV__) {
    // 一些调试逻辑
  }

  return element;
};

🍎 推荐阅读

工程化系列

本系列是一个从0到1的实现过程,如果您有耐心跟着实现,您可以实现一个完整的react18 + ts5 + webpack5 + 代码质量&代码风格检测&自动修复 + storybook8 + rollup + git action实现的一个完整的组件库模板项目。如果您不打算自己配置,也可以直接clone组件库仓库切换到rollup_comp分支即是完整的项目,当前实现已经足够个人使用,后续我们会新增webpack5优化、按需加载组件、实现一些常见的组件封装:包括但不限于拖拽排序、瀑布流、穿梭框、弹窗等

面试手写系列

其他

🍋 写在最后

如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!

感兴趣的同学可以关注下我的公众号ObjectX前端实验室

🌟 少走弯路 | ObjectX前端实验室 🛠️「精选资源|实战经验|技术洞见」

相关推荐
环境栈笔记8 分钟前
指纹浏览器检查代理失败怎么排查?先分清代理、网络和检测服务
前端·人工智能·后端·自动化
ShineWinsu13 分钟前
对于HTML:HTML常见标签的解析—下
前端·css·面试·html·标签·表单·emmet
恋猫de小郭14 分钟前
Kotlin 2.5 的 Companion 改造,对 KMP 的重要升级
android·前端·flutter
前端双越老师27 分钟前
程序员 1v1 访谈系列第二期:很焦虑但又不知道干啥
前端·ai编程·全栈
广州华水科技41 分钟前
北斗形变监测传感器在大坝安全监测中的应用与优势分析
前端
孔令飞1 小时前
如何学习Go
前端
HUIBUR科技1 小时前
AI 中枢,面向企业场景构建的 AI 智能体
前端·数据库·人工智能
怕浪猫1 小时前
给AI配一台电脑,开源版Muse来了,OpenMuse赶紧去二次开发吧
javascript·面试·github
随便写个软件1 小时前
【新版本】EasyExplorer资源管理器 3.12.0——地址栏重要更新
运维·服务器·前端·everything·资源管理器
Jack N1 小时前
【Strapi 系列之二】Collection / Single / Component 怎么选?Strapi 字段大全一篇讲透
前端·javascript·strapi