In the previous two articles, we have implemented vDOM rendering and JSX compilation, and implemented function and class components. In this article, we will implement patch update.

Can do vDOM rendering and update, support components (props, state), this is a relatively complete front-end framework.

First, let’s prepare the test code:

The test code

On the basis of the previous section, make a transformation:

Add a delete button, an input box and an add button, and add the corresponding event listener:

This part of code is often written, but will not be explained much:

function Item(props) {
    return <li className="item" style={props.style}>{props.children}  <a href="#" onClick={props.onRemoveItem}>X </a></li>;
}

class List extends Component {
    constructor(props) {
        super(a);this.state = {
            list: [{text: 'aaa'.color: 'pink'
                },
                {
                    text: 'bbb'.color: 'orange'
                },
                {
                    text: 'ccc'.color: 'yellow'}}}]handleItemRemove(index) {
        this.setState({
            list: this.state.list.filter((item, i) = >i ! == index) }); }handleAdd() {
        this.setState({
            list: [
                ...this.state.list, 
                {
                    text: this.ref.value
                }
            ]
        });
    }

    render() {
        return <div>
            <ul className="list">
                {this.state.list.map((item, index) => {
                    return <Item style={{ background: item.color.color: this.state.textColor}} onRemoveItem={()= > this.handleItemRemove(index)}>{item.text}</Item>
                })}
            </ul>
            <div>
                <input ref={(ele)= > {this.ref = ele}}/>
                <button onClick={this.handleAdd.bind(this)}>add</button>
            </div>
        </div>;
    }
}

render(<List textColor={'#000'} / >.document.getElementById('root'));
Copy the code

Now that we’ve implemented rendering, we’re going to implement updating, which is the process of updating the page after setState.

Realize the patch

The simplest update is to re-render the dom at setState, replacing the entire dom:

setState(nextState) {
    this.state = Object.assign(this.state, nextState);
 
    const newDom = render(this.render());
    this.dom.replaceWith(newDom);
    this.dom = newDom;
}
Copy the code

Under test:

We implemented the update feature!

Just kidding. The front-end framework will not be updated this way, with a lot of unnecessary DOM manipulation and poor performance.

Therefore, patch still needs to be implemented, that is:

setState(nextState) {
    this.state = Object.assign(this.state, nextState);
    if(this.dom) {
        patch(this.dom, this.render()); }}Copy the code

The patch function diff the VDOM to be rendered and the existing DOM, and only update the DOM that needs to be updated, that is, update as needed.

ShouldComponentUpdate (); if the props and state are still the same, then the patch is not used.

setState(nextState) {
    this.state = Object.assign(this.state, nextState);

    if(this.dom && this.shouldComponentUpdate(this.props, nextState)) {
        patch(this.dom, this.render()); }}shouldComponentUpdate(nextProps, nextState) {
    returnnextProps ! =this.props || nextState ! =this.state;
}
Copy the code

How to implement patch?

We use recursive VDOM for rendering, doing different things to elements, text, and components, including creating nodes and setting properties. The recursion is the same when patch is updated, but the processing of elements, text and components is different:

The text

To determine whether a DOM node is text, look again at vDOM:

  • If the vDOM is not a text node, replace it
  • If the VDOM is also a text node, compare the content and replace it if the content is different
if (dom instanceof Text) {
    if (typeof vdom === 'object') {
        return replace(render(vdom, parent));
    } else {
        return dom.textContent != vdom ? replace(render(vdom, parent)) : dom;
    }
}
Copy the code

Replace is implemented with replaceChild:

const replace = parent ? el= > {
    parent.replaceChild(el, dom);
    return el;
} : (el= > el);
Copy the code

Then there is the component update:

component

If the VDOM is a component, the corresponding DOM may or may not be rendered by the same component.

To determine if the DOM is rendered by the same component, replace it directly, or update the child element if it is:

How do you know what component the DOM is rendered from?

We need to add a dom attribute to render to record:

Change the render part of the code and add the instance property:

instance.dom.__instance = instance;
Copy the code

Constructor (); dom (); dom (); dom (); dom (); dom ();

if (dom.__instance && dom.__instance.constructor == vdom.type) {
    dom.__instance.componentWillReceiveProps(props);

    return patch(dom, dom.__instance.render(), parent);
} 
Copy the code

Otherwise, if it is not the same component, it will be replaced directly:

Class component replacement:

if (Component.isPrototypeOf(vdom.type)) {
    const componentDom = renderComponent(vdom, parent);
    if (parent){
        parent.replaceChild(componentDom, dom);
        return componentDom;
    } else {
        return componentDom
    }
}
Copy the code

Function component replacement:

if(! Component.isPrototypeOf(vdom.type)) {return patch(dom, vdom.type(props), parent);
}
Copy the code

So the component update logic looks like this:

function isComponentVdom(vdom) {
    return typeof vdom.type == 'function';
}

if(isComponentVdom(vdom)) {
    const props = Object.assign({}, vdom.props, {children: vdom.children});
    if (dom.__instance && dom.__instance.constructor == vdom.type) {
        dom.__instance.componentWillReceiveProps(props);
        return patch(dom, dom.__instance.render(), parent);
    } else if (Component.isPrototypeOf(vdom.type)) {
        const componentDom = renderComponent(vdom, parent);
        if (parent){
            parent.replaceChild(componentDom, dom);
            return componentDom;
        } else {
            return componentDom
        }
    } else if(! Component.isPrototypeOf(vdom.type)) {returnpatch(dom, vdom.type(props), parent); }}Copy the code

