Patch for renderer

In the previous chapter we explained and implemented the mount logic of the renderer, which is essentially the process of rendering various types of VNodes into real DOM. In addition to mounting the new VNode into the real DOM, the renderer’s other responsibility is to compare the old VNode with the new VNode and update the DOM in the appropriate way, also known as patch. In addition to understanding the basic comparison logic, this chapter also explains the principles you should follow when comparing old and new VNodes. Let’s get started!

The basic principle of

Re-render usually starts with the update of the component, because the UI is automatically updated within the framework by changing the data state at the framework level, but the update of the component is essentially an update of the real DOM, or tag elements. So let’s first look at how to update a tag element.

Let’s first review the renderer code as follows:

function render(vnode, container) {
    const prevVNode = container.vnode
    if (prevVNode == null) {
        if (vnode) {
            // There is no old VNode. Use 'mount' to mount a new VNode
            mount(vnode, container)
            // Add the new VNode to the container. VNode property so that the old VNode exists for the next rendering
            container.vnode = vnode
        }
    } else {
        if (vnode) {
            // If there is an old VNode, call 'patch' to patch it
            patch(prevVNode, vnode, container)
            / / update the container. Vnode
            container.vnode = vnode
        } else {
            // There is an old VNode but no new VNode, which indicates that the DOM should be removed and the removeChild function can be used in the browser.
            container.removeChild(prevVNode.el)
            container.vnode = null}}}Copy the code

As the two lines highlighted above show, when a brand new VNode is rendered using the Render renderer, the mount function is called to mount the VNode and the container element stores a reference to the VNode object. So when the renderer is called again to render a new VNode object to the same container element, the old VNode already exists, so patch is called to update it in the appropriate way, as shown in the following code:

/ / the old VNode
const prevVNode = h('div')
/ / new VNode
const nextVNode = h('span')
// The first time you render VNode to #app, the mount function is called
render(prevVNode, document.getElementById('app'))
// Render a new VNode to the same #app element a second time, calling patch
render(nextVNode, document.getElementById('app'))
Copy the code

The patch function will compare the old and new VNodes, also known as diff. Then what comparison rules should be followed between two different VNodes? In fact, this question is easy to answer. We know that there are types of VNodes, and there are certain differences between different types of VNodes. Therefore, the first comparison principle between different VNodes is: Only vNodes of the same type can be compared. For example, we have two VNodes, one of which is of label element type and the other is of component type. When these two VNodes are compared, The optimal method is to completely replace the old VNode with the new VNode. In other words, we do not do any comparison operation at all, because it is completely meaningless. Therefore, according to this idea, we implement the patch function as follows:

function patch(prevVNode, nextVNode, container) {
    // Get the new and old VNode types respectively, i.e. Flags
    const nextFlags = nextVNode.flags
    const prevFlags = prevVNode.flags
    // Check whether the types of the old and new vNodes are the same. If the types are different, call replaceVNode directly to replace the VNode
    // If the old and new vNodes are of the same type, different comparison functions are called according to the different types
    if(prevFlags ! == nextFlags) { replaceVNode(prevVNode, nextVNode, container) }else if (nextFlags & VNodeFlags.ELEMENT) {
        patchElement(prevVNode, nextVNode, container)
    } else if (nextFlags & VNodeFlags.COMPONENT) {
        patchComponent(prevVNode, nextVNode, container)
    } else if (nextFlags & VNodeFlags.TEXT) {
        patchText(prevVNode, nextVNode)
    } else if (nextFlags & VNodeFlags.FRAGMENT) {
        patchFragment(prevVNode, nextVNode, container)
    } else if (nextFlags & VNodeFlags.PORTAL) {
        patchPortal(prevVNode, nextVNode)
    }
}
Copy the code

As shown in the code above, since the function of patch is to compare the old and new VNodes, the patch function must accept the old and new vnodes as parameters. The prevVNode parameter represents the old VNode, and the nextVNode parameter represents the new VNode. First we need to get the new VNode type (flags), followed by a series of if… Else if statement, its core principle is: if the type is different, call replaceVNode directly to replace the old VNode with the new VNode, otherwise call the matching function according to the different type, as shown in the following figure:

Replace the VNode

Let’s first examine how to replace a VNode, that is, what the replaceVNode function should do. Let’s first reproduce the scenario where a VNode needs to be replaced, as shown in the following code:

// The old VNode is a div tag
const prevVNode = h('div'.null.'the old VNode')
class MyComponent {
    render () {
        return h('h1'.null.'new VNode')}}// The new VNode is a component
const nextVNode = h(MyComponent)
// Render old and new vNodes to #app successively
render(prevVNode, document.getElementById('app'))
render(nextVNode, document.getElementById('app'))
Copy the code

In the code above, we render the old and new vNodes to the #app element. Since the old and new vnodes have different types, this will trigger a VNode replacement operation. The replacement operation is not complicated, essentially removing the DOM rendered by the old VNode and mounting the new VNode. Here is an implementation of the replaceVNode function:

function replaceVNode(prevVNode, nextVNode, container) {
    // Remove the old VNode rendered DOM from the container
    container.removeChild(prevVNode.el)
    // Mount the new VNode to the container
    mount(nextVNode, container)
}
Copy the code

It looks simple, but these two lines of code are flawed. We will explain the drawbacks later in this chapter, because we have not provided enough background so far.

Updating tag elements

Basic principles for updating tag elements

If the old VNode and the new VNode have different types, the replaceVNode function is called to replace the old VNode with the new VNode. However, if the old and new VNodes are of the same type, different comparison functions will be called depending on the type. In this section we will look at how to update a tag element.

First, even if two VNodes are of the same type as tag elements, they may be different tags, meaning they have different tag attributes. This leads to the principle of renewal: For example, the UL tag can only render the Li tag, so it doesn’t make sense to compare the UL tag with a DIV tag. In this case, we don’t patch the old tag element, but replace it with a new one. To do this, we need to use the replaceVNode function we discussed earlier, as shown in the following patchElement function:

function patchElement(prevVNode, nextVNode, container) {
    // If the old and new vnodes describe different labels, the replaceVNode function is called to replace the old VNode with the new VNode
    if(prevVNode.tag ! == nextVNode.tag) { replaceVNode(prevVNode, nextVNode, container)return}}Copy the code

What if old and new VNodes describe the same tags? If the tag is the same, the difference between two VNodes will only be between VNodeData and children, so the comparison between two Vnodes describing the same tag is essentially a comparison between VNodeData and children. Let’s first look at how to update VNodeData, as shown in the following two vnodes:

/ / the old VNode
const prevVNode = h('div', {
    style: {
        width: '100px'.height: '100px'.backgroundColor: 'red'}})/ / new VNode
const nextVNode = h('div', {
    style: {
        width: '100px'.height: '100px'.border: '1px solid green'}})Copy the code

As the code above shows, both the old and new vNodes describe div tags, but they have different styles. The old VNode describes a div with a red background, while the new VNode describes a div with a green border. For this case only, our update rule would be: Remove the red background from the element before adding a green border to the element. If we macroscopize the solution to the problem, we apply the new VNodeData to all elements and remove the data that no longer exists on the new VNodeData from the elements. With this in mind, we add the following highlighted code to the patchElement function:

function patchElement(prevVNode, nextVNode, container) {
    // If the old and new vnodes describe different labels, the replaceVNode function is called to replace the old VNode with the new VNode
    if(prevVNode.tag ! == nextVNode.tag) { replaceVNode(prevVNode, nextVNode, container)return
    }
    // Get the el element and make nextvNode. el reference that element as well
    const el = (nextVNode.el = prevVNode.el)
    // Get old and new VNodeData
    const prevData = prevVNode.data
    const nextData = nextVNode.data
    // The update is necessary only when the new VNodeData exists
    if (nextData) {
        // Iterate over the new VNodeData
        for (let key in nextData) {
            // Get the old and new VNodeData values according to the key
            const prevValue = prevData[key]
            const nextValue = nextData[key]
            switch (key) {
                case 'style':
                    // Iterate over the style data in the new VNodeData to apply the new style to the element
                    for (let k in nextValue) {
                        el.style[k] = nextValue[k]
                    }
                    // Iterate over the style data in the old VNodeData and remove the data that no longer exists in the new VNodeData
                    for (let k in prevValue) {
                        if(! nextValue.hasOwnProperty(k)) { el.style[k] =' '}}break
                default:
                    break}}}}Copy the code

