草庐IT

「深入浅出」实现JSX的转换

时光屋小豪 2023-03-28 原文

前言

由于近期在看React框架源码、底层实现方面的知识,所以想把学习心得整理出来。

这也是一个新的系列「从0实现React 18核心模块」的第一篇。

接下来还会更新:render、commit阶段的实现,以及Hooks架构、useState、useEffect、单双节点Diff的过程还有React 18中的并发更新原理。

在看文章之前,我们可以先想几个问题:

  • JSX 是什么语法?
  • JSX 有什么优势,它的转换规则是什么或者它内部是如何实现的?
  • 既然 React 一直在使用 JSX,那它的实现被写应该写在哪个包里(比如react、react-dom,react-reconciler)?
  • 在React 17之前和React 17之后,JSX转换的方法实现有哪些异同?
  • 如何实现React.createElement方法和运行时的 jsx 方法?
  • 写一个Demo引入自己实现的jsx方法,看看运行结果

下文提到的 big-react 是从0到1实现的React的核心功能模块原理的项目

如果自己实现一个 React 框架,它需要包含哪些内置的包:

  • react包是 React 的核心库,提供了创建和管理组件所需的基本功能(比如组件创建、组件生命周期管理、虚拟DOM以及Hooks等),主要是一些和宿主环境无关的方法。
  • react-reconciler包实现了 React 的 reconciliation 协调算法,是一种核心优化策略的实现,主要自定义协调器的实现。以及在不同的平台或环境中使用 React。
  • shared包是big-react公用的辅助方法,和宿主环境无关。
如果还有一个必要的包,那就是react-dom:

  • react-dom:这个包提供了将 React 与 DOM(浏览器环境)集成的方法。它包含了用于将 React 组件渲染到 DOM 中的 ReactDOM.render() 函数,以及其他与浏览器环境相关的实用功能。对于在浏览器中运行的 React 应用程序,react-dom 是必需的。

react与react-reconciler包是什么

react包为我们提供了什么

当我们在项目中使用 React 构建界面时,主要使用的就是 react​ 包。它提供了开发者需要的所有API。如React.Component、React.createElement、React.useState等等,所以它也是大多数 React 项目的基础。

react-reconciler包实现了什么​

react-reconciler包是一个更底层、更高级的库,它实现了reconciliation协调算法,reconciliation是 React 的一种核心优化策略,用于在更新组件时比较虚拟DOM树的差异,并将实际更改应用到实际的DOM树。这有助于提高性能,因为避免了不必要的DOM操作。

它主要用于创建自定义渲染器,以及在不同的平台中去使用 React。例如,react-dom(用于Web平台)和react-native(用于移动应用)都使用react-reconciler作为底层库,实现了针对各自平台的渲染逻辑。

JSX 是什么

const element = <div className="container">Hello, world!</div>;
在React中,JSX是一种JavaScript语法扩展,允许你在JavaScript代码中编写类似HTML的标记。要使用JSX,需要在构建过程中将其转换为标准的JavaScript代码。

通常,这个转换过程包括两个主要部分:

  • 编译时:通常指将 JSX 语法转换为浏览器可以理解的普通 JavaScript 代码的过程,这个过程通常由 Babel 完成。
  • 构建时:在将JSX语法转换为标准的JavaScript代码后,通常会使用构建和打包工具(如Webpack、Rollup)对代码进行优化、压缩和打包。打包工具将源代码和依赖项组合成一个或多个文件(“bundles”或“chunks”),用于在浏览器中运行。
  • 运行时:React会根据编译后的代码创建虚拟DOM树,然后将其渲染到实际的DOM中。还会发生的阶段有状态管理和更新、事件处理和Diff算法的比较等。

JSX 被 Babel 编译成了什么

在React 17之前,JSX语法会被编译成React.createElement函数的调用,用来创建虚拟DOM元素。

转换结果如下:

const element = React.createElement(
"div",
{ className: "container" },
"Hello, world!"
);
从React 17开始,引入了新的JSX转换功能,称为"Runtime Automatic"(自动运行时)。这意味着在使用JSX语法时,不再需要手动引入React库。在自动运行时模式下,JSX会被转换成新的入口函数,import {jsx as _jsx} from 'react/jsx-runtime'; 和 import {jsxs as _jsxs} from 'react/jsx-runtime';。

转换结果如下:

import { jsx as _jsx } from "react/jsx-runtime";

const element = _jsx("div", {
className: "container",
children: "Hello, world!"
});
接下来我们就来实现jsx方法或React.createElement方法(包括dev、prod两个环境)。

