Parse Property Resolution of Compile-Source Edition

Posted by Cynix on Fri, 09 Aug 2019 08:02:31 +0200

It's not easy to write articles. Give a compliment to your brother.


Focus on Vue source code sharing, the article is divided into vernacular version and source version, vernacular version helps to understand the working principle, source version helps to understand the internal details, let's learn together.
Research based on Vue version [2.5.17]

If you think the typesetting is ugly, please click on the link below or pull down to pay attention to the public number below.

[Vue Principle] Attribute Resolution of Compile-Source Edition

Haha, today we have finally reached the part of attribute resolution. We have talked about parse process, tag resolution, and finally only attribute resolution ()

If you are not interested in compile, don't look at it first. After all, it won't play any role immediately.

If you haven't read the previous two articles, it's highly recommended that you take a look at them.~

Main Process of Parse in Compile
Label Resolution of Compile

If you look at it, you should know what part of Attribute Resolution is, and yes, in the parse-start section dealing with header tags.

So let's go to the parse - start function!

As you can see from the source code below, functions with process are used to process attributes.

function parse(template){
    
    parseHTML(template,{        

        start:(...Pull out the lower surface)

    })
}



function start(tag, attrs, unary) {    



    // Create AST nodes

    var element = createASTElement(tag, attrs, currentParent);    



    // Nodes need to be parsed and not yet processed

    if (!element.processed) {

        processFor(element);
        processIf(element);
        processSlot(element);        



        for (var i = 0; i < transforms.length; i++) {

            element = transforms[i](element, options) || element;
        }

        processAttrs(element);
    }
    
    .... Omit some unimportant code  



    // The parent node is the last node, which is placed directly in the children array of the previous node.

    if (currentParent) {      



        // Explain that the preceding node has v-if

        if (element.elseif || element.else) {
            processIfConditions(element, currentParent);

        } else {
            currentParent.children.push(element);
            element.parent = currentParent;
        }
    }
}

Look at it. There are probably several functions that handle attributes above.

It's not hard, just a little more content.

1. ProceFor, parse v-for

2. ProceIf, parse v-if

3. ProceSlot, parse slot

4. ProceAttrs, parsing other attributes

5. transforms, parsing style attributes

And only when element.processed is false will it be parsed

Because element.processed means that the attribute has been parsed, the value of element.processed is undefined at first.

Here's how to do it one by one.

First, what is element?

As mentioned in the parse process, element generates ast by parsing tag information

The following four functions will be analyzed one by one, and the corresponding element examples will be attached for reference.

In fact, there are many other processing functions, in order to maintain the length of the article, so I removed them.

Before you start, you need to know the function getAndRemoveAttr, which is used in many places below.

The purpose is to find an attribute from el.attrList and return the value of the attribute.

function getAndRemoveAttr(el, name, removeFromMap) {  



    var val =el.attrsMap[name];  



    if (removeFromMap) {        

        delete el.attrsMap[name];

    }    



    return val

}

tramsforms in parse-start

At the beginning of the parse-start function, we see a transfroms thing.

transforms is an array that stores two functions, one dealing with static and dynamic class es, and the other dealing with static and dynamic style s.

Both of them are very simple. Let's take a brief look at the results.

Handling class

function transformNode(el, options) {    



    var staticClass = getAndRemoveAttr(el, 'class');    



    if (staticClass) {

        el.staticClass = JSON.stringify(staticClass);
    }    



    // class="b" returns B directly
    var classBinding = getBindingAttr(el, 'class', false);    



    if (classBinding) {

        el.classBinding = classBinding;
    }
}

{    

    classBinding: "b"

    staticClass: ""a""
    tag: "span"
    type: 1

}

Handling style

function transformNode$1(el, options) {    



    var staticStyle = getAndRemoveAttr(el, 'style');    



    if (staticStyle) {        

        // For example, bind style="height:0;width:0"

        // parseStyleText parses the object {height:0,width:0}
        el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
    }  



    // style="{height:a}" resolves to {height:a}
    var styleBinding = getBindingAttr(el, 'style', false);    

    if (styleBinding) {

        el.styleBinding = styleBinding;
    }
}

{    

    staticStyle: "{"width":"0"}"

    styleBinding: "{height:a}"
    tag: "span"
    type: 1

}

Analytical v-for

In the parse - start function, you see processFor, yes, parsing the v-for instruction!

