Introduction
This article focuses on how React’s JSX is compiled into VNodes.
Vue’s Render Function
Before diving into React’s JSX, let’s recall how Vue declares components using the Vue template method. Vue templates are compiled by the Vue compiler into render functions. The render function returns VNodes, and these VNodes are then rendered into real DOM through the patch function.
Details of the renderer have been discussed in previous chapters and will not be reiterated here.
React’s JSX
React
maps JSX written in components to the screen, and when the state within components changes, React
updates these “changes” on the screen.
JSX is ultimately transformed into a form like React.createElement
by babel
. As for how babel
transforms JSX into React.createElement
, we can use babel’s online compiler to check.
<div>
<img src="avatar.png" className="profile" />
<Hello />
</div>
Will be transformed by babel into:
React.createElement(
"div",
null,
React.createElement("img", {
src: "avatar.png",
className: "profile"
}),
React.createElement(Hello, null)
);
Babel
provides an online REPL compiler that can convert ES6 code to ES5 code online. The transformed code can be directly inserted into a webpage and run as ES5 code. Here’s a link: Babel Online Compiler
During the transformation process, babel at compile time judges the first letter of the component in JSX:
When the first letter is lowercase, it is recognized as a native DOM tag, and the first variable of createElement
is compiled into a string.
When the first letter is uppercase, it is recognized as a custom component, and the first variable of createElement
is compiled into an object.
In the end, both will be mounted through the ReactDOM.render(...)
method, as shown below:
ReactDOM.render(<App />, document.getElementById("root"));
How is React’s JSX Transformed?
In React, nodes can be roughly divided into four categories:
- Native tag nodes
- Text nodes
- Function components
- Class components
As shown below:
class ClassComponent extends Component {
static defaultProps = {
color: "pink"
};
render() {
return (
<div className="border">
<h3>ClassComponent</h3>
<p className={this.props.color}>{this.props.name}</p>
</div>
);
}
}
function FunctionComponent(props) {
return (
<div className="border">
FunctionComponent
<p>{props.name}</p>
</div>
);
}
const jsx = (
<div className="border">
<p>xx</p>
<a href=" ">xxx</a>
<FunctionComponent name="Function Component" />
<ClassComponent name="Class Component" color="red" />
</div>
);
These categories will ultimately be transformed into the form of React.createElement
.
function createElement(type, config, ...children) {
if (config) {
delete config.__self;
delete config.__source;
}
// Detailed handling is done in the source code, such as filtering out key, ref, etc.
const props = {
...config,
children: children.map(child =>
typeof child === "object" ? child : createTextNode(child)
)
};
return {
type,
props
};
}
function createTextNode(text) {
return {
type: TEXT,
props: {
children: [],
nodeValue: text
}
};
}
export default {
createElement
};
The createElement
function makes decisions based on the passed node information:
- If it’s a native tag node, the type is a string, such as div, span.
- If it’s a text node, the type is not present, here it is TEXT.
- If it’s a function component, the type is the function name.
- If it’s a class component, the type is the class name.
The virtual DOM is rendered into real DOM using ReactDOM.render
, with the following usage:
ReactDOM.render(element, container[, callback])
When called for the first time, all DOM elements within the container node are replaced. Subsequent calls use React’s diff algorithm for efficient updates.
If an optional callback function is provided, it will be executed after the component is rendered or updated.
The render
function implementation is roughly as follows:
function render(vnode, container) {
console.log("vnode", vnode); // Virtual DOM object
// vnode -> node
const node = createNode(vnode, container);
container.appendChild(node);
}
// Create a real DOM node
function createNode(vnode, parentNode) {
let node = null;
const { type, props } = vnode;
if (type === TEXT) {
node = document.createTextNode("");
} else if (typeof type === "string") {
node = document.createElement(type);
} else if (typeof type === "function") {
node = type.isReactComponent
? updateClassComponent(vnode, parentNode)
: updateFunctionComponent(vnode, parentNode);
} else {
node = document.createDocumentFragment();
}
reconcileChildren(props.children, node);
updateNode(node, props);
return node;
}
// Traverse through child vnodes, convert them to real DOM nodes, and then insert into the parent node
function reconcileChildren(children, node) {
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (Array.isArray(child)) {
for (let j = 0; j < child.length; j++) {
render(child[j], node);
}
} else {
render(child, node);
}
}
}
function updateNode(node, nextVal) {
Object.keys(nextVal)
.filter(k => k !== "children")
.forEach(k => {
if (k.slice(0, 2) === "on") {
let eventName = k.slice(2).toLocaleLowerCase();
node.addEventListener(eventName, nextVal[k]);
} else {
node[k] = nextVal[k];
}
});
}
// Return a real DOM node
// Execute the function
function updateFunctionComponent(vnode, parentNode) {
const { type, props } = vnode;
let vvnode = type(props);
const node = createNode(vvnode, parentNode);
return node;
}
// Return a real DOM node
// Instantiate first, then execute the render function
function updateClassComponent(vnode, parentNode) {
const { type, props } = vnode;
let cmp = new type(props);
const vvnode = cmp.render();
const node = createNode(vvnode, parentNode);
return node;
}
export default {
render
};
In the React source code, the overall process of converting virtual DOM to real DOM is shown in the following diagram:
The rendering process is illustrated as follows:
Use
React.createElement
or writeReact
components with JSX. In reality, all JSX code is eventually transformed intoReact.createElement(...)
with the help of Babel.The
createElement
function handles special props such as key and ref, assigns default props with defaultProps, and processes the passed children nodes, ultimately constructing a virtual DOM object.ReactDOM.render
renders the generated virtual DOM onto the specified container. It employs mechanisms such as batch processing, transactions, and performance optimizations for specific browsers, ultimately transforming into real DOM.
Comments