Introduction to the

Today we are going to write our own renderer, the React renderer. Before we get started, there are three core elements of React.

  • React exposes several methods, mainly defining components, without any logic on how to handle updates.
  • The Renderer handles updates to the view
  • After the Reconciler version 16, React was rewritten from stack Reconciler to Fiber Reconciler. Its main function is to walk through nodes, find out which nodes need to be updated, and then hand it over to the Renderer to update the view

Start to write

First create a project with creation-React-App, then install the React-Reconciler, and modify the index.js file to render it with our own renderer.

Let’s create a file called Renderer for the React-Reconciler reconciler’s readme.md is as follows:

import Reconciler from "react-reconciler"

const hostConfig = {
    / /... Write the function you want to use here
};
const MyReconcilerInstance = Reconciler(hostConfig);
const MyCustomRenderer = {
    render(ele, containerDom, callback){
        const container = MyReconcilerInstance.createContainer(containerDom, false);
        MyReconcilerInstance.updateContainer(
            ele,
            container,
            null,
            callback
        )
    }
};
export default MyCustomRenderer

Copy the code

How to write hostConfig? The official list has been provided, but we do not need to write so much, first write the required, as follows, first log all functions, see the call order:

// These are needed for rendering
consthostConfig = { getPublicInstance(... args) {console.log('getPublicInstance'. args); }, getChildHostContext(... args) {console.log('getChildHostContext'. args); }, getRootHostContext(... args) {console.log('getRootHostContext'. args); }, appendChildToContainer(... args) {console.log('appendChildToContainer'. args); }, prepareForCommit(... args) {console.log('prepareForCommit'. args) }, resetAfterCommit(... args) {console.log('resetAfterCommit'. args) }, createInstance(... args) {console.log('createInstance'. args) }, appendInitialChild(... args) {console.log('appendInitialChild'. args) }, finalizeInitialChildren(... args) {console.log('prepareUpdate'. args) }, shouldSetTextContent(... args) {console.log('shouldSetTextContent'. args) }, shouldDeprioritizeSubtree(... args) {console.log('shouldDeprioritizeSubtree'. args); }, createTextInstance(... args) {console.log('createTextInstance'. args); }, scheduleDeferredCallback(... args) {console.log('scheduleDeferredCallback'. args); }, cancelDeferredCallback(... args) {console.log('cancelDeferredCallback'. args); }, shouldYield(... args) {console.log('shouldYield'. args); }, scheduleTimeout(... args) {console.log('scheduleTimeout'. args); }, cancelTimeout(... args) {console.log('cancelTimeout'. args); }, noTimeout(... args) {console.log('noTimeout'. args); }, now(... arg){console.log('now'. args); }, isPrimaryRenderer(... args) {console.log('isPrimaryRenderer'. args); },supportsMutation:true,}Copy the code

Then we modify the app.js file and simply write a counter like this:

class App extends Component {
    state = {
        count: 1
    }
    
    increment = (a)= > {
        this.setState((state) = > {
            return { count: state.count + 1 }
        })
    }

    decrement = (a)= > {
        this.setState((state) = > {
            return { count: state.count - 1 }
        })
    }

    render() {
        const { count } = this.state;
        return (
            <div>
                <button onClick={this.decrement}> - </button>
                <span>{count}</span>
                <button onClick={this.increment}> + </button>
            </div>)}}Copy the code

Open the browser and see that nothing has been rendered. Open Console and these functions are called in the following order. Ok, so let’s start writing these functions:

First apply colours to a drawing

  • The now function returns the current time, so we’ll just write date.now.
  • GetRootHostContext is a function that passes information to the next-level node, so we simply return an empty object.
// rootContainerInstance root
getRootHostContext(rootContainerInstance){
    return{}}Copy the code
  • GetChildHostContext is used to fetch the context from the previous function and pass it down, so we then return an empty object
/** * parentHostContext Context passed from the upper node * type nodeType of the current node * rootContainerInstance Root node */ 
getChildHostContext(parentHostContext, type, rootContainerInstance){
    return{}}Copy the code
  • ShouldSetTextContent = shouldSetTextContent = shouldSetTextContent = shouldSetTextContent = shouldSetTextContent = shouldSetTextContent = shouldSetTextContent = shouldSetTextContent = shouldSetTextContent = shouldSetTextContent So let’s make it a little bit simpler, so we don’t have to worry about dangerouslySetInnerHTML, so we can just check whether children are a string or a number
