【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呢?这就引入了babelbabel 是一个工具链,主要用于将 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前端实验室 🛠️「精选资源|实战经验|技术洞见」

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6415 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js