工作量包括:

  • 实现jsx方法
  • 实现打包流程
  • 实现调试打包结果的环境

实现 jsx 转换方法

jsx 转换方法包括:

  • React.createElement方法
  • jsxDEV方法(dev环境)
  • jsx方法(prod环境)

实现React.createElement

在React 17之前,JSX转换应用的是createElement方法,下面是它的实现:

/**
*
* @param type 元素类型
* @param config 元素属性,包括key,不包括子元素children
* @param maybeChildren 子元素children
* @returns 返回一个ReactElement
*/
const createElement = (
type: ElementType,
config: any,
...maybeChildren: any
) => {
// reactElement 自身的属性
let key: Key = null;
let ref: Ref = null;

// 创建一个空对象props,用于存储属性
const props: Props = {};

// 遍历config对象,将ref、key这些ReactElement内部使用的属性提取出来,不应该被传递下去
for (const prop in config) {
const val = config[prop];
if (prop === 'key') {
if (val !== undefined) {
key = '' + val;
}
continue;
}
if (prop === 'ref') {
if (val !== undefined) {
ref = val;
}
continue;
}
// 去除config原型链上的属性,只要自身
// 一般使用{...props}将所有属性都传递下去,所以摘除ref、key属性外需要被保存到props中
if ({}.hasOwnProperty.call(config, prop)) {
props[prop] = val;
}
}

const maybeChildrenLength = maybeChildren.length;
if (maybeChildrenLength) {
// [child] [child, child, child]
if (maybeChildrenLength === 1) {
props.children = maybeChildren[0];
} else {
props.children = maybeChildren;
}
}

return ReactElement(type, key, ref, props);
};
注意:React.createElement方法和jsx方法的区别这里只体现在第三个参数上。

实现jsx方法

从React 17之后,JSX转换应用的是jsx方法,下面是它的实现:

/**
*
* @param type 元素类型
* @param config 元素属性
* @param maybeKey 可能的key值
* @returns 返回一个ReactElement
*/
const jsx = (type: ElementType, config: any, maybeKey: any) => {
// 初始化key和ref为空
let key = null;
let ref = null;

// 创建一个空对象props,用于存储属性
const props: Props = {};

// 遍历config对象,将ref、key这些ReactElement内部使用的属性提取出来,不应该被传递下去
for (const prop in config) {
const val = config[prop];
if (prop === "key") {
continue;
}
if (prop === "ref") {
if (val !== undefined) {
ref = val;
}
continue;
}
// 一般使用{...props}将所有属性都传递下去,所以摘除ref、key属性外需要被保存到props中
if ({}.hasOwnProperty.call(config, prop)) {
props[prop] = val;
}
}

// 将 maybeKey 添加到 key 中
if (maybeKey !== undefined) {
key = "" + maybeKey;
}

return ReactElement(type, key, ref, props);
};
这段代码定义了一个jsx函数,主要用于创建React元素。首先,它会提取可能存在的key和ref属性,并将剩余属性添加到一个新的props对象中。最后用ReactElement函数创建一个React元素并返回。

从上面代码中可以看到还实现了ReactElement方法:

// jsx-runtime.js
const supportSymbol = typeof Symbol === 'function' && Symbol.for;

// 为了不滥用 React.elemen,所以为它创建一个单独的键
// 为React.element元素创建一个 symbol 并放入到 symbol 注册表中
export const REACT_ELEMENT_TYPE = supportSymbol
? Symbol.for('react.element')
: 0xeac7;

export const ReactElement = function (type, key, ref, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref,
props,
_mark: 'lsh',
};
return element;
};

export const jsx =...

用自己实现的的jsx接入Demo

我们试着把自己实现的jsx方法,创建一个ReactElement,看它是否能够渲染在页面上。

实现jsx方法

jsx-Demo运行地址

jsx方法和createElement的区别

jsx函数和createElement函数都用于在React中创建虚拟DOM元素,但它们的语法和用法有所不同。jsx函数来自于React 17及更高版本中的新的JSX转换功能,称为"Runtime Automatic"。

以下是两者之间的主要区别:

  1. 语法和转换方式:jsx函数用于处理新的JSX转换方式,其语法更简洁。createElement函数用于处理传统的JSX转换方式。
例如,一个JSX元素:

const element = <div className="container">Hello, world!</div>;
使用createElement转换后的代码如下:

const element = React.createElement(
"div",
{ className: "container" },
"Hello, world!"
);
使用jsx函数(自动运行时)转换后的代码如下:

import { jsx as _jsx } from "react/jsx-runtime";

