This story started a few years ago when React and React-Dom divorced.

When React first became popular, there was no React dom. One day, React became king, and people wanted to learn time management. React was separated from dom and became concubines. React-native, Remax, etc. Why is React so ruthless? I showdown, plait can’t go on, say to write a good article he not sweet?

Seriously, here we go!

I believe you are familiar with the concept of cross-end, what is cross-end? React allows you to write Web, small programs, and native applications. This greatly reduces the cost. However, React is your responsibility.

  • Web: the react – dom
  • Small program: remax
  • Ios and Android: React-native

Does that make sense? Let’s look at another picture

React and React -dom subcontracting React and React -dom subcontracting The React package itself has very little code. It only provides specifications and API definitions. Platform-related content is stored in host-related packages.

With that said, can we also define our React renderer? Of course, otherwise follow this article, after learning will, will also want to learn.

Create the React project

Start by creating a demo project using the React scaffolding

Erection of scaffolding

npm i -g create-react-app

Create a project

create-react-app react-custom-renderer

Run the project

yarn start

Now we can code in VS Code

Modify app.js file source

import React from "react";
import "./App.css";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0}; } handleClick =(a)= > {
    this.setState(({ count }) = > ({ count: count + 1 }));
  };

  render() {
    const { handleClick } = this;
    const { count } = this.state;
    return (
      <div className="App">
        <header className="App-header" onClick={handleClick}>
          <span>{count}</span>
        </header>
      </div>); }}export default App;

Copy the code

If you open up your browser, you’ll see the page, and if you click on the page and try it out, the number will gradually increase.

Now that the simple React project has been created, we’re ready to customize the renderer.

Getting to know the renderer

Open SRC /index.js and, unsurprisingly, you should see this line of code:

import ReactDOM from 'react-dom';
Copy the code

And this line

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
Copy the code

Now we’ll replace the React-dom with our own code, create myrenderer.js, and modify the contents in index.js

import MyRenderer from './MyRenderer'

MyRenderer.render(
  <App />,
  document.getElementById('root')
)
Copy the code

When we open the browser, we will see an error message, and we will follow the error message to improve the content of myRenderer.js. First of all, the basic structure of the file is as follows

import ReactReconciler from "react-reconciler";

const rootHostContext = {};
const childHostContext = {};

const hostConfig = {
  getRootHostContext: (a)= > {
    return rootHostContext;
  },
  getChildHostContext: (a)= > {
    returnchildHostContext; }};const ReactReconcilerInst = ReactReconciler(hostConfig);
export default {
  render: (reactElement, domElement, callback) = > {
    if(! domElement._rootContainer) { domElement._rootContainer = ReactReconcilerInst.createContainer( domElement,false
      );
    }
    return ReactReconcilerInst.updateContainer(
      reactElement,
      domElement._rootContainer,
      null, callback ); }};Copy the code

We can use it as a scheduler for creating and updating the React-Reconciler, and then schedule it in scheduler. We export an object with a method called Render that takes the same arguments as the Render method of the React-DOM. It is the create operation if the DOM element is the root container, and the update operation if it is not, which calls the createContainer method of the React-Reconciler instance. The update operation calls the updateContainer method of the React-Reconciler instance. Let’s look at the more important concept, hostConfig.

Host Indicates host-related configurations

HostConfig is the host-related configuration. The Host here is the operating environment, whether it is web, small program or native APP. With this configuration, the React-Reconciler is scheduled to facilitate UI updates based on the host environment.

Let’s go to the browser and follow the error message to improve the content of hostConfig. I will list the core methods as follows for your reference.

  • getRootHostContext
  • getChildHostContext
  • shouldSetTextContent
  • prepareForCommit
  • resetAfterCommit
  • createTextInstance
  • createInstance
  • appendInitialChild
  • appendChild
  • finalizeInitialChildren
  • appendChildToContainer
  • prepareUpdate
  • commitUpdate
  • commitTextUpdate
  • removeChild

See these methods can not help but think of the DOM related manipulation methods, are semantic naming, here do not repeat the actual meaning of each method, let’s modify the relevant methods, to get the project running again, to help you understand how the renderer works.

Define hostConfig

We’ll focus on createInstance and commitUpdate. The other methods are shown in code snippets at the end. (Note: The relevant implementation may be quite different from the actual use, only for reference and learning)

createInstance

The method parameters

  • type
  • newProps
  • rootContainerInstance
  • _currentHostContext
  • workInProgress

The return value

Based on the type passed in, create the DOM element, process props, and so on, and finally return the DOM element. Let’s just consider a few props for this example

  • children
  • onClick
  • className
  • style
  • other

Code implementation

const hostConfig = {
  createInstance: (
    type,
    newProps,
    rootContainerInstance,
    _currentHostContext,
    workInProgress
  ) => {
    const domElement = document.createElement(type);
    Object.keys(newProps).forEach((propName) = > {
      const propValue = newProps[propName];
      if (propName === "children") {
        if (typeof propValue === "string" || typeof propValue === "number") { domElement.textContent = propValue; }}else if (propName === "onClick") {
        domElement.addEventListener("click", propValue);
      } else if (propName === "className") {
        domElement.setAttribute("class", propValue);
      } else if (propName === "style") {
        const propValue = newProps[propName];
        const propValueKeys = Object.keys(propValue)
        const propValueStr = propValueKeys.map(k= > `${k}: ${propValue[k]}`).join('; ')
        domElement.setAttribute(propName, propValueStr);
      } else {
        constpropValue = newProps[propName]; domElement.setAttribute(propName, propValue); }});returndomElement; }},Copy the code