function processFor(el) {    



    var exp = getAndRemoveAttr(el, 'v-for')    



    if (exp) {        

        // For example, the instruction is v-for="(item,index) in arr"

        // res = {for: "arr", alias: "item", iterator1: "index"}
        var res = parseFor(exp);        

        if (res) {            

            // Merge the res and el attributes

            extend(el, res);
        } 
    }
}

There's no difficulty, just look at the template and the final result.

<div v-for="(item,index) in arr"></div>
{    

    alias: "item",    

    for: "arr",    

    iterator1: "index",    

    tag: "div",    

    type: 1,

}

Analytical v-if

In the parse - start function, you see processFor, yes, parsing the v-if instruction!

function processIf(el) {    



    var exp = getAndRemoveAttr(el, 'v-if');    



    if (exp) {


        el.if = exp;
        
        (el.ifConditions || el.ifConditions=[])
        .push({            

            exp: exp,            

            block: el

        })
    } else {        



        if (getAndRemoveAttr(el, 'v-else') != null) {

            el.else = true;
        }        



        var elseif = getAndRemoveAttr(el, 'v-else-if');        



        if (elseif) {

            el.elseif = elseif;
        }
    }
}

In dealing with v-if, we need to save the expression and node of v-if.

And v-else, you just need to set el.else to true, v-else-if also needs to save the expression.

Here, v-else and v-else-if do not do much processing, but do it in the first parse-start.

if (element.elseif || element.else) {
    processIfConditions(element, currentParent);
} 

After processIf, the attribute exists elseif or else

A method is called, as follows

function processIfConditions(el, parent) {    



    var prev = findPrevElement(parent.children);    



    if (prev && prev.if) {    

    
        (prev.ifConditions ||prev.ifConditions=[])
        .push({            
            exp: el.elseif,            

            block: el

        })
    }
} 

This method mainly attaches nodes with v-else-if and v-else to nodes with v-if.

Let's look at the results of affiliation first.

<div>
    <p></p>
    <div v-if="a"></div>
    <strong v-else-if="b"></strong>
    <span v-else></span>

</div>
{    

    tag: "header",    

    type: 1,    

    children:[{        

        tag: "header",        

        type: 1,        

        if: "a",        

        ifCondition:[

            {exp: "a", block: {header Of ast node}}
            {exp: "b", block: {strong Of ast node}}
            {exp: undefined, block: {span Of ast node}}
        ]
    },{        

        tag: "p"

        type: 1
    }]
} 

As we can see, the original two child nodes, strong and span, are not in the child ren of div.

Instead, it ran into header's ifCondition s.

Now look at processIfConditions, which only handles nodes with v-else-if and v-else

And we need to find the node of v-if, how to find it? You can see a way.

function findPrevElement(children) {    



    var i = children.length;    



    while (i--) {        

        if (children[i].type === 1) {            

            return children[i]

        } else {
            children.pop();
        }
    }
} 

Starting from the end of the sibling child node, when type = 1, the node is a node with v-if

So the v-else two can be directly attached to it.

You ask, why don't you return the span node from the end, and why does type = 1 have v-if?

First of all, you can't analyze it from the perspective of normal parsing, but from the perspective of tag-by-tag parsing.

For example, we have now parsed the v-if node and added the children of the parent node.

Then parse the next node, such as the node with v-else-if, and then go to parent.children to find the last node (that is, the v-if node just added)

Surely the node returned is v-if, naturally it can be correctly attached.

v-else equivalence

If you say V-IF and v-else-if are separated by another node, then v-else-if can't be attached to v-if.

Then you must be an obstinate person, v-else-if must follow v-if, otherwise you will make mistakes and the mistakes will not be discussed.

Analytical slot

In the parse - start function, you see processSlot, which, yes, parses slot correlation.

function processSlot(el) {    



    if (el.tag === 'slot') {

        el.slotName = el.attrsMap.name



    } else {        

        

        var slotScope = getAndRemoveAttr(el, 'slot-scope')

        el.slotScope = slotScope;     

   

        // Name of slot
        var slotTarget = el.attrsMap.slot        



        if (slotTarget) {

            el.slotTarget = 
                slotTarget === '""' 

                ? '"default"'

                : slotTarget;

        }
    }
}

There seems to be nothing to say about this. Just simply record the results of the analysis.