As the code highlighted above shows, our thinking when updating VNodeData is divided into the following steps:

  • Step 1: Be newVNodeDataExisting, traversal the newVNodeData.
  • Step 2: According to the newVNodeDataIn thekey, try to read the old value and the new value respectively, i.eprevValuenextValue.
  • Step 3: Useswitch... caseStatement matches different data for different update operations

Take the style update as an example, as shown in the code above:

  • 1: Iterate over the new style data (prevValue), apply all the new style data to the element
  • 2: Iterate over old style data (nextValue), remove styles from elements that no longer exist in the new style data, and finally we finish updating the element style.

This process is essentially the basic rule for updating tag elements.

Update VNodeData

Looking at the code we used to update the style in the patchElement function, do you notice any deja vu? Yes, this code is very similar to the code used to process VNodeData in the mountElement function, which guides us to encapsulate a function to process VNodeData uniformly. In fact, both the mountElement code and the patchElement code used to process VNodeData are essentially applying data from VNodeData to DOM elements. The only difference is that there is no “old” data in the mountElement function, and there is both old and new data in the patchElement function, so we can encapsulate a function called patchData that takes old and new data as arguments. As for the mountElement function, it has no old data to speak of, so it only needs to pass NULL as old data when calling patchData function.

We will first use the patchData function to modify the code of the patchElement function as follows:

function patchElement(prevVNode, nextVNode, container) {
    // If the old and new vnodes describe different labels, the replaceVNode function is called to replace the old VNode with the new VNode
    if(prevVNode.tag ! == nextVNode.tag) { replaceVNode(prevVNode, nextVNode, container)return
    }
    // Get the el element and make nextvNode. el reference that element as well
    const el = (nextVNode.el = prevVNode.el)
    const prevData = prevVNode.data
    const nextData = nextVNode.data
    if (nextData) {
        // Iterate over the new VNodeData, passing the old and new values to the patchData function
        for (let key in nextData) {
            const prevValue = prevData[key]
            const nextValue = nextData[key]
            patchData(el, key, prevValue, nextValue)
        }

    }

    if (prevData) {
        // Iterate over the old VNodeData to remove the data that no longer exists in the new VNodeData
        for (let key in prevData) {
            const prevValue = prevData[key]
            if(prevValue && ! nextData.hasOwnProperty(key)) {// The fourth argument is null, which means to remove data
                patchData(el, key, prevValue, null)}}}}Copy the code

As shown in the code highlighted above, the code rewritten by using the patchData function is much simpler than before. The core idea remains unchanged: Traversing the new VNodeData, passing the old value and the new value to the patchData function, which is responsible for updating data; We also need to iterate over the old VNodeData, removing the data that is no longer in the new VNodeData from the element, so we can see that if there is no old data in the old VNodeData, or if there is old data but the old data is no longer in the new data, The fourth parameter we pass to the patchData function is null, which means that the data is removed from the element. The following is the implementation of the patchData function, which essentially moves the switch statement block in the original patchElement function to the patchData function:

export function patchData(el, key, prevValue, nextValue) {
    switch (key) {
        case 'style':
            // Apply the new style data to the element
            for (let k in nextValue) {
                el.style[k] = nextValue[k]
            }
            // Remove the style that no longer exists
            for (let k in prevValue) {
                if(! nextValue.hasOwnProperty(k)) { el.style[k] =' '}}break
        default:
            break}}Copy the code

Of course, the code in the above patchData function only contains the processing of style data. In fact, we can copy the complete code used to process VNodeData from the mountElement function in the previous chapter into the patchData function as follows:

export function patchData(el, key, prevValue, nextValue) {
    switch (key) {
        case 'style':
            for (let k in nextValue) {
                el.style[k] = nextValue[k]
            }
            for (let k in prevValue) {
                if(! nextValue.hasOwnProperty(k)) { el.style[k] =' '}}break
        case 'class':
            el.className = nextValue
            break
        default:
            if (key[0= = ='o' && key[1= = ='n') {
                / / event
                el.addEventListener(key.slice(2), nextValue)
            } else if (domPropsRE.test(key)) {
            // Treat as a DOM Prop
                el[key] = nextValue
            } else {
                // Treat as Attr
                el.setAttribute(key, nextValue)
            }
        break}}Copy the code

In this way, the patchData function can be used to handle style, class, DOM Prop and Attr updates, and can meet the requirements of both mountElement and patchElement. However, the patchData function can not meet the update operation of events, because when the new VNodeData does not contain an event, we need to remove the old event callback function, the solution is simple as follows:

export function patchData(el, key, prevValue, nextValue) {
    switch (key) {
        case 'style':
            // omit processing style code...
        case 'class':
            // omit class code...
        default:
            if (key[0= = ='o' && key[1= = ='n') {
                / / event
                // Remove the old event
                if (prevValue) {
                    el.removeEventListener(key.slice(2), prevValue)
                }
            // Add a new event

            if (nextValue) {
                el.addEventListener(key.slice(2), nextValue)
            } else if (domPropsRE.test(key)) {
                // Treat as a DOM Prop
                el[key] = nextValue
            } else {
                // Treat as Attr
                el.setAttribute(key, nextValue)
            }
            break}}Copy the code

As the code highlighted above shows, we remove the old event callback from the DOM element if it exists, and then add the new event callback to the DOM element if it exists. At this point, our patchData function is done.

Update child nodes

When the VNodeData update is complete, the difference between the old and new tags is the child node, so the last thing we need to do in the patchElement function is to update the child node recursively, as shown in the code highlighted below:

function patchElement(prevVNode, nextVNode, container) {
    // If the old and new vnodes describe different labels, the replaceVNode function is called to replace the old VNode with the new VNode
    if(prevVNode.tag ! == nextVNode.tag) { replaceVNode(prevVNode, nextVNode, container)return
    }

    // Get the el element and make nextvNode. el reference that element as well
    const el = (nextVNode.el = prevVNode.el)
    const prevData = prevVNode.data
    const nextData = nextVNode.data

    if (nextData) {
        // Iterate over the new VNodeData, passing the old and new values to the patchData function
        for (let key in nextData) {
            const prevValue = prevData[key]
            const nextValue = nextData[key]
            patchData(el, key, prevValue, nextValue)
        }
    }
    if (prevData) {
        // Iterate over the old VNodeData to remove the data that no longer exists in the new VNodeData
        for (let key in prevData) {
            const prevValue = prevData[key]
            if(prevValue && ! nextData.hasOwnProperty(key)) {// The fourth argument is null, which means to remove data
                patchData(el, key, prevValue, null)}}}// Call patchChildren recursively to update the child node
    patchChildren(
        prevVNode.childFlags, // The type of the old VNode child
        nextVNode.childFlags, // The type of the new VNode child
        prevVNode.children, // Old VNode child node
        nextVNode.children, // New VNode child node
        el // The current label element, which is the parent of these child nodes)}Copy the code

At the end of the patchElement function, we call the patchChildren function, which is used to compare the child nodes of the old and new VNodes at the same level. It receives five parameters, The first four parameters are the old and new VNode children and their types, and the fifth parameter el is the parent of those children, which is the label element that is currently being updated.

Before we start implementing updates to children at the same level, we need to think about how we should do this based on what we know so far, and the idea is to be able to write code. We look at the children of the following two div tags, which we represent as vnodes:

const prevVNode = h('div'.null, h('span'))
const nextVNode = h('div')

Copy the code

As you can see from the code above, the prevVNode div tag has only one child, so prevVNode’s child type should be childrenFlags.single_vnode, while nextVNode’s div tag has no children. So the nextVNode child type should be childrenFlags.no_children. If we just look at this example, how do we update it? Simply remove the prevVNode child. Let’s look at the following two vNodes:


const prevVNode = h('div')
const nextVNode = h('div'.null, h('span'))

Copy the code

PrevVNode has no child node and nextVNode has one child, So the prevVNode and nextVNode children are childrenFlags. NO_CHILDREN and childrenFlags. SINGLE_VNODE respectively. Simply mount the children of nextVNode to the div tag. Consider the following example:


const prevVNode = h('div'.null, h('p'))
const nextVNode = h('div'.null, h('span'))

Copy the code

In this example, both the old and new DIV tags have a child node, so their child nodes are of the same type. In this case, the update operation of the child node is equivalent to the patch between the two child nodes.

Through these examples, we noticed that according to the different types of child nodes of the old and new labels, we could easily find appropriate ways to update them. We have emphasized that the type identification of VNode is very important information in the patch stage, which is reflected here.

However, whether it is a new label or an old label, the child node of the label can be divided into three cases: only one child node, no child node, and multiple child nodes. The type of the child node of a label is known by the childFlags property of the VNode object to which the label corresponds. Finally, under the guidance of this idea, we can write the patchChildren function, as shown in the following code:


function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {

switch (prevChildFlags) {

// The old children was a single child that would execute the case block

case ChildrenFlags.SINGLE_VNODE:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

// If the new children is also a single child, the case block is executed

break

case ChildrenFlags.NO_CHILDREN:

// The case block is executed when there are no children in the new children

break

default:

// The case block is executed when there are multiple children in the new children

break

}

break

// This case block is executed when there are no children in the old children

case ChildrenFlags.NO_CHILDREN:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

// If the new children is a single child, the case block is executed

break

case ChildrenFlags.NO_CHILDREN:

// The case block is executed when there are no children in the new children

break

default:

// The case block is executed when there are multiple children in the new children

break

}

break

// The old children block is executed when there are multiple children

default:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

// If the new children is a single child, the case block is executed

break

case ChildrenFlags.NO_CHILDREN:

// The case block is executed when there are no children in the new children

break

default:

// The case block is executed when there are multiple children in the new children

break

}

break}}Copy the code

As shown in the code above, which looks long but regular, we use nested switches… Case statement, outer switch… The case statement matches the old children type, and the inner switch… The case statement matches the new children type. Since there are three cases of both the old and the new children, there are nine cases (3 * 3) in total. According to different cases, we will do different operations. Next, we will implement the patchChildren function one by one. When we have implemented all the update operations in these nine cases, our patchChildren function will be completed.

SINGLE_VNODE: childrenFlags.single_vnode: childrenFlags.single_vnode That is, both old and new children are single child nodes. As mentioned above, in this case, the comparison between old and new children is equivalent to the comparison between two children(single child node), so it only needs to recursively call the patch function, as follows:


function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {

switch (prevChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

PrevChildren and nextChildren are VNode objects

patch(prevChildren, nextChildren, container)

break

case ChildrenFlags.NO_CHILDREN:

// The case block is executed when there are no children in the new children

break

default:

// The case block is executed when there are multiple children in the new children

break

}

break


/ / to omit...}}Copy the code

As the highlighted code above shows, it only takes one line of code to do this. Let’s write a case to test our code:


/ / the old VNode

const prevVNode = h('div'.null,

h('p', {

style: {

height: '100px'.width: '100px'.background: 'red'}}))/ / new VNode

const nextVNode = h('div'.null,

h('p', {

style: {

height: '100px'.width: '100px'.background: 'green'

}

})

)


render(prevVNode, document.getElementById('app'))


// Update in 2 seconds

setTimeout(() = > {

render(nextVNode, document.getElementById('app'))},2000)

Copy the code

As you can see from the code above, both old and new vNodes describe div tags that have only one P tag as a child node, so the children type of both old and new div tags is a single child node, but the p tags have different background colors. We then call the Render renderer to render both vNodes successively, and the result is that the background color of the P tag is correctly updated.

Then let’s look at what happens when the old children type is childrenFlags.single_vnode and the new children type is childrenFlags.no_children. That is, the old children is a single child node, and the new children is null, that is, the new VNode has no children node. In this case we just need to remove the old child node, as shown in the following code:


function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {

switch (prevChildFlags) {

// The old children was a single child that would execute the case block

case ChildrenFlags.SINGLE_VNODE:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

// If the new children is also a single child, the case block is executed

patch(prevChildren, nextChildren, container)

break

case ChildrenFlags.NO_CHILDREN:

// The case block is executed when there are no children in the new children

container.removeChild(prevChildren.el)

break

default:

// The case block is executed when there are multiple children in the new children

break

}

break

/ / to omit...}}Copy the code

As the code highlighted above shows, container is the parent element, and we call removeChild to remove the prevchildren. el from the parent element, again using only one line of code. If prevChildren is a fragment, it may render multiple elements into the container, so we need to do extra processing for vNodes of the fragment type. But the essence remains the same: find a way to remove already rendered DOM elements from the page.

Finally, we tested our code with the following example:


/ / the old VNode

const prevVNode = h(

'div'.null,

h('p', {

style: {

height: '100px'.width: '100px'.background: 'red'The \}}))/ / new VNode

const nextVNode = h('div')


render(prevVNode, document.getElementById('app'))


// Update in 2 seconds

setTimeout(() = > {

render(nextVNode, document.getElementById('app'))},2000)

Copy the code

PrevVNode is a div with a p tag on a red background as a child, and nextVNode is a DIV with no children. We then render both the old and new vNodes, with the p tag removed.

When the old children type is childrenFlags. SINGLE_VNODE and the new children type is childrenFlags. SINGLE_VNODE, in this case, the old children are one and the new children are multiple, Therefore, we can remove the old single child node and mount the new multiple child nodes. Based on this idea, we can make the following implementation and modify our patchChildren function:


function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {

switch (prevChildFlags) {

// The old children was a single child that would execute the case block

case ChildrenFlags.SINGLE_VNODE:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

patch(prevChildren, nextChildren, container)

break

case ChildrenFlags.NO_CHILDREN:

container.removeChild(prevChildren.el)

break

default:

// Remove the old single child node

container.removeChild(prevChildren.el)

// Iterate over the new child nodes and mount them into the container one by one

for (let i = 0; i < nextChildren.length; i++) {

mount(nextChildren[i], container)

}

break

}

break


/ / to omit...}}Copy the code

As the code highlighted above shows, it is also very simple to implement. We remove the old single child node using the same method as before, and then iterate over the new child nodes, calling the mount function to mount them into the container one by one. We can test our code using the following example:


/ / the old VNode

const prevVNode = h('div'.null, h('p'.null.'Only one child node'))


/ / new VNode

const nextVNode = h('div'.null, [

h('p'.null.'Child node 1'),

h('p'.null.'Child node 2')

])


render(prevVNode, document.getElementById('app'))

// Update in 2 seconds

setTimeout(() = > {

render(nextVNode, document.getElementById('app'))},2000)


Copy the code

As shown in the code above, the old VNode was a DIV tag with only one child node, while the new VNode is a DIV tag with multiple child nodes. The end result is that the old single child node is removed and the new multiple child nodes are all added.

Above we explain and implement the update operation in all cases when the old children type is a single child node, which can be summarized in a diagram as follows:

Similarly, when the old children type is childrenFlags.no_children, that is, there are no children, the new children can still have three different cases, which can also be represented by a graph:

Let’s explain how this works:

  • Case 1: There is no old child node and the new child node is a single child node. In this case, you only need to add the new single child node to the container element.

  • Case two: there are no old children and no new children, so there is nothing to do.

  • Case 3: There are no old child nodes, but there are many new child nodes, then add all these child nodes to the container element.

Based on this, we can easily write the corresponding logic, as shown in the patchChildren function below:


function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {

switch (prevChildFlags) {

/ / to omit...


// This case block is executed when there are no children in the old children

case ChildrenFlags.NO_CHILDREN:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

// If the new children is a single child, the case block is executed

// Use the mount function to mount the new child node to the container element

mount(nextChildren, container)

break

case ChildrenFlags.NO_CHILDREN:

// The case block is executed when there are no children in the new children

// Do nothing

break

default:

// The case block is executed when there are multiple children in the new children

// Iterate over multiple new child nodes, mounting each to the container element using the mount function

for (let i = 0; i < nextChildren.length; i++) {

mount(nextChildren[i], container)

}

break

}

break


/ / to omit...}}Copy the code