Does that look familiar? Who says native JavaScript is not important? We can see that inside the framework, we still need to use native JavaScript to manipulate the DOM, but we won’t go into that.

commitUpdate

Where does the update come from? It’s easy to think of setState and, of course, forceUpdate. For example, the old question: Dude, is setState synchronous or asynchronous? When are we going to synchronize? This involves the content of Fiber. In fact, scheduling is determined by the calculation of expirationTime, which means that update requests received within a certain interval will be queued and labeled at the same time. Imagine that, if other conditions are the same, these updates will be executed at the same time, seemingly asynchronous. In effect, it gives priority to more needed tasks.

As a bit of an extension, we come back with updates from setState, forceUpdate, and finally commitUpdate after a series of dispatches.

The method parameters

  • domElement
  • updatePayload
  • type
  • oldProps
  • newProps

CreateInstance (createInstance, createInstance, createInstance, createInstance); createInstance (createInstance, createInstance, createInstance, createInstance);

Code implementation

const hostConfig = {
  commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
    Object.keys(newProps).forEach((propName) = > {
      const propValue = newProps[propName];
      if (propName === "children") {
        if (typeof propValue === "string" || typeof propValue === "number") {
          domElement.textContent = propValue;
        }
        // TODO also considers arrays
      } else if (propName === "style") {
        const propValue = newProps[propName];
        const propValueKeys = Object.keys(propValue)
        const propValueStr = propValueKeys.map(k= > `${k}: ${propValue[k]}`).join('; ')
        domElement.setAttribute(propName, propValueStr);
      } else {
        constpropValue = newProps[propName]; domElement.setAttribute(propName, propValue); }}); }},Copy the code

With the two main methods covered, are you starting to feel the react cross-platform appeal? We can imagine that the myRenderer.render method passes in a second argument that is not a DOM object, but a GUI object from another platform, Is it ok to use the corresponding GUI creation and update apis in the createInstance and commitUpdate methods? That’s right!

Complete configuration

const hostConfig = {
  getRootHostContext: (a)= > {
    return rootHostContext;
  },
  getChildHostContext: (a)= > {
    return childHostContext;
  },
  shouldSetTextContent: (type, props) = > {
    return (
      typeof props.children === "string" || typeof props.children === "number"
    );
  },
  prepareForCommit: (a)= > {},
  resetAfterCommit: (a)= > {},
  createTextInstance: (text) = > {
    return document.createTextNode(text);
  },
  createInstance: (
    type,
    newProps,
    rootContainerInstance,
    _currentHostContext,
    workInProgress
  ) => {
    const domElement = document.createElement(type);
    Object.keys(newProps).forEach((propName) = > {
      const propValue = newProps[propName];
      if (propName === "children") {
        if (typeof propValue === "string" || typeof propValue === "number") { domElement.textContent = propValue; }}else if (propName === "onClick") {
        domElement.addEventListener("click", propValue);
      } else if (propName === "className") {
        domElement.setAttribute("class", propValue);
      } else if (propName === "style") {
        const propValue = newProps[propName];
        const propValueKeys = Object.keys(propValue)
        const propValueStr = propValueKeys.map(k= > `${k}: ${propValue[k]}`).join('; ')
        domElement.setAttribute(propName, propValueStr);
      } else {
        constpropValue = newProps[propName]; domElement.setAttribute(propName, propValue); }});return domElement;
  },
  appendInitialChild: (parent, child) = > {
    parent.appendChild(child);
  },
  appendChild(parent, child) {
    parent.appendChild(child);
  },
  finalizeInitialChildren: (domElement, type, props) = > {},
  supportsMutation: true.appendChildToContainer: (parent, child) = > {
    parent.appendChild(child);
  },
  prepareUpdate(domElement, oldProps, newProps) {
    return true;
  },
  commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
    Object.keys(newProps).forEach((propName) = > {
      const propValue = newProps[propName];
      if (propName === "children") {
        if (typeof propValue === "string" || typeof propValue === "number") {
          domElement.textContent = propValue;
        }
        // TODO also considers arrays
      } else if (propName === "style") {
        const propValue = newProps[propName];
        const propValueKeys = Object.keys(propValue)
        const propValueStr = propValueKeys.map(k= > `${k}: ${propValue[k]}`).join('; ')
        domElement.setAttribute(propName, propValueStr);
      } else {
        constpropValue = newProps[propName]; domElement.setAttribute(propName, propValue); }}); }, commitTextUpdate(textInstance, oldText, newText) { textInstance.text = newText; }, removeChild(parentInstance, child) { parentInstance.removeChild(child); }};Copy the code

Go to the browser, it works, click on the page and the count increases.

That’s all for this section. Do you understand? If you want to see other framework principles, feel free to leave a comment

  • Wechat official account “JavaScript Full Stack”
  • Nuggets’ Master of One ‘
  • Bilibili, The Master of One
  • WeChat: zxhy – heart

I am one. Farewell hero.