Subcomponent Template

<span>
    <slot name=" header"
        :a="num" :b="num">
    </slot>
</span>

Resolve into

{  
    tag: "span"
    type: 1
    children:[{  
        attrsMap: {name: " header", :a: "num", :b: "num"}
        slotName: "" header""
        tag: "slot"
        type: 1
    }]
}

Parent component template

<div>
    <child >
        <p slot="header" slot-scope="c"> {{ c }}</p>
    </child>

</div>

Resolve into

{    

    children: [{        

        tag: "child",        

        type: 1,        

        children: [{            

            slotScope: "c",            

            slotTarget: ""header "",            

            tag: "p",            

            type: 1

        }]
    }],    

    tag: "div",    

    type: 1

}

The following is a lot, but it's not difficult.

Resolve other attributes

This piece of content is a lot, but in general there is no difficulty, that is to see a little bored, and then put the source code to the end, intend to write parsing first.

Here we focus on the remaining types of attributes, roughly divided into two cases

1Vue Attributes

For example, attribute names with three symbols "v-", ":", "@", each of which will be treated separately.

Before these three attributes are processed, modifiers with attribute names are extracted.

For example, instructions with modifiers

v-bind.a.b.c = "xxxx"

After processing, the modifiers object is extracted, as follows

{a: true, b: true, c: true}

For instructions

Then you start working on three types of attributes

1 " : "

We all know that ":" is equal to "v-bind", and when matched to this attribute name, it will go into the processing here.

After a general look, you can see that this part of the processing.

Attributes are stored in el.props or el.attrs

So here comes the question...?

How do you determine whether an attribute is put in el.props or el.attrs?

There are two conditions.

1,modifiers.prop

When you add. prop to an instruction, for example

<div :sex.prop="myName"></div>

Then the property of sex will be stored in el.props.

2. Forms

You see this code

!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)

First, it can't be a component.

Second, form elements are important attributes of forms

Let's take a look at the platform MustUseProp. It's easy.

When the element is input,textarea,option,select,progress

Attributes are selected, checked, value, etc.

They're all stored in el.props.

function a(tag, type, attr) {    



    return (

        (attr === 'value' && 'input,textarea,option,select,progress'.indexOf(tag)>-1) 
        && type !== 'button' 
        || (attr === 'selected' && tag === 'option') 
        || (attr === 'checked' && tag === 'input') 
        || (attr === 'muted' && tag === 'video')
    )
};

Maybe you will ask

What's the difference between el.props and el.attrs?

props are added directly to the dom attribute, not on the label


attrs are used to display on tag attributes


There is another problem.

Why translate the attribute of el.props into hump naming?

As you can see, all attribute names will pass through a camelize method. Why?

Because the attributes of DOM are named by hump, there is no name for cross bar.

So we need to change the naming of a-b to aB and cut off a picture at random.

However, innerHTML is special, humps are not good, so special processing is done, as you can see.

The hump method should be useful. Put it on.

var camelize = function(str) {    



    return str.replace(/-(\w)/g, function(_, c) { 

        return c ? c.toUpperCase() : ''; 
    })
})

modifiers.sync

After that, you should have found a treasure, which is sync.

I believe you should have used it. For parent-child communication, the child component wants to modify the prop passed in by the parent component.

Update props by indirectly modifying parent component data through events

To avoid being forgotten, here's an example of how to use it.

The parent component passes in the name to the child component, which can be modified in two directions by adding sync

<div>    

    <child-test :name.sync="xxx"></child-test>

</div>



If the child component wants to modify the name passed in by the parent component, it can trigger the event directly and pass in the parameter.

this.$emit("update:name", 222)

So now let's look at how he implemented attribute resolution.

addHandler(el, 
    "update:" + camelize(name),
    genAssignmentCode(value, "$event")
);

Look at what this code does.

First

camelize(name)

Turn names into hump letters, such as get-name, into getName

Then the following code executes

genAssignmentCode(value, "$event")

Parsing returns "value = $event"

Then addHandler saves the event name and event callback to el.events, as follows

The saved events will be parsed later, and the value will be wrapped in a layer of function.

Equivalent to listening for events to subcomponents

@update:name ="function($event){ xxx = $event }"

$event is the value passed in when the subcomponent triggers the event

xxx is the data of the parent component, after assignment, it is equivalent to the child component modifying the data of the parent component.