There are also updates to elements:

The element

If dom is an element, check to see if it is of the same type:

  • Different types of elements, direct substitution
if(dom.nodeName ! == vdom.type.toUpperCase() &&typeof vdom === 'object') {
    return replace(render(vdom, parent));
} 
Copy the code
  • Update child nodes and attributes for elements of the same type

Update child nodes as we wish to reuse, so render each element with an identity key:

instance.dom.__key = vdom.props.key;
Copy the code

Render a new key if it is not found.

First we put the DOM of all the child nodes into an object:

constoldDoms = {}; [].concat(... dom.childNodes).map((child, index) = > {
    const key = child.__key || `__index_${index}`;
    oldDoms[key] = child;
});
Copy the code

[]. Concat is used to flatten an array because its elements are also arrays.

The default key is set to index.

Then the children of vDOM will be rendered in a loop. If the corresponding key is found, it will be reused directly, and then patch its child elements. Render a new one if not found:

[].concat(... vdom.children).map((child, index) = > {
    const key = child.props && child.props.key || `__index_${index}`;
    dom.appendChild(oldDoms[key] ? patch(oldDoms[key], child) : render(child, dom));
    delete oldDoms[key];
});
Copy the code

Remove the new DOM from oldDoms. All that is left is the dom that is no longer needed, which can be deleted:

for (const key in oldDoms) {
    oldDoms[key].remove();
}
Copy the code

You can also execute the component’s willUnmount lifecycle function before deleting it:

for (const key in oldDoms) {
    const instance = oldDoms[key].__instance;
    if (instance) instance.componentWillUnmount();

    oldDoms[key].remove();
}
Copy the code

When the child node is processed, the following attributes are processed:

Delete the old properties and set the new props:

for (const attr of dom.attributes) dom.removeAttribute(attr.name);
for (const prop in vdom.props) setAttribute(dom, prop, vdom.props[prop]);
Copy the code

Before setAttribute, we only handled style, event Listener and common attributes. We still need to improve:

Each event listener needs to be removed and then added, so that there will always be only one render:

function isEventListenerAttr(key, value) {
    return typeof value == 'function' && key.startsWith('on');
}

if (isEventListenerAttr(key, value)) {
    const eventType = key.slice(2).toLowerCase();
 
    dom.__handlers = dom.__handlers || {};
    dom.removeEventListener(eventType, dom.__handlers[eventType]);
  
    dom.__handlers[eventType] = value;
    dom.addEventListener(eventType, dom.__handlers[eventType]);
}
Copy the code

Put the listener of each event on the __handlers property of the DOM, and delete the previous one each time and replace it with a new one.

Then support the ref attribute:

function isRefAttr(key, value) {
    return key === 'ref' && typeof value === 'function';
}

if(isRefAttr(key, value)) {
    value(dom);
} 
Copy the code

Here’s what it does:

<input ref={(ele)= > {this.ref = ele}}/>
Copy the code

Support key Settings:

if (key == 'key') {
    dom.__key = value;
} 
Copy the code

There are also Settings for special properties, including Checked, Value, and className:

if (key == 'checked' || key == 'value' || key == 'className') {
    dom[key] = value;
} 
Copy the code

The rest is set by setAttribute:

function isPlainAttr(key, value) {
    return typeofvalue ! ='object' && typeofvalue ! ='function';
}

if (isPlainAttr(key, value)) {
    dom.setAttribute(key, value);
}
Copy the code

So now setAttribute looks like this:

const setAttribute = (dom, key, value) = > {
    if (isEventListenerAttr(key, value)) {
        const eventType = key.slice(2).toLowerCase();
        dom.__handlers = dom.__handlers || {};
        dom.removeEventListener(eventType, dom.__handlers[eventType]);
        dom.__handlers[eventType] = value;
        dom.addEventListener(eventType, dom.__handlers[eventType]);
    } else if (key == 'checked' || key == 'value' || key == 'className') {
        dom[key] = value;
    } else if(isRefAttr(key, value)) {
        value(dom);
    } else if (isStyleAttr(key, value)) {
        Object.assign(dom.style, value);
    } else if (key == 'key') {
        dom.__key = value;
    } else if(isPlainAttr(key, value)) { dom.setAttribute(key, value); }}Copy the code

The update logic for the text, component, and element is complete, so let’s test it:

And you’re done!

We have implemented the function of Patch, which is fine-grained update on demand.

The code is uploaded to github: github.com/QuarkGluonP…

conclusion

Patch, like render, is also a recursive processing of elements, components and text.

During patch, it is necessary to compare some information of THE DOM and vDOM to be rendered, and then decide whether to render the new DOM or reuse the existing DOM. Therefore, instance, key and other information should be recorded in the DOM during render.

Child element updates of an element should support keys to identify the element so that previous elements can be reused and dom creation is reduced. When setting the event Listener, delete the existing one and add a new one to ensure that there is only one event Listener.

Vdom rendering and updating, component and lifecycle implementation, this is already a complete front-end framework.

This is the first version of the front-end framework we implemented, called Dong 1.0.

However, the current front-end framework is recursive render and patch, if the VDOM tree is too large, it will be a lot of calculation and performance will not be very good. In the future of Dong 2.0, we will modify the VDOM to Fiber, and then implement the hooks function.