If you have any thoughts on my blog or articles and you want to let me know, you can either post a comment below(public) or tell me via this i_kkkp@163.com

Getting Started with React - Part 2

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

vue-react

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.

vue-react

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:

vue-react

The rendering process is illustrated as follows:

  • Use React.createElement or write React components with JSX. In reality, all JSX code is eventually transformed into React.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.

Getting Started with React - Part 3 Getting Started with React - Part 1

Comments