The Essence of JSX#
What exactly is JSX? Let's first take a look at a definition provided by the React official website:
JSX is a syntax extension for JavaScript that is very similar to a template language, but it fully leverages the capabilities of JavaScript.
For JSX, Facebook defines it as a "syntax extension," while fully leveraging the capabilities of JS. However, it actually looks quite similar to HTML, not the familiar JavaScript. How can we explain this?
The Relationship Between JSX Syntax and JavaScript#
In fact, the answer is quite simple, see React official website
JSX is compiled into React.createElement(), which returns a JS object called a "React Element."
So, JSX is actually transformed into a React Element
JS object through a compilation process via the call to React.createElement()
.
So how does compilation work? For ECMAScript 2015+
code, we typically need to use a tool called Babel
to ensure compatibility with older browsers.
For example, the very useful template string syntax sugar in ES2015+
:
var text = 'World'
console.log(`Hello ${text}!`) //Hello World!
Babel can help us convert this code into ES5 code that most older browsers can recognize:
var text = 'World'
console.log('Hello'.concat(text, '!')) //Hello World!
Similarly, Babel also has the capability to convert JSX syntax into JavaScript code. So what does Babel specifically transform JSX into? 【Example】
As we can see, the JSX tags are transformed into corresponding calls to React.createElement, so in fact, JSX is just writing React.createElement. It looks like HTML, but the core is JS.
The essence of JSX is the syntax sugar of the JavaScript call React.createElement, which perfectly echoes the statement from the React official website that "JSX fully leverages the capabilities of JavaScript."
As shown in the image above, the advantages of JSX are obvious: JSX code is clear and well-structured, with a clear nesting relationship; however, using React.createElement looks much more chaotic than JSX, making it less friendly to read and harder to write.
JSX syntax sugar allows front-end developers to use the familiar class HTML tag syntax to create virtual DOM, reducing the learning curve while also enhancing development efficiency and experience.
How Does JSX Map to DOM?#
First, let's take a look at the source code of createElement. Here is a snippet of commented source code:
export function createElement(type, config, children) {
var propName
// Extract reserved names
var props = {}
var key = null
var ref = null
var self = null
var source = null
// If the tag's attributes are not empty, it means the tag has attribute values. Special handling: assign key and ref to separate variables.
if (config != null) {
// Valid ref
if (hasValidRef(config)) {
ref = config.ref
}
// Valid key
if (hasValidKey(config)) {
key = '' + config.key
}
self = config.__self === undefined ? null : config.__self
source = config.__source === undefined ? null : config.__source
// Remaining properties in config that are not native properties (properties of the RESERVED_PROPS object) are added to the new props object.
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName] // Remove key/ref from config and put other properties into the props object.
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
// Number of child elements (the third argument and subsequent arguments are child elements, sibling nodes)
var childrenLength = arguments.length - 2
if (childrenLength === 1) {
props.children = children
} else if (childrenLength > 1) {
var childArray = Array(childrenLength) // Declare an array
// Push children into the array one by one
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2]
}
{
// Freeze the array to prevent modification of the core object of the library. Freezing objects greatly improves performance.
if (Object.freeze) {
Object.freeze(childArray)
}
}
props.children = childArray // The parent component accesses the child component's values through this.props.children
}
// Set default values for child components, generally for components.
// class com extends React.component then com.defaultProps gets the current component's static method.
if (type && type.defaultProps) {
// If the current component has defaultProps, define the current component's default content in defaultProps.
var defaultProps = type.defaultProps
for (propName in defaultProps) {
if (props[propName] === undefined) {
// If the corresponding value in the parent component is undefined, assign the default value to props as a property of props.
props[propName] = defaultProps[propName]
}
}
}
{
// If key or ref exists
if (key || ref) {
// If type is a component
var displayName =
typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type
if (key) {
defineKeyPropWarningGetter(props, displayName)
}
if (ref) {
defineRefPropWarningGetter(props, displayName)
}
}
}
// props: 1. config's property values 2. children properties (strings/arrays) 3. default property values
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
}
Parameter Analysis#
function createElement(type, config, children)
has three parameters type, config, children
with the following meanings:
type: Used to identify the type of the node. It can be a standard HTML tag string like "h1" or "div", or it can be a React component type or React fragment type.
config: Passed in as an object, all properties of the component are stored in the config object as key-value pairs.
children: Passed in as an object, it records the nested content between component tags, also known as "child nodes" or "child elements."
For example, the following call:
React.createElement(
'ul',
{
// Pass in attribute key-value pairs
className: 'list',
// From the third argument onwards, the passed parameters are all children
},
React.createElement(
'li',
{
key: '1',
},
'1'
),
React.createElement(
'li',
{
key: '2',
},
'2'
)
)
Corresponds to the following DOM structure:
<ul className="list">
<li key="1">1</li>
<li key="2">2</li>
</ul>
After understanding the parameters, let's continue.
Breakdown of the config Parameter#
if (config != null) {
// Valid ref
if (hasValidRef(config)) {
ref = config.ref
}
// Valid key
if (hasValidKey(config)) {
key = '' + config.key
}
self = config.__self === undefined ? null : config.__self
source = config.__source === undefined ? null : config.__source
// Remaining properties in config that are not native properties (properties of the RESERVED_PROPS object) are added to the new props object.
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName] // Remove key/ref from config and put other properties into the props object.
}
}
}
Here, the input parameter config is broken down into several properties: ref, key, self, source, props
, followed by the processing of children.
Extracting Child Elements#
After breaking down config, the next part of the code handles child elements, storing all parameters after the second one into the props.children
array:
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
// Number of child elements (the third argument and subsequent arguments are child elements, sibling nodes)
var childrenLength = arguments.length - 2
if (childrenLength === 1) {
props.children = children
} else if (childrenLength > 1) {
var childArray = Array(childrenLength) // Declare an array
// Push children into the array one by one
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2]
}
{
// Freeze the array to prevent modification of the core object of the library. Freezing objects greatly improves performance.
if (Object.freeze) {
Object.freeze(childArray)
}
}
props.children = childArray // The parent component accesses the child component's values through this.props.children
}
Next, it handles the situation where the parent component passes props to children. If the child component has default values and the parent component does not pass props (i.e., the value is undefined
), it uses the provided default values.
// Set default values for child components, generally for components.
// class com extends React.component then com.defaultProps gets the current component's static method.
if (type && type.defaultProps) {
// If the current component has defaultProps, define the current component's default content in defaultProps.
var defaultProps = type.defaultProps
for (propName in defaultProps) {
if (props[propName] === undefined) {
// If the corresponding value in the parent component is undefined, assign the default value to props as a property of props.
props[propName] = defaultProps[propName]
}
}
}
Next, it checks whether key
and ref
have been assigned values. If so, it executes the functions defineKeyPropWarningGetter
and defineRefPropWarningGetter
, and then passes a series of assembled data to ReactElement
.
The purpose of the defineKeyPropWarningGetter
and defineRefPropWarningGetter
functions is to throw an error when ref and key are accessed.
ReactElement#
The method ultimately called by createElement()
is actually the ReactElement()
method, which simply adds some marked properties like type
, source
, self
, etc., to the passed data and directly returns a JS object.
The nodes used in JSX, which resemble HTML, are transformed into nested ReactElement
objects with the help of Babel. This information is crucial for constructing the application tree structure later, and React provides this type of data to free itself from platform limitations.