/* * type The nodeType of the current node * props The property to be assigned to the current node */
shouldSetTextContent(type, props) {
    return typeof props.children === 'string' || typeof props.children === 'number'
},
Copy the code
  • CreateInstance is the function that generates the DOM.
/** * type The nodeType of the current node * newProps The properties passed to the current node * rootContainerInstance The root node * currentHostContext The context passed from the parent node * WorkInProgress The fiber node */ corresponding to the current DOM node 
createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress) {
    const element = document.createElement(type);
    for (const key in newProps) {
        const val = newProps[key];
        if (key === 'className') {
            element.className = val
        } else if (key === 'style') {
            element.style = val
        } else if (key.startsWith('on')) {
            const eventType = key.slice(2).toLowerCase();
            element.addEventListener(eventType, val);
        } else if (key === 'children') {
            if (typeof val === 'string' || typeof val === 'number') {
                const textNode = document.createTextNode(val);
                element.appendChild(textNode)
            }
        } else {
            element.setAttribute(key, val)
        }
    }
    return element
},
Copy the code
  • The finalizeInitialChildren function is used to determine whether the current node fails to complete some functions in the commit phase. This function can only be completed after the DOM node has been mounted. So let’s simply check if we have this property
/** * domElement Currently generated DOM node * type nodeType * props * rootContainerInstance Root node * hostContext Context passed from the parent */ 
finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) {
    return!!!!! props.autoFocus },Copy the code
  • AppendInitialChild is the function used to mount the newly generated node to the node at the previous level.
/** * parentInstance Parent node * child child node */ 
appendInitialChild(parentInstance, child) {
    parentInstance.appendChild(child);
}
Copy the code
  • When prepareForCommit is called, our DOM node is generated and ready to be mounted to the root node, where we need to do some preparatory work, such as disabling event triggering and counting nodes that require Autofocus. We don’t have to, we can just write an empty function.
// rootContainerInstance Root node
prepareFomCommit(rootContainerInstance){}
Copy the code
  • AppendChildToContainer is the function that inserts the generated node into the root node.
// container Our root node
// child Generated node
appendChildToContainer(container, child){
    container.appendChild(child)
}
Copy the code
  • The resetAfterCommit function fires after the real node has been mounted, so we’ll write an empty function.
resetAfterCommit(){}
Copy the code

Okay, now we’re done with our first render.

And then I drew an ugly flow chart:

update

  • PrepareUpdate The prepareUpdate function is used to count updates, returning an array indicating that an update is required, or null if no update is required. The returned array is then returned to the corresponding Fiber node of the current DOM node, assigning the updateQueue to the fiber node, and marking the current fiber node as the state to be updated.
/** * domElement The DOM node currently traversed * type nodeType * oldProps Old property * newProps new property * rootContainerInstance Root node * hostContext Context */ passed from the upper level node 
prepareUpdate(domElement, type, oldProps, newProps, rootContainerInstance, hostContext) {
    console.log('prepareUpdate', [...arguments]);
    let updatePayload = null;
    for (const key in oldProps) {
        const lastProp = oldProps[key];
        const nextProp = newProps[key];
        if (key === 'children') {
            if(nextProp ! = lastProp && (typeof nextProp === 'number' || typeof nextProp === 'string')) { updatePayload = updatePayload || []; updatePayload.push(key, nextProp); }}else {
            // The rest is not considered for the moment}};return updatePayload
}
Copy the code
  • CommitUpdate is already iterated and ready to update.
/** * the dom node corresponding to domElement * updatePayload the update we just decided to return * type nodeType * oldProps old property * newProps new property * InternalInstanceHandle Specifies the fiber node */ corresponding to the current DOM node 
commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
    for (var i = 0; i < updatePayload.length; i += 2) {
        var propKey = updatePayload[i];
        var propValue = updatePayload[i + 1];
        if (propKey === 'style') {}else if (propKey === 'children') {
            domElement.textContent = propValue;
            // Other cases are not considered for the time being
        } else{}}},Copy the code

Woola! Updates are complete.

The last

Code a little bit much, you look hard! All code for this article can be found here

Reference:

  • react reconciler
  • hello world custom react renderer