The Essence of JSX#
What exactly is JSX? Let's first 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 very similar to HTML, not the familiar JavaScript. How do we explain this?
The Relationship Between JSX Syntax and JavaScript#
The answer is quite simple, see React official website
JSX will be compiled to React.createElement(), which returns a JS object called a "React Element."
So, in fact, JSX will be transformed into a React Element
JS object through a compilation process via the call to React.createElement()
.
So how does compilation work? For code written in ECMAScript 2015+
, we usually need to use a tool called Babel
to ensure compatibility with older browsers.
For example, the handy template string syntax 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 you can see, the JSX tags are transformed into corresponding calls to React.createElement, so in reality, JSX is just a way of writing React.createElement. It looks like HTML, but at its core, it is JS.
The essence of JSX is the syntax sugar for the JavaScript call React.createElement, which perfectly echoes the statement from the React official documentation that "JSX fully leverages the capabilities of JavaScript."
As shown in the image above, the advantages of JSX are obvious: JSX code is well-structured and has clear nesting relationships; however, using React.createElement looks much more chaotic than JSX, making it less friendly to read and more cumbersome 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 is JSX Mapped to DOM?#
Let's first take a look at the source code for createElement. Here is a commented snippet of the 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 indicates that 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 place 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 array to return the original childArray which cannot be modified, preventing someone from modifying 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 own 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 (string/array) 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 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'
)
)
It 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 move on.
Breaking Down 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 place other properties into the props object.
}
}
}
Here, the input parameter config is broken down into ref, key, self, source, props
, followed by the processing of children.
Extracting Child Elements#
After breaking down config, the code for processing child elements follows, where all parameters after the second one are stored in 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 array to return the original childArray which cannot be modified, preventing someone from modifying 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 is handling the situation when 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
), the provided default values are used.
// Set default values for child components, generally for components.
// class com extends React.component then com.defaultProps gets the current component's own 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 the assembled data into 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 these types of data to break free from platform limitations.