const element = _jsx("div", { className: "container", children: "Hello, world!" });
  1. ​子元素和key值处理:jsx函数将子元素作为属性(children)传递,而createElement函数将子元素作为额外的参数传递。同时子元素上的key值在jsx函数中也会以第三个参数的形式传递,而在createElement函数中,则是存在于config第二个参数中。
在createElement函数中:

React.createElement("div", {className: "app", key: "appKey"}, "hello,app");
在jsx函数中:

import { jsx as _jsx } from "react/jsx-runtime";

_jsx("div", {className: "app", children: "hello,app"}, "appKey");
  1. ​兼容性和版本:createElement函数在所有React版本中可用,而jsx函数仅在React 17及更高版本中提供。尽管React团队推荐使用新的JSX转换方式,但许多现有项目可能仍在使用createElement函数。
这时可能产生两个疑问:

  • 从React 17之后使用Runtime Automatic自动运行时有什么好处?
  1. 简化组件代码:不再需要在每个组件文件顶部添加**import React from 'react';**。这使得组件代码更简洁,更易于阅读和维护。
  2. 节省包大小:由于不再需要导入整个React对象,构建工具可以更好地优化输出代码,从而减小输出包的大小。
  • 改成jsx函数后,为什么要把key属性单独拿出来放在第三个参数?
在之前的React版本中,每当创建一个新的React元素时,React都需要从属性对象中提取key​和ref,这会导致额外的性能开销。

将key​作为单独的参数传递,可以让React在处理虚拟DOM树时更容易地访问key,无需每次都从属性对象中查找。这有助于提高React的性能和效率,特别是在处理大量元素和复杂组件树时。

实现打包流程

打包流程稍微有些复杂,后续写到文章里。

简单来说就是使用 Rollup,将编写jsx方法的文件打包出来,通过pnpm link --global的方式生成一个全局的react包,这样就可以通过pnpm link react --global调试自己创建的 create-react-app demo项目了。

构建react包思路

有关「深入浅出」实现JSX的转换的更多相关文章

  1. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  2. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  3. ruby - 将数组的内容转换为 int - 2

    我需要读入一个包含数字列表的文件。此代码读取文件并将其放入二维数组中。现在我需要获取数组中所有数字的平均值,但我需要将数组的内容更改为int。有什么想法可以将to_i方法放在哪里吗?ClassTerraindefinitializefile_name@input=IO.readlines(file_name)#readinfile@size=@input[0].to_i@land=[@size]x=1whilex 最佳答案 只需将数组映射为整数:@land边注如果你想得到一条线的平均值,你可以这样做:values=@input[x]

  4. ruby - 将散列转换为嵌套散列 - 2

    这道题是thisquestion的逆题.给定一个散列,每个键都有一个数组,例如{[:a,:b,:c]=>1,[:a,:b,:d]=>2,[:a,:e]=>3,[:f]=>4,}将其转换为嵌套哈希的最佳方法是什么{:a=>{:b=>{:c=>1,:d=>2},:e=>3,},:f=>4,} 最佳答案 这是一个迭代的解决方案,递归的解决方案留给读者作为练习:defconvert(h={})ret={}h.eachdo|k,v|node=retk[0..-2].each{|x|node[x]||={};node=node[x]}node[

  5. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  6. ruby-on-rails - Ruby url 到 html 链接转换 - 2

    我正在使用Rails构建一个简单的聊天应用程序。当用户输入url时,我希望将其输出为html链接(即“url”)。我想知道在Ruby中是否有任何库或众所周知的方法可以做到这一点。如果没有,我有一些不错的正则表达式示例代码可以使用... 最佳答案 查看auto_linkRails提供的辅助方法。这会将所有URL和电子邮件地址变成可点击的链接(htmlanchor标记)。这是文档中的代码示例。auto_link("Gotohttp://www.rubyonrails.organdsayhellotodavid@loudthinking.

  7. ruby-on-rails - 使用 ruby​​ 将多个实例变量转换为散列的更好方法? - 2

    我收到格式为的回复#我需要将其转换为哈希值(针对活跃商家)。目前我正在遍历变量并执行此操作:response.instance_variables.eachdo|r|my_hash.merge!(r.to_s.delete("@").intern=>response.instance_eval(r.to_s.delete("@")))end这有效,它将生成{:first="charlie",:last=>"kelly"},但它似乎有点hacky和不稳定。有更好的方法吗?编辑:我刚刚意识到我可以使用instance_variable_get作为该等式的第二部分,但这仍然是主要问题。

  8. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

随机推荐