Now we have one last case left for the old children type, which is when the old children type has multiple children. Again, let’s draw a graph:

As shown in the figure above, when the old children type has multiple children nodes, the new children type has three cases, and different cases adopt different operations:

  • Case 1: There are multiple old child nodes, but the new child node is a single child node. In this case, all the old child nodes need to be removed, and the new single child node can be added to the container element.

  • Case 2: There are multiple old child nodes, but no new child nodes. In this case, you only need to remove all the old child nodes.

  • Case three: Old and new child nodes are multiple child nodes, which brings us to the crucial step, where the core Diff algorithm comes in.

In fact, in the whole comparison between old and new children, only when the old and new child nodes are multiple children is it necessary to carry out the real core diff, so as to reuse the child nodes as much as possible.

For case 1 and case 2, this is fairly easy to implement, as shown in the following code:


function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {

switch (prevChildFlags) {

/ / to omit...


default:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

for (let i = 0; i < prevChildren.length; i++) {

container.removeChild(prevChildren[i].el)

}

mount(nextChildren, container)

break

case ChildrenFlags.NO_CHILDREN:

for (let i = 0; i < prevChildren.length; i++) {

container.removeChild(prevChildren[i].el)

}

break

default:

// The case block is executed when there are multiple children in the new children

break

}

break}}Copy the code

As shown in the code highlighted above, for the case where the new children is a single child node, we remove the old children from the container element one by one, and mount the new child node to the container element by calling mount function. For the case where the new children is no child node, We simply iterate over the old child nodes and remove all of them from the container element. In fact, in the whole process of children’s patch, the last situation is the most complicated: Old and the new child node is more child nodes, in this case, the update operation will be complicated, because we have to “own” the demand is higher, because the hypothesis according to the previous ideas we can adopt “removes old all child nodes, and then add all the new child node” train of thought to complete the update, so many things will be simple, But while this serves the ultimate purpose, all UPDATES to the DOM have no reuse at all. Limited by the space of this chapter, we temporarily adopt simple methods to complete the updating of child nodes. We will focus on the real core DIff algorithm in the next chapter. The simplified version is as follows:


function patchChildren(prevChildFlags, nextChildFlags, prevChildren, nextChildren, container) {

switch (prevChildFlags) {

/ / to omit...


// The old children block is executed when there are multiple children

default:

switch (nextChildFlags) {

case ChildrenFlags.SINGLE_VNODE:

for (let i = 0; i < prevChildren.length; i++) {

container.removeChild(prevChildren[i].el)

}

mount(nextChildren, container)

break

case ChildrenFlags.NO_CHILDREN:

for (let i = 0; i < prevChildren.length; i++) {

container.removeChild(prevChildren[i].el)

}

break

default:

// Iterate over the old child nodes and remove all of them

for (let i = 0; i < prevChildren.length; i++) {

container.removeChild(prevChildren[i].el)

}

// Iterate over the new child nodes and add them all

for (let i = 0; i < nextChildren.length; i++) {

mount(nextChildren[i], container)

}

break

}

break}}Copy the code

As highlighted in the code above, we first iterate over the old child nodes, removing them all from the container element. The new child nodes are then iterated over and all added to the container element. This completes the update, but again: we’re doing this for space, and for the sake of the code for the following example, in the next chapter we’ll focus on how to reuse as many children as possible when both the old and the new are multiple.

Update text node

We spent a lot of time talking about updating tag elements, which are actually the main operation in DOM updates, and then we’ll talk about updating text nodes. If the types of the old and new vNodes are both plain text, the patchText function will be called inside the patch to update the old text node. Updating a text node is very simple. If a DOM element is a text node or comment node, the content of the text node (or comment node) can be read or set by calling the nodeValue property of the DOM object, for example:


// Create a text node

const textEl = document.createTextNode('a')
textEl.nodeValue // 'a'
textEl.nodeValue = 'b'
textEl.nodeValue // 'b'
Copy the code

Using this, we can easily implement the update of text elements. The implementation of patchText function is as follows:


function patchText(prevVNode, nextVNode) {

// Take the text element el and make nextvNode. el point to that text element

const el = (nextVNode.el = prevVNode.el)

// Updates are necessary only if the old and new text contents are inconsistent

if(nextVNode.children ! == prevVNode.children) { el.nodeValue = nextVNode.children } }Copy the code

The patchText function takes the old and new VNodes as arguments. First we need to get the text node element that has been rendered on the page using the old prevvNode. el property and have NextvNode. el point to it. The children property of VNode stores the text content of VNode. Therefore, the children property of VNode stores the text content of VNode. The children property of VNode stores the text content of VNode, so the children property of VNode stores the text content of VNode, so the children property of VNode stores the text content of VNode. This completes the update of the text node.

We can test our code using the following example:


/ / the old VNode

const prevVNode = h('p'.null.'Old text')


/ / new VNode

const nextVNode = h('p'.null.'New text')


render(prevVNode, document.getElementById('app'))

// Update in 2 seconds

setTimeout(() = > {

render(nextVNode, document.getElementById('app'))},2000)

Copy the code

We created two P tags with text child nodes and called the Render renderer to render the old VNode and the new VNode. The end result is that the text is updated two seconds later.

Update the fragments

If the types of both VNodes are fragments, the Patch function calls the patchFragment function to update the contents of the fragments. In fact, the update of the fragment is a simplified version of the update of the tag element. As we know, the update process of the tag element is divided into two steps: first, the VNodeData of the tag itself needs to be updated, and then its child nodes need to be updated. However, since the Fragment does not wrap elements, only child nodes, our update of the Fragment is essentially updating the “child nodes” of the two fragments.

The patchFragment function is implemented as follows:


function patchFragment(prevVNode, nextVNode, container) {

// Call patchChildren to update the child nodes of the old and new fragments

patchChildren(

prevVNode.childFlags, // The child node type of the old fragment

nextVNode.childFlags, // The child node type of the new fragment

prevVNode.children, // The child node of the old fragment

nextVNode.children, // The child node of the new fragment

container

)

}

Copy the code

As shown in the code above, we simply call patchChildren to update the child nodes of the old and new fragments, but don’t forget to update the nextvNode. el property, just like we did when we implemented mountFragment. VNode references different elements. We add the following code to patchFragment:


function patchFragment(prevVNode, nextVNode, container) {

// Call patchChildren to update the child nodes of the old and new fragments

patchChildren(

prevVNode.childFlags, // The child node type of the old fragment

nextVNode.childFlags, // The child node type of the new fragment

prevVNode.children, // The child node of the old fragment

nextVNode.children, // The child node of the new fragment

container

)


switch (nextVNode.childFlags) {

case ChildrenFlags.SINGLE_VNODE:

nextVNode.el = nextVNode.children.el

break

case ChildrenFlags.NO_CHILDREN:

nextVNode.el = prevVNode.el

break

default:

nextVNode.el = nextVNode.children[0].el

}

}

Copy the code

As highlighted in the code above, we check the children type of the new fragment. If the children type of the new fragment is a single child node, it means that the value of the vNode. Children property is the vNode object. So nextvNode.children. el is assigned to nextvNode.el. If the new fragment doesn’t have child nodes, we know that we use an empty text node placeholder for the fragment that doesn’t have child nodes, and the prevvNode. el property references that empty text node, So we get the empty text element from the old fragment prevvNode. el and assign it to the new fragment nextvNode. el. If the type of the new fragment is multiple children, nextvNode. children is a VNode array, and we make the nextvNode. el property of the new fragment refer to the first element in the array. In fact, this logic is consistent with the logic we implemented in the mountFragment function.

We can test our code using the following example:


/ / the old VNode

const prevVNode = h(Fragment, null, [

h('p'.null.'Old segment child node 1'),

h('p'.null.'Old segment child node 2')])/ / new VNode

const nextVNode = h(Fragment, null, [

h('p'.null.'New movie node 1'),

h('p'.null.'New Movie sub node 2')

])


render(prevVNode, document.getElementById('app'))

// Update in 2 seconds

setTimeout(() = > {

render(nextVNode, document.getElementById('app'))},2000)

Copy the code

In the code above, we created the old fragment and the new fragment, and rendered it using the renderer one after the other. As a result, the fragment was updated correctly.

Update the Portal

If the types of both VNodes are Portal, the patchPortal function will be called inside the patch function to update the vNodes. We made a loose but intuitive analogy in the chapter “Mounting renderers” : think of Portal as a Fragment that can be mounted anywhere. In fact, the update of Portal is similar to that of Fragment. We need to update its children. However, since portals can be mounted everywhere, the mounting target of old and new portals may be different. If the mount target of the new Portal changes, we need to move the contents of the Portal from the old container to the new container. We start by updating the Portal child node, as shown in the code below, the same as updating the Fragment child node:


patchPortal (prevVNode, nextVNode){

patchChildren(

prevVNode.childFlags,

nextVNode.childFlags,

prevVNode.children,

nextVNode.children,

prevVNode.tag // Notice that the container element is the old container

)


// Make nextvNode. el point to prevvNode. el

nextVNode.el = prevVNode.el

}

Copy the code

As shown in the above code, we first call patchChildren function to update the child node of Portal. It should be noted that the fifth parameter of patchChildren is the old mount container, that is to say, even if the mount target of the new Portal is changed, However, the contents of the Portal remain in the old container after this step is updated. El is much easier than Fragment because we know that the EL property is always a placeholder text node for a Portal-type VNode.

After the above work is done, the question to consider is the mount target. Since the mount target of the old and new Portals may be different, for example:

// Mount the element with id="box1"
const prevPortal = h(Portal, { target: '#box1' }, h('div'))
// Mount the target element with id="box2"
const nextPortal = h(Portal, { target: '#box2' }, h('div'))
Copy the code

As you can see, the old Portal is mounted to a container element with ID =”box1″, while the new Portal is mounted to a container element with ID =”box2″. However, since the container elements passed to the patchChildren function are always the old container elements in the process of updating the child nodes, the final result is: The updated child node also exists in the old container, so we still need to do the last step, which is to move the elements in the old container to the new container. We add the following code to the patchPortal function:


function patchPortal(prevVNode, nextVNode) {

patchChildren(

prevVNode.childFlags,

nextVNode.childFlags,

prevVNode.children,

nextVNode.children,

prevVNode.tag // Note that the container is old

)

// Make nextvNode. el point to prevvNode. el

nextVNode.el = prevVNode.el

\


// If the new container is different from the old one, it needs to be moved

if(nextVNode.tag ! == prevVNode.tag) {// Get a new container element, the mount target

const container =

typeof nextVNode.tag === 'string'

? document.querySelector(nextVNode.tag)

: nextVNode.tag


switch (nextVNode.childFlags) {

case ChildrenFlags.SINGLE_VNODE:

// If the new Portal is a single child node, move that node to the new container

container.appendChild(nextVNode.children.el)

break

case ChildrenFlags.NO_CHILDREN:

// The new Portal has no child nodes and does not need to be transported

break

default:

// If the new Portal is multiple child nodes, traverse them one by one to move them to the new container

for (let i = 0; i < nextVNode.children.length; i++) {

container.appendChild(nextVNode.children[i].el)

}

break}}}Copy the code

As shown in the highlighted code, we pass nextvNode.tag! Prevvnode. tag Checks whether the containers of the old and new Portals are the same. Move the Portal only when the containers are different. What is the principle of transport? We know that when we call appendChild to add an element to the DOM, if the added element already exists on the page, it will be moved under the target container element. We take advantage of this by calling appendChild on the new container element because the new child nodes already exist in the old container after the patchChildren function.

Of course, during the move, we need to check the child node type of the new Portal and apply the appropriate handling. We can test our code using the following example:


/ / the old VNode

const prevVNode = h(

Portal,

{ target: '#old-container' },

h('p'.null.'the old Portal'))/ / new VNode

const nextVNode = h(

Portal,

{ target: '#new-container' },

h('p'.null.'the new Portal')

)


render(prevVNode, document.getElementById('app'))


// Update in 2 seconds

setTimeout(() = > {

render(nextVNode, document.getElementById('app'))},2000)

Copy the code

As shown in the code above, in this example both prevVNode and nextVNode are of type Portal, and the mount targets of the old and new portals are #old-container and #new-container, respectively. Below is the complete code and online experience address.

Updates to stateful components

The next step is to update stateful components. The first question we need to consider is: under what circumstances will stateful component updates be triggered? There are actually two ways to update a stateful component: active and passive.

What is active updating? The so-called active update refers to the update caused by the change of the state of the component itself. For example, if the data data of the component changes, it must be re-rendered. But let’s not forget: Render the contents of a component is very may contain other components, namely child components, for child components, which in addition to their own state, probably also contains the parent component passed in external state (props), so the parent component status change is likely to cause child components the change of the external state, at this time will need to update the child components, Such component updates due to changes in external state are called passive updates.

Take the initiative to update

We know that the core of a component is the rendering function, which produces a VNode. The renderer will render the VNode generated by the rendering function into a real DOM. When the state of the component changes, all we need to do is re-execute the rendering function and produce a new VNode. Finally, the real DOM is updated through the patch algorithm between the old and new VNodes. The key point here is to re-execute the render function after the data changes to get a new VNode. Let’s recall the mountStatefulComponent function used to mount the state component as explained in the previous section:


function mountStatefulComponent(vnode, container, isSVG) {

// Create a component instance

const instance = new vnode.tag()

/ / render VNode

instance.$vnode = instance.render()

/ / a mount

mount(instance.$vnode, container, isSVG)

// Both the el attribute value and the component instance's $EL attribute refer to the component's root DOM element

instance.$el = vnode.el = instance.$vnode.el

}

Copy the code

The core component mounting steps are divided into three steps: 1, create component instance, 2, call render of component to get VNode, 3, mount VNode to container element. In fact, we can encapsulate the code other than the component instance creation step into a function, as follows:


function mountStatefulComponent(vnode, container, isSVG) {

// Create a component instance

const instance = new vnode.tag()


instance._update = function() {

// 1. Render VNode

instance.$vnode = instance.render()

// 2

mount(instance.$vnode, container, isSVG)

// The el attribute value and the component instance's $EL attribute refer to the component's root DOM element

instance.$el = vnode.el = instance.$vnode.el

}


instance._update()

}

Copy the code

As shown in the code above, inside the mountStatefulComponent function, we encapsulate everything except creating the component instance into the instance._update function on the component instance object. Why call _update immediately at the end of the mountStatefulComponent function? In fact, all the _update function does is render the component so that when the state of the component itself changes, the _update function can be called again to update the component.

Suppose we have MyComponent as follows:


class MyComponent {

// Own state or local state

localState = 'one'


/ / mounted hook

mounted() {

After two seconds, change the value of the local state and re-call the _update() function to update the component

setTimeout(() = > {

this.localState = 'two'

this._update()

}, 2000)}render() {

return h('div'.null.this.localState)

}

}

Copy the code

As shown in the previous component, this component has a data called localState that is used in the Render function. / / Set a timer in the component’s Mounted hook function that changes its localState after two seconds. / / Call _update manually to update the component. We’ll explain how to do this in more detail in the section on response systems. The Mounted lifecycle hook is used in the component above, but the mountStatefulComponent function does not have the ability to call any of the component’s lifecycle functions. We need to add the ability to perform mounted callbacks to the mountStatefulComponent function. Simply call mounted after the component has been rendered as a real DOM, as shown in the code below:


function mountStatefulComponent(vnode, container, isSVG) {

// Create a component instance

const instance = new vnode.tag()


instance._update = function() {

// 1. Render VNode

instance.$vnode = instance.render()

// 2

mount(instance.$vnode, container, isSVG)

// The el attribute value and the component instance's $EL attribute refer to the component's root DOM element

instance.$el = vnode.el = instance.$vnode.el

// Call mounted hook

instance.mounted && instance.mounted()

}


instance._update()

}

Copy the code

When we use the mountStatefulComponent function to mount a state component, if the component provides a Mounted method, it will be called as a hook function. The MyComponent mounted hook is now executed correctly. We modify the component state and call _update to update the component. However, when updating, we should not call the mount function as we did when mounting components for the first time. Instead, we should call the patch function to compare the newly produced VNode of components with the old VNode produced when mounting components for the first time and complete the update. How does the _update function know whether the current render is a first mount or a subsequent update? Therefore, we need to design a Boolean status flag for the component instance to indicate whether the component has been mounted, so that the _update function can distinguish whether the current render is mounted for the first time or a subsequent update. Here’s the code for our modified mountStatefulComponent function:


function mountStatefulComponent(vnode, container, isSVG) {

// Create a component instance

const instance = new vnode.tag()


instance._update = function() {

// If instance._mounted is true, the component is mounted and updated

if (instance._mounted) {

// Get the old VNode

const prevVNode = instance.$vnode

// 2. Re-render the new VNode

const nextVNode = (instance.$vnode = instance.render())

// 3. Patch update

patch(prevVNode, nextVNode, prevVNode.el.parentNode)

// update vnode.el and $el

instance.$el = vnode.el = instance.$vnode.el

} else {

// 1. Render VNode

instance.$vnode = instance.render()

// 2

mount(instance.$vnode, container, isSVG)

// 3. Indicates that the component has been mounted

instance._mounted = true

// The el attribute value and the component instance's $EL attribute refer to the component's root DOM element

instance.$el = vnode.el = instance.$vnode.el

// Call mounted hook

instance.mounted && instance.mounted()

}

}


instance._update()

}

Copy the code

As shown in the code above, we pass an if… The else statement checks whether the instance._mounted attribute of the component instance is true or false to determine whether the first or update operation should be performed. The code inside the if block is used to perform the update operation, which is roughly divided into four steps:

  • $VNode = $VNode; $VNode = $VNode; $VNode = $VNode; $VNode = $VNode;

  • 2. Recall the render function to produce a new VNode.

  • 3. Call the patch function to compare the old and new VNodes to complete the update operation.

In addition to the above three steps, we should update the values of the vNode. el property and the component instance’s $el property with new real DOM elements. In addition, notice the third parameter we passed to the patch function in step 3, which is the container element, which can be obtained by obtaining the parent node of the old vnode.el.

Now that active component updates are over, the link below is the complete code and the online experience address.

Preliminarily understand the external state of the component props

We talked about active updates for stateful components. We should have continued with passive updates for stateful components, but before we talk about passive updates, we need to take a moment to lay the foundation. Let’s look at the props of the component. Because passive updates to components are caused by changes in the external state of the component, and props is the external state of the component. This section won’t go into props, though; we’ll cover props in more detail in a later section.

Assume the parent component’s template is as follows:


<! Parent component template -->

<template>

<ChildComponent :text="localState" />

</template>

Copy the code

The parent component’s template renders the ChildComponent child. The ChildComponent child has a text property, which is a binding property whose variable is the parent component’s own state, localState. The compiled rendering function for this template can be expressed as:


render() {

return h(ChildComponent, {

text: this.localState

})

}

Copy the code

The render function is the render function of the parent component, so we can define the parent component as follows:


class ParentComponent {


// Local status

localState = 'one'


render() {

childCompVNode = h(ChildComponent, {

text: this.localState

})

return childCompVNode

}

}

Copy the code

As shown in the code above, the parent component rendering function returns the child component’s VNode, childCompVNode. ChildCompVNode will be mounted by the mountStatefulComponent function in the familiar steps of creating a component instance, calling the Render function of the component instance, and mounting it by calling the mount function. We can actually initialize the props of the component immediately after the component instance is created. Add the following code to the mountStatefulComponent function:


function mountStatefulComponent(vnode, container, isSVG) {

// Create a component instance

const instance = (vnode.children = new vnode.tag())

// Initialize props

instance.$props = vnode.data


/ / to omit...

}

Copy the code

As the code highlighted above shows, after the component instance is created, we add the $props property to the component instance and assign vNode. data to $props. This.$props. Text allows the props data passed in from the parent to be accessed in the child component. Here’s how external data is used in ChildComponent:


// Subcomponent class

class ChildComponent {

render() {

// Call this.$props. Text to access external data

return h('div'.null.this.$props.text)

}

}

Copy the code

This gives the parent the ability to pass props to its children, but in the simplest way, we simply assign VNodeData to $props. We know that VNodeData is not all props. It also contains events and other important information. So in a real implementation, we would extract props from VNodeData. But that’s not the point of this chapter. We’ll keep it simple.

Now that the child component has the ability to get props data passed in from the parent component, we can test our code using the following example:


// Subcomponent class

class ChildComponent {

render() {

// Access external state in child component: this.$props. Text

return h('div'.null.this.$props.text)

}

}

// Parent component class

class ParentComponent {

localState = 'one'


render() {

return h(ChildComponent, {

// Props passed by the parent component to the child component

text: this.localState

})

}

}


// Stateful component VNode

const compVNode = h(ParentComponent)

render(compVNode, document.getElementById('app'))

Copy the code

Passive update

With props in place, we can start talking about passive updates to stateful components. / / add a mounted hook to the parent component and set the localState of the parent component to the localState of the parent component. / / add a mounted hook to the parent component and set the localState of the parent component to the localState of the parent component.


// Subcomponent class

class ChildComponent {

render() {

// Access external state in child component: this.$props. Text

return h('div'.null.this.$props.text)

}

}

// Parent component class

class ParentComponent {

localState = 'one'

mounted() {

// Change localState to 'two' after two seconds

setTimeout(() = > {

this.localState = 'two'

this._update()

}, 2000)}render() {

return h(ChildComponent, {

// Props passed by the parent component to the child component

text: this.localState

})

}

}


// Stateful component VNode

const compVNode = h(ParentComponent)

render(compVNode, document.getElementById('app'))

Copy the code

As highlighted above, we define the mounted hook function for the parent component. Inside the mounted hook function we set a timer, change localState to ‘two’ after two seconds and call the _update method to update the parent component. The ParentComponent, ParentComponent, produces two different vnodes: the VNode generated by the first rendering is:


const prevCompVNode = h(ChildComponent, {

text: 'one'

})

Copy the code

The second VNode produced due to its state change is:

const nextCompVNode = h(ChildComponent, {
text: 'two'
})

Copy the code

So the update operation inside the _update function is equivalent to the patch between prevCompVNode and nextCompVNode, i.e.

patch(prevCompVNode, nextCompVNode, prevCompVNode.el.parentNode)
Copy the code

Since prevCompVNode and nextCompVNode are component vNodes, the patchComponent function is called inside the Patch function to update, as shown in the code highlighted below:


function patch(prevVNode, nextVNode, container) {
const nextFlags = nextVNode.flags
const prevFlags = prevVNode.flags


if(prevFlags ! == nextFlags) { replaceVNode(prevVNode, nextVNode, container) }else if (nextFlags & VNodeFlags.ELEMENT) {

patchElement(prevVNode, nextVNode, container)

} else if (nextFlags & VNodeFlags.COMPONENT) {

patchComponent(prevVNode, nextVNode, container)

} else if (nextFlags & VNodeFlags.TEXT) {

patchText(prevVNode, nextVNode)

} else if (nextFlags & VNodeFlags.FRAGMENT) {

patchFragment(prevVNode, nextVNode, container)

} else if (nextFlags & VNodeFlags.PORTAL) {

patchPortal(prevVNode, nextVNode)

}

}

Copy the code

The patchComponent function receives three arguments: the old VNode, the new VNode and the container element. The patchComponent function is implemented as follows:


function patchComponent(prevVNode, nextVNode, container) {

// Check whether the component is stateful

if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {

// Get the component instance

const instance = (nextVNode.children = prevVNode.children)

// 2, update props

instance.$props = nextVNode.data

// update the component

instance._update()

}

}

Copy the code

As shown in the code above, we determine whether a component is stateful by checking its flags and update it if it is. The update operation is simple in three steps:

  • Prevvnode. children get the component instance

  • Update the $props property of the component instance with the new VNodeData

  • Because $props has been updated, call the _update method to render the component.

One thing to clarify here is that we can read component instances from the children property of a VNode, such as prevvNode. children in the code above, because every VNode of type stateful component, During mount, we will have the children property reference the instance of the component so that the component instance object can be accessed through VNode. We mentioned this in the chapter “Design VNode First”. So we need to modify the mountStatefulComponent function to assign the instance object to the vNode. children property after creating the component instance as follows:


function mountStatefulComponent(vnode, container, isSVG) {

// Create a component instance

const instance = (vnode.children = new vnode.tag())


/ / to omit...

}

Copy the code

In this way we can get the component instance from VNode in the patchComponent function. Here we emphasize again: The children attribute of a VNode is supposed to store children, but for a component-type VNode, its children should all exist as slots, and we chose to store slot content in a separate slots attribute rather than in the children attribute. This allows the children property to be used to store component instances, as we’ll explain when we talk about slots in a later section.

Here is the complete code and the online experience address:

In the example above, when the parent component changes its own state, the ChildComponent rendered by it does not change. It is still ChildComponent. Only the props data passed to the ChildComponent changes. However, sometimes a change in the parent’s own state can cause the parent to render different child components, as shown in the following code:


// Parent component class

class ParentComponent {

isTrue = true

mounted() {

setTimeout(() = > {

this.isTrue = false

this._update()

}, 2000)}render() {

// if this.isTrue isTrue, render ChildComponent1, otherwise render ChildComponent2

return this.isTrue ? h(ChildComponent1) : h(ChildComponent2)

}

}

// Stateful component VNode

const compVNode = h(ParentComponent)

render(compVNode, document.getElementById('app'))

Copy the code

As shown in the code above, observe the render function of ParentComponent. ChildComponent1 is rendered when the ParentComponent’s state isTrue isTrue. Otherwise the child component ChildComponent2 is rendered. Set the timer in mounted hook, change the value of isTrue to false two seconds later, and call _update to update ParentComponent. In this case, the ParentComponent will render different components due to its own state change. When the ParentComponent is first mounted, the VNode produced by the ParentComponent is:


const pervCompVNode = h(ChildComponent1)

Copy the code

The VNode output from the ParentComponent after the update is:


const nextCompVNode = h(ChildComponent2)

Copy the code

Although pervCompVNode and nextCompVNode are both components of type, they are different components. PervCompVNode: ChildComponent1; nextCompVNode: ChildComponent2; This leads to one of our principles for updating components: we assume that different components render different content, so for different components, the solution we adopt is to replace the content rendered by the old component with the content of the new component. Based on this idea, we modify the code of the patchComponent function as follows:


function patchComponent(prevVNode, nextVNode, container) {

// The value of the tag attribute is the component class, which is used to determine whether the new component class is the same as the new one

if(nextVNode.tag ! == prevVNode.tag) { replaceVNode(prevVNode, nextVNode, container) }else if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {

// Get the component instance

const instance = (nextVNode.children = prevVNode.children)

/ / update the props

instance.$props = nextVNode.data

// Update the component

instance._update()

}

}

Copy the code

As shown in the highlighted code of patchComponent function above, a judgment condition has been added. We know that for VNode of component type, its tag attribute value refers to the component class itself. We can determine whether the old and new components are the same by comparing whether the component classes before and after are the same. If not, the replaceVNode function is called to replace the old component with the new one. Remember how to implement the replaceVNode function? As follows:


function replaceVNode(prevVNode, nextVNode, container) {

container.removeChild(prevVNode.el)

mount(nextVNode, container)

}

Copy the code

This is the replaceVNode function we implemented earlier, which removes the rendered content of the old VNode from the container element and mounts the new VNode to the container element. This code also applies to components, but for components we can’t just remove the content rendered by the component and then call the unmounted hook. So we add the following code to the replaceVNode function:


function replaceVNode(prevVNode, nextVNode, container) {

container.removeChild(prevVNode.el)

// If the VNode type to be removed is a component, call the unmounted hook function of the component instance

if (prevVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {

// A VNode of type stateful component whose children property is used to store component instance objects

const instance = prevVNode.children

instance.unmounted && instance.unmounted()

}

mount(nextVNode, container)

}

Copy the code

As the code highlighted above shows, if the prevVNode type to be removed is a stateful component, you need to call the unmounted hook function of the component instance. Here is the address of the code and online experience complete: codesandbox. IO/s/ll92yq0o2… .

We need shouldUpdateComponent

“Placeholder”

Updates to functional components

Next we will discuss functional component updates. In fact, whether stateful component or functional component, the principle of update is the same: the component’s new VNode is compared with the old VNode, so as to complete the update. To keep things from getting too abstract, let’s take a concrete example as shown in the following code:

// Subcomponents - functional components
function MyFunctionalComp(props) {
    return h('div'.null, props.text)
}
// MyFunctionalComp child is rendered in the render function of the parent component
class ParentComponent {
localState = 'one'
mounted() {

setTimeout(() = > {

this.localState = 'two'

this._update()

}, 2000)}render() {

return h(MyFunctionalComp, {

text: this.localState

})

}

}


// Stateful component VNode

const compVNode = h(ParentComponent)

render(compVNode, document.getElementById('app'))

Copy the code

Looking at the code above, we define the ParentComponent, which is a stateful component that renders the MyFunctionalComp child, which is a functional component, in its render function. MyFunctionalComp () {this.$props. XXX () {this.$props. Here’s the mountFunctionalComponent function we implemented earlier:


function mountFunctionalComponent(vnode, container, isSVG) {

/ / get VNode

const $vnode = vnode.tag()

/ / a mount

mount($vnode, container, isSVG)

// The el element references the root element of the component

vnode.el = $vnode.el

}

Copy the code

To implement props pass for functional components, we need to make some modifications to the mountFunctionalComponent function, as shown in the following code:


function mountFunctionalComponent(vnode, container, isSVG) {

/ / get props

const props = vnode.data

/ / get VNode

const $vnode = (vnode.children = vnode.tag(props))

/ / a mount

mount($vnode, container, isSVG)

// The el element references the root element of the component

vnode.el = $vnode.el

}

Copy the code

As shown in the highlighted code, we need to get props before calling the component function to get VNode. Here we also directly use the entire VNodeData as props. The reason for doing this is simple as we explained earlier. After getting the props data, pass props as a parameter when calling the component function vnode.tag(props) so that the component can use the parameter to access the data passed by the parent component. In addition, we assign the VNode produced by the component to the vNode. children property, which requires some caveat. For a stateful component VNode, we use the children property to store component instances. The slots attribute will be used to store slot data in the future. Similarly, in functional components, since functional components have no component instances, for functional component vNodes, we use the children attribute to store the vNodes produced by the component. We will also use the slots attribute to store slot data in the future. This was a design decision, not necessarily, but in keeping with the Vue3 design, we stuck with the Vue3 design.

ParentComponent Mounts hook now that we have implemented the function to receive props, let’s look at the example above. In this example, we provide the ParentComponent with the Mounted hook function. After two seconds, we change the localState of the ParentComponent. And call the _update function to re-render. During the re-render process, what happens inside the _update function is equivalent to:

/ / the old VNode
const prevVNode = h(MyFunctionalComp, {
text: 'one'
})

/ / new VNode
const nextVNode = h(MyFunctionalComp, {
text: 'two'
})
/ / update
patch(prevVNode, nextVNode, prevVNode.el.parentNode)
Copy the code

Since both prevVNode and nextVNode are components, the patchComponent function will be called inside the patch function to update. Let’s review the code of the patchComponent function:


function patchComponent(prevVNode, nextVNode, container) {

if(nextVNode.tag ! == prevVNode.tag) { replaceVNode(prevVNode, nextVNode, container) }else if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {

// Get the component instance

const instance = (nextVNode.children = prevVNode.children)

/ / update the props

instance.$props = nextVNode.data

// Update the component

instance._update()

}

}

Copy the code

In this code, the code inside the if block handles updates between two different components, else… The code inside the IF block is used to handle updates to stateful components, so the patchComponent function cannot complete updates to functional components. To achieve this, we need to add a piece of code to the patchComponent function to handle the VNode update of the functional component type, as shown below:


function patchComponent(prevVNode, nextVNode, container) {

if(nextVNode.tag ! == prevVNode.tag) { replaceVNode(prevVNode, nextVNode, container) }else if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {

/ / to omit...

} else {

// Write the update logic for functional components here}}Copy the code

As highlighted above, we simply add the else statement block to it, where we will write the update logic for the functional component. But the question is, how should it be updated? As we said at the beginning of this section, both stateful and functional components update the same principle, so we can follow the implementation of stateful components.

The core steps for mounting functional components are two: 1. Call the component’s definition function to get the VNode produced by the component, and 2. Mount the VNode to the container element. Similar to mounting a state component, we can encapsulate these steps into a function that can be called again when the component is updated. However, unlike stateful components, functional components have no component instances, so there is no way to encapsulate functions like instance._update. What should we do? Simply define the update function on the VNode of the functional component as follows:


function mountFunctionalComponent(vnode, container, isSVG) {

// Add the Handle property, which is an object, to the vNode of the functional component type

vnode.handle = {

prev: null.next: vnode,

container,

update: () = > {

// Initialize props

const props = vnode.data

/ / get VNode

const $vnode = (vnode.children = vnode.tag(props))

/ / a mount

mount($vnode, container, isSVG)

// The el element references the root element of the component

vnode.el = $vnode.el

}

}


// Call vnode.handle.update immediately to complete the initial mount

vnode.handle.update()

}

Copy the code

Here is our modified mountFunctionalComponent function. You can see that we have added the Handle property to the functional component type VNode, which is an object with four properties:


vnode.handle = {

prev: null.next: vnode,

container,

update() {/ *... * /}}Copy the code

We moved the code we used to mount functional components into vnode.handle.update, so we immediately called vnode.handle.update at the end of the mountFunctionalComponent function. This keeps the original functionality intact. The Handle has three other properties in addition to the update method:

  • Handle. prev: Stores the old functional component VNode. When first mounted, there are no old VNodes to speak of, so the initial value is null.

  • Handle. next: Stores the new functional component VNode, which, when first mounted, is assigned the value of the currently mounted functional component VNode.

  • Handle. container: Stores a mount container

Now that the handle.update function is available, we can try to update the functional component by calling the handle.update function inside the patchComponent function, as shown in the following code:


function patchComponent(prevVNode, nextVNode, container) {

if(nextVNode.tag ! == prevVNode.tag) { replaceVNode(prevVNode, nextVNode, container) }else if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {

/ / to omit...

} else {

// Update functional components

// Get the handle with prevvNode. handle

const handle = (nextVNode.handle = prevVNode.handle)

// Update the Handle

handle.prev = prevVNode

handle.next = nextVNode

handle.container = container


// Call the update function to complete the update

handle.update()

}

}

Copy the code

As the code highlighted above shows, we first fetch the Handle using the old VNode(prevVNode), and then we update the value of each property under the Handle:

  • 1. Assign the old functional component VNode(prevVNode) to handle.prev.

  • 2. Assign the new functional component VNode(nextVNode) to handle.next.

  • 3. Update the Container (even if it may not change).

Finally, we call handle.update to complete the update operation. To take a closer look at what happens in this process, the value of handle after the functional component is first mounted is:


handle = {

prev: null.next: prevVNode,

container,

update() {/ *... * /}}Copy the code

After the patchComponent function is used to update the Handle, the value of the handle will change to:


handle = {

prev: prevVNode,

next: nextVNode,

container,

update() {/ *... * /}}Copy the code

As you can see, the handle.prev property is not empty. The prev and next properties store vNodes of the old and new functional component types, respectively. This updated action is critical. After the update is completed, the handle.update function is immediately called for re-rendering. The following is the handle.update function we have implemented so far:


function mountFunctionalComponent(vnode, container, isSVG) {

// Add the Handle property, which is an object, to the vNode of the functional component type

vnode.handle = {

prev: null.next: vnode,

container,

update: () = > {

// Initialize props

const props = vnode.data

/ / get VNode

const $vnode = (vnode.children = vnode.tag(props))

/ / a mount

mount($vnode, container, isSVG)

// The el element references the root element of the component

vnode.el = $vnode.el

}

}


// Call vnode.handle.update immediately to complete the initial mount

vnode.handle.update()

}

Copy the code

As the code highlighted above shows, the current update function can only do the initial mount. When the update function is called again, we cannot execute the mounted code again, as is the case with the stateful component’s instance.update implementation. We need to add the update logic to the handle.update function as follows:


function mountFunctionalComponent(vnode, container, isSVG) {

vnode.handle = {

prev: null.next: vnode,

container,

update: () = > {

if (vnode.handle.prev) {

// The updated logic is written here

} else {

/ / get props

const props = vnode.data

/ / get VNode

const $vnode = (vnode.children = vnode.tag(props))

/ / a mount

mount($vnode, container, isSVG)

// The el element references the root element of the component

vnode.el = $vnode.el

}

}

}


// Call vnode.handle.update immediately to complete the initial mount

vnode.handle.update()

}

Copy the code

In the above code, we can determine whether the functional component is mounted for the first time or updated later by judging whether vnode.handle.prev exists. Since we have assigned the vnode.handle.prev attribute to the old component vnode in the patchComponent function, So the presence of vnode.handle.prev indicates that the functional component is not first mounted, but updated, so we write the update logic in the if block, and the code for the first mount is put in the else block.

So what’s the new thinking? As mentioned above, we just need to find a way to get the old and new vNodes produced by the component separately, so that we can update them with patch function. The following code looks like this:


function mountFunctionalComponent(vnode, container, isSVG) {

vnode.handle = {

prev: null.next: vnode,

container,

update: () = > {

if (vnode.handle.prev) {

/ / update

PrevVNode is the old component VNode, nextVNode is the new component VNode

const prevVNode = vnode.handle.prev

const nextVNode = vnode.handle.next

// prevTree is an old VNode produced by the component

const prevTree = prevVNode.children

// Update props data

const props = nextVNode.data

// nextTree is a new VNode produced by the component

const nextTree = (nextVNode.children = nextVNode.tag(props))

// Call patch to update

patch(prevTree, nextTree, vnode.handle.container)

} else {

/ / to omit...}}}// Call vnode.handle.update immediately to complete the initial mount

vnode.handle.update()

}

Copy the code

As the code highlighted above shows, since we have updated the Handle in the patchComponent function, So we can get the old component vnode and the new component vnode via vnode.handle.prev and vnode.handle.next respectively. PrevVNode and nextVNode are vNodes that describe functional components, not vNodes produced by functional components. Because the VNode output stored in the functional components used to describe the functional components of VNode children attribute, so in the above code we through prevVNode. Children get the component output the old VNode prevTree, Tag (props) to get the new VNode (nextTree). With prevTree and nextTree, we can call patch to perform the update operation.

This is the update process for functional components.