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.