If you want to understand the internal principles of event, you can see Event - source version of the binding component custom events

2 " @ "

When matched to @ or v-on, it's an add event, and there's not much processing here.

AddiHandler saves all events to el.events.

3 " v- "

The rest of the attributes with v - will be dealt with here.

Matching parameters, the comments in the source code are also clear, not explained here.

Then they are all saved in el.directives

2 General Attributes

Nothing to say, ordinary attributes, stored directly in el.attrs

The following is the source code for processing other attributes, you don't look very long, in fact, it's very simple!

var onRE = /^@|^v-on:/;

var dirRE = /^v-|^@|^:/;

var bindRE = /^:|^v-bind:/;

var modifierRE = /\.[^.]+/g;

var argRE = /:(.*)$/;



function processAttrs(el) {    



    var list = el.attrsList;    

    var i, l, name, rawName, value, modifiers, isProp;   



    for (i = 0, l = list.length; i < l; i++) {


        name = rawName = list[i].name;
        value = list[i].value;        



        // Determine whether attributes have'v-','@',':'

        if (dirRE.test(name)) {            



            // mark element as dynamic

            el.hasBindings = true;       

     

            // For example, v-bind.a.b.c= "xxxzxxx"

            // Then modifiers = {a: true, b: true, c: true}
            modifiers = parseModifiers(name);            



            // Extract pure names

            if (modifiers) {    

            

                // name = "v-bind.a.b.c = "xxzxxxx" "

                // So name= v-bind
                name = name.replace(modifierRE, '');
            }        

    

            // Collecting dynamic attributes, v-bind, may be bound attributes, or props passed into subcomponents

            // bindRE = /^:|^v-bind:/
            if (bindRE.test(name)) {   

            

                // Extract pure names, such as name= v-bind

                // After replacement, name = bind
                name = name.replace(bindRE, '');
                isProp = false;      

          

                if (modifiers) {   

                 

                    // Add directly to the properties of dom
                    if (modifiers.prop) {
                        isProp = true;    

                    

                        // Become the name of the hump

                        name = camelize(name);                        

                        if (name === 'innerHtml')   

                            name = 'innerHTML'; 

                    }      

              

                    // Synchronized modification of subcomponents
                    if (modifiers.sync) {
                        addHandler(el,      

                            // Get the name of the hump                      

                            "update:" + camelize(name), 

                            // Get "value= $event"

                            genAssignmentCode(value, "$event")
                        );
                    }
                }  

              

                // The function of el.props is that some of the necessary attributes of the form are stored in el.props.
                if (

                     isProp ||

                     // Platform MustUseProp determines if this property is to be placed in el.props

                     // For example, form element input, attribute value selected, checked, etc.

                     // For example, tag=input, name=value, then value attribute to house ah el.props

                     (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))

                ) {


                    (el.props || (el.props = [])).push({ 
                        name, 
                        value
                    });
                } 

                // Other attributes are placed in el.attrs

                else {


                    (el.attrs || (el.attrs = [])).push({ 
                        name, 
                        value
                    });
                }
            }            



            // Collect events, v-on, onRE = /^@ | ^ v-on:./

            else if (onRE.test(name)) {    

            

                // Get rid of v-on or @ and get the real command name
                // For example, name ="@click" and name = "click" after replacement
                name = name.replace(onRE, '');
                addHandler(el, name, value, modifiers, false);
            }            



            // Collect other instructions, such as "v-once".

            else { 

                // Get rid of v-and get the real command name.
                name = name.replace(dirRE, '');                



                // name = "bind:key" , argMatch = [":a", "a"]

                var argMatch = name.match(argRE);                

                var arg = argMatch && argMatch[1];    

            

                if (arg) {                    

                    // For example, name = bind: key, remove:key

                    // Then name = bind
                    name = name.slice(0, -(arg.length + 1));
                }

                (el.directives || (el.directives = [])).push({ 
                    name, 
                    rawName, 
                    value, 
                    arg, 
                    modifiers
                });
            }

        } else {

            (el.attrs || (el.attrs = [])).push({ 
                name, 
                value
            });
        }
    }
}

Last

In view of my limited ability, there will inevitably be omissions and errors. Please forgive me. If there are any inappropriate descriptions, please contact me in the background. Thank you very much.

Topics: Javascript Attribute Vue JSON REST