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.