Renderer.ts: fue-next /packages/ Runtime-core/SRC /renderer.ts

At the time of writing (2021-10-30 22:44), vUE (V3.2.1) is still the fastest running JS Web frameworks benchmark among the three major frameworks (Angular, React, vUE). Personally think vue3 diFF algorithm is very interesting, and the implementation of this paper is a broad DIFF algorithm – patch, and due to the pursuit of simplification, part of the implementation of this paper and the source code has a certain difference, but the basic principle is consistent, you can rest assured reference

To introduce

In a project, updating page elements is unavoidable. If you simply empty the page elements every time and re-render them all, no one can accept such performance consumption. In the above simple implementation of a Render module without patch, However, if a render without patch is used to render the following nodes

render(
    h('ul'.null, [h('li'.null.'first'), h('li'.null.'second'), h('li'.null.'third')),document.body
);

render(
    h('ul'.null, [h('li'.null.'first'), h('li'.null.'I'm not second'), h('li'.null.'third')),document.body
);
Copy the code

As you can see, only one text has been changed between rendering and rendering, but you still need to clear the entire VNode tree and create a new one, which is really stupid. Patch is used to compare the changes of elements in the specified container before and after rendering to minimize the impact of the update. In other words, patch can update DOM nodes in the smallest unit, corresponding to the above example, only the text of the second Li in the original page will be updated, and the performance improvement is beyond doubt

Look at the

The old VNode tree is n1, and the new VNode tree is N2

In fact, it’s easy to think of the following points

  • We need to compare n1 and n2, so we have to pass in N2, and we have to get n1
  • You can’t just add and subtract, so you need the associated unmount operation to unload unwanted nodes
  • Different node types need different operation processes for comparison. For example, text nodes only need to compare text content, while element nodes need to compare tag types, node attributes and child nodes. Therefore, different functions are needed to compare different nodes separately

Draw a diagram

Note that the operations of different types of nodes in the figure are simply summarized as Patchxxx, processxxx, etc. The specific implementation is not so simple, and the general patch process can be understood through this figure

The following is a detailed explanation

  • inrender, determine whether N2 exists. If n2 exists, it means that patch needs to be performed; if n2 does not exist, it means that the original N1 needs to be unloaded
  • inpatchIn, n1 and N2 are of the same type and can be directly processed. If they are different, it means that the whole node changes, and N1 needs to be unmounted first and then N2 needs to be mounted
  • inprocessxxxIf n1 exists, you can reuse the node of N1 and update the content instead of creating a new node. If n1 does not exist, you need to remount it

Write about the

After the above analysis, we can try to rewrite the render part of the original (above)

unmount

The implementation of unmount is actually much, much simpler because there are no other types to consider

const unmount = vnode= > {
    // The previous el is useful
    const { el } = vnode;
    el.parentNode.removeChild(el);
}
Copy the code

unmountChildren

UnmountChildren implementation and mountChildren can not be said exactly the same, can only say very similar, later to use, incidentally write together

const unmountChildren = children= > {
    children.forEach(child= > {
        unmount(child);
    });
};
Copy the code

render

To get n1 in render, you can store the entire N1 in a container as a _vnode attribute, as follows

const render = (n2, container) = > {
    / / for n1
    const n1 = container._vnode;

    if(! n2) { n1 && unmount(n1); }else {
        patch(n1, n2, container);
    }

    // Save n2 as n1 of the next patch
    container._vnode = n2;
}
Copy the code

patch

Patch needs to determine whether the element node type is the same or not, and call different processXXX based on the different node type to further process

const patch = (n1, n2, container) = > {
    // If n1 exists and n1 n2 nodes are of different types
    if(n1 && n1.type ! == n2.type) {// Uninstall and empty n1
        unmount(n1);
        n1 = null;
    }

    const { shapeFlag } = n2;

    Call different processxxx methods depending on the n2 node type
    if(shapeFlag & SHAPEFLAGS.TEXT) {
        processText(n1, n2, container);
    } else if (shapeFlag & SHAPEFLAGS.ELEMENT) {
        processElement(n1, n2, container);
    }
    
    // Leave a hole in the classic, later implementation
    else if(shapeFlag & ShapeFlags.COMPONENT) { processComponent(n1, n2, container); }}Copy the code

processText

In fact, processxxx method is quite simple, is a routine, according to the above flow chart to write, in addition, here can separate a patchTextNode but there is no need to write directly

const processText = (n1, n2, container) = > {
    // the presence of n1 means that n1 nodes can be reused directly
    // Just update the node content
    if (n1) {
        n2.el = n1.el;
        n1.el.textContent = n2.children;
    }

    // if n1 does not exist, it needs to be remounted
    else{ mountTextNode(n2, container); }}Copy the code

processElement

And processText similarly

const processElement = (n1, n2, container) = > {
    if (n1) {
        patchElement(n1, n2);
    } else{ mountElement(n2, container); }}Copy the code

patchElement

What patchElement has to do looks simple, but actually requires patch properties and child nodes, which is very troublesome

const patchElement = (n1, n2) = > {
    // N1 must exist, just take it
    n2.el = n1.el
    patchProps(n1.props, n2.props);
    patchChildren(n1, n2, n2.el);
}
Copy the code

patchProps

In the whole patch process, the most troublesome part is actually the patchProps and patchChildren, but the overall idea is the same. It can only be changed when changes are made. In addition, patchProps isolated a method, patchDomProp, to filter out properties that do not need comparison and can be handled directly

const patchProps = (oldProps, newProps, el) = > {
    if (oldProps === newProps) {
        return;
    }

    oldProps = oldProps || {};
    newProps = newProps || {};

    // Remove old attributes, new attributes do not have
    for (const key in oldProps) {
        // If the current attribute is 'key', skip
        if (key === 'key') {
            continue;
        }

        if (newProps[key] == null) {
            patchDomProp(oldProps[key], null, key, el); }}// Add new attributes that the old attributes do not have
    for (const key in newProps) {
        if(key === 'key') {
            continue;
        }

        if(oldProps[key] ! == newProps[key]) { patchDomProp(oldProps[key], newProps[key], key, el) } } }Copy the code

patchDomProp

The overall implementation of patchDomProp is very similar to the previous mountProps. It looks very messy and complicated. In fact, it simply removes the unnecessary attributes and adds the required attributes

const eventReg = /^on[A-Z]/;
const patchDomProps = (prev, next, key, el) = > {
    switch (key) {
        case 'class':
            el.className = next || ' ';
            break;

        case 'style':
            if (next == null) {
                el.removeAttribute('style');
            } else {
                // get rid of what you don't need
                if (prev) {
                    for (const styleName in prev) {
                        if (next[styleName] == null) {
                            el.style[styleName] = ' '; }}}// Add what is needed
                for (const styleName innext) { el.style[styleName] = next[styleName]; }}break;

        default:
            if (eventReg.test(key)) {
                // 'onClick' -> 'click'
                const eventName = key.slice(2).toLowCase();

                // get rid of what you don't need
                if (prev) {
                    el.removeEventListener(eventName, prev);
                }

                // Add what is needed
                if(next) { el.addEventListener(eventName, next); }}else {
                // get rid of what you don't need
                if (next == null || next === false) {
                    el.removeAttribute(key);
                } 
                
                // Add what is needed
                else{ el.setAttribute(key, next); }}break; }}Copy the code

patchChildren

The diff algorithm that we will talk about later is also from here, called patchKeyedChildren. This article will not introduce the diff algorithm, but leave a pit for now

PatchChilren needs to consider the comparison of different child node types, which can be said to be very troublesome. There are a lot of merged in the source code, but in fact, there are almost the same. For the sake of intuitive appearance, the following implementation does not merge for a little analysis, the child node has the following three types:

  • TEXT_CHILDREN: text child, set toa
  • ARRAY_CHILDREN: array children, set tob
  • Null: no child. Set this parameter toc

Therefore, there are 9 (3 x 3) scenarios for comparing the children of the two nodes:

  1. (a a): Both are text child nodes, just change the content
  2. (a b): The original text, now changes the element, the original content needs to be cleared, and then re-mount the element child node
  3. (a c): The original text, now missing, directly empty the original content
  4. (b a): Original element, now text, unload the original element child node, and then change the content
  5. (b b): Both are element children, patchArrayChildren, more on that later
  6. (b c): The original element, now missing, uninstall the original element child node
  7. (c a): Not before, now there is a text, mount
  8. (c b): Not before, now there are some elements, mount
  9. (c c): No one has it
const patchChildren = (n1, n2, container) = > {
    const { shapeFlag: prevShapeFlag, children: prevChildren } = n1;
    const { shapeFlag: nextShapeFlag, children: nextChildren } = n2;

    if (prevShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
        if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
            / / 1
            container.textContent = nextChildren;
        } else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
            / / 2
            container.textContent = ' ';
            mountChildren(nextChildren, container);
        } else {
            / / 3
            container.textContent = ' '; }}else if (prevShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
        if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
            / / 4
            unmountChildren(prevChildren);
            container.textContent = nextChildren;
        } else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
            / / 5
            // This is where the diff algorithm starts
            patchArrayChilren(prevChildren, nextChildren, container);
        } else {
            / / 6unmountChildren(prevChildren); }}else {
        if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
            / / case 7
            container.textContent = nextChildren;
        } else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
            / / is 8
            mountChildren(nextChildren, container);
        }
        // Case 9}}Copy the code

patchArrayChildren

With the diff algorithm, this method is called patchUnkeyedChildren

PatchArrayChildren is used to patch an array of two element child nodes. The element child nodes are essentially two arrays, but what needs to be compared is not the value, but the nodes in the array. The implementation idea is as follows

  • Extract the common length part for patch
  • Determine whether to mount or unmount the remaining parts based on the ownership of the remaining parts
const patchArrayChildren = (prev, next, container) = > {
    const oldLength = prev.length;
    const newLength = next.length;
    const commonLength = Math.min(oldLength, newLength);

    // Patch the public part
    for (let i = 0; i < commomLength; i++) {
        patch(prev[i], next[i], container);
    }

    // If the original length is larger than the current length
    // Unmount those outside the public part
    / / such as
    // Original: a b C
    // Now: a b
    if (oldLength > newLength) {
        unmountChildren(prev.slice(commomLength));
    }
    
    // If the length is now greater than the original length
    // The public part should be mounted
    / / such as
    // Original: a b
    // Now: a b C
    else if(oldLength < newLength) { mountChildren(next.slice(commomLength), container); }}Copy the code

Run the

The test code is as follows

render(
    h('ul'.null, [
        h('li'.null.'I am the first'),
        h('li'.null, [
            h('li'.null.'I'm the first of second'),
            h('li'.null.'I'm the second of second'),
            h('li'.null.'I'm the third of second'),
        ]),
        h('li'.null.'third'),]),document.body
);

setTimeout(() = > {
    render(
        h('ul'.null, [
            h('li'.null.'first'),
            h('li'.null, [
                h('li'.null.'I am the first of second'),
                h('li'.null.'I am second of second'),
                h('li'.null.'I'm the third of the second.'),
            ]),
            h('li'.null.'third'),]),document.body
    );
}, 2000);

Copy the code

The page looks like this

Two seconds later

To summarize

In fact, still similar to the previous render, most of them are some process operations, the most troublesome is the need to handle according to different situations, and the logic of thinking to be clear, so draw a picture will be very helpful to understand. The key of the whole patch is to accurately determine the minimum update unit

Q&A

Q: Are you lazy? Why does it look so easy? A: There are many types in the source code, and there is also a very important fragment type. When this fragment type is used for patch, a new attribute anchor, namely anchor point, is needed to control the insertion position. Core and the diff algorithm is also using this stuff, but this article only text node and element nodes, it is not necessary to use, and if it is wished to understand the process of patch, or a look from the beginning of a simple is better, the core principle of general process in the above, understand the element node and the operation of the text node, The rest are pretty much the same (not so much the same)

Q: How can you write so complicated, who can understand it, is your code too bad? A: In fact, as said above, has stolen a lot of lazy beggar version, so the content is much, much, much, much, much, much less, and in order to look more intuitive and easier to understand, without any coding skills, the source used a lot, a lot, a lot, a lot of chain ternary expressions of what, But personally, I think it’s the simplest if-else, which looks intuitive, so I’ll do whatever I want, just to understand the principle and the process. There are a lot of situations to consider, so most of them are flow control, and once you grasp the key principles, you should have no problem understanding them. Finally, or really too much I write write smelly can go to see the source code or others mini- Vue implementation

Q: The former core principle, the latter core principle, too stream of consciousness, what is it? A: To be honest, I really don’t find any subtle principles of this patch that are worth talking about in detail, so the key is an operation process and how two VNode trees are compared, which is quite key. So in other words, if you understand my picture above, you will understand most of this patch

The last

Put the complete code in this section for easy reference

// vnode
const SHAPEFLAGS = {
    ELEMENT: 1.TEXT: 1 << 1.TEXT_CHILDREN: 1 << 4.ARRAY_CHILDREN: 1 << 5};const Text = Symbol('Text');

// h
const h = (type, props, children) = > {
    let shapeFlag = 0;

    if (typeof type === 'string') {
        shapeFlag |= SHAPEFLAGS.ELEMENT;
    } else if (type === Text) {
        shapeFlag |= SHAPEFLAGS.TEXT;
    }

    if (typeof children === 'string') {
        shapeFlag |= SHAPEFLAGS.TEXT_CHILDREN;
    } else if (typeof children === 'number') {
        shapeFlag |= SHAPEFLAGS.TEXT_CHILDREN;
        children = children.toString();
    } else if (Array.isArray(children)) {
        shapeFlag |= SHAPEFLAGS.ARRAY_CHILDREN;
    }

    return {
        type,
        props,
        children,
        shapeFlag,
        el: null.key: props && props.key,
    };
};

// render
const render = (vnode, container) = > {
    const prevVNode = container._vnode;

    if (vnode) {
        patch(prevVNode, vnode, container);
    } else {
        prevVNode && unmount(prevVNode);
    }

    container._vnode = vnode;
};

// process
const processTextNode = (n1, n2, container) = > {
    if (n1) {
        patchTextNode(n1, n2);
    } else{ mountTextNode(n2, container); }};const processElement = (n1, n2, container) = > {
    if (n1) {
        patchElement(n1, n2);
    } else{ mountElement(n2, container); }};// patch
const patch = (n1, n2, container) = > {
    if(n1 && n1.type ! == n2.type) { unmount(n1); n1 =null;
    }

    const { shapeFlag } = n2;
    if (shapeFlag & SHAPEFLAGS.TEXT) {
        processTextNode(n1, n2, container);
    } else if (shapeFlag & SHAPEFLAGS.ELEMENT) {
        processElement(n1, n2, container);
    }
    
    // Leave a hole in the classic, later implementation
    else if(shapeFlag & ShapeFlags.COMPONENT) { processComponent(n1, n2, container); }};const patchTextNode = (n1, n2) = > {
    n2.el = n1.el;
    n1.el.textContent = n2.children;
};

const patchElement = (n1, n2) = > {
    n2.el = n1.el;

    patchProps(n1.props, n2.props, n2.el);
    patchChildren(n1, n2, n2.el);
};

const patchChildren = (n1, n2, container) = > {
    const { shapeFlag: prevShapeFlag, children: prevChildren } = n1;
    const { shapeFlag: nextShapeFlag, children: nextChildren } = n2;

    if (prevShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
        if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
            container.textContent = nextChildren;
        } else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
            container.textContent = ' ';
            mountChildren(nextChildren, container);
        } else {
            container.textContent = ' '; }}else if (prevShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
        if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
            unmountChildren(prevChildren);
            container.textContent = nextChildren;
        } else if (nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
            patchArrayChilren(prevChildren, nextChildren, container);
        } else{ unmountChildren(prevChildren); }}else {
        if (nextShapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
            container.textContent = nextChildren;
        } else if(nextShapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) { mountChildren(nextChildren, container); }}};const patchArrayChilren = (prev, next, container) = > {
    const oldLength = prev.length;
    const newLength = next.length;
    const commomLength = Math.min(oldLength, newLength);

    for (let i = 0; i < commomLength; i++) {
        patch(prev[i], next[i], container);
    }

    if (oldLength > newLength) {
        unmountChildren(prev.slice(commomLength));
    } else if(oldLength < newLength) { mountChildren(next.slice(commomLength), container); }};const patchProps = (oldProps, newProps, el) = > {
    if (oldProps === newProps) {
        return;
    }

    for (const key in oldProps) {
        if (newProps[key] == null) {
            patchDomProps(oldProps[key], null, key, el); }}for (const key in newProps) {
        const prev = oldProps[key];
        const next = newProps[key];

        if(prev ! == next) { patchDomProps(prev, next, key, el); }}};const eventReg = /^on[A-Z]/;
const patchDomProps = (prev, next, key, el) = > {
    switch (key) {
        case 'class':
            el.className = next || ' ';
            break;

        case 'style':
            if (next == null) {
                el.removeAttribute('style');
            } else {
                if (prev) {
                    for (const styleName in prev) {
                        if (next[styleName] == null) {
                            el.style[styleName] = ' '; }}}for (const styleName innext) { el.style[styleName] = next[styleName]; }}break;

        default:
            if (eventReg.test(key)) {
                const eventName = key.slice(2).toLowerCase();

                if (prev) {
                    el.removeEventListener(eventName, prev);
                }

                if(next) { el.addEventListener(eventName, next); }}else {
                if (next == null || next === false) {
                    el.removeAttribute(key);
                } else{ el.setAttribute(key, next); }}break; }};// mount
const mountTextNode = (vnode, container) = > {
    const textNode = document.createTextNode(vnode.children);
    container.appendChild(textNode);

    vnode.el = textNode;
};

const mountElement = (vnode, container) = > {
    const { type, props, children, shapeFlag } = vnode;

    const el = document.createElement(type);

    patchProps(null, props, el);

    if (shapeFlag & SHAPEFLAGS.TEXT_CHILDREN) {
        mountTextNode(vnode, el);
    } else if (shapeFlag & SHAPEFLAGS.ARRAY_CHILDREN) {
        mountChildren(children, el);
    }

    container.appendChild(el);
    vnode.el = el;
};

const mountChildren = (children, container) = > {
    children.forEach(child= > {
        patch(null, child, container);
    });
};

// unmount
const unmount = vnode= > {
    const { el } = vnode;

    el.parentNode.removeChild(el);
};

const unmountChildren = children= > {
    children.forEach(child= > {
        unmount(child);
    });
};

// test
render(
    h('ul'.null, [
        h('li'.null.'I am the first'),
        h('li'.null, [
            h('li'.null.'I'm the first of second'),
            h('li'.null.'I'm the second of second'),
            h('li'.null.'I'm the third of second'),
        ]),
        h('li'.null.'third'),]),document.body
);

setTimeout(() = > {
    render(
        h('ul'.null, [
            h('li'.null.'first'),
            h('li'.null, [
                h('li'.null.'I am the first of second'),
                h('li'.null.'I am second of second'),
                h('li'.null.'I'm the third of the second.'),
            ]),
            h('li'.null.'third'),]),document.body
    );
}, 2000);
Copy the code