Front-end Compilation principles
Refer to the super – a tiny – the compiler.
Parse => transform => generate. Parse converts code or template strings into an AST tree, transform processes the AST, and generate generates code
Vue2.6 compilation
Take a look at the source code for vue2.6 to compile the core code
var createCompiler = createCompilerCreator(function baseCompile (template, options) {
var ast = parse(template.trim(), options);
if(options.optimize ! = =false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
Copy the code
Parse: Template optimize into code the AST optimize is a static node that generates the render function, which generates the VNode tree
File directory
To better clarify the code structure, let’s take a look at how VUE gets here. First of all,
// scripts/config.js
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd'.env: 'development'.alias: { he: './entity-decoder' },
banner
},
Copy the code
You can see that the entry is a web/entry-runtime-with-compiler.js file.
// src/platforms/wev/entry-runtime-with-compiler.js
import Vue from './runtime/index'
// Put Vue. Prototype.$mount on the prototype
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
el = el && query(el)
const options = this.$options
if(! options.render) {// Get the template string
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) = = =The '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this}}else if (el) {
template = getOuterHTML(el)
}
// Get the render function
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV ! = ='production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
Copy the code
The mount function above actually ends up executing mountComponent
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
Copy the code
The above function fetch Render comes from compileToFunctions
// src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
// createCompiler is the core function originally mentioned above
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
Copy the code
Parse
Parse generates an AST tree from template
var ast = parse(template.trim(), options);
Copy the code
Get the template string
From the above analysis, you can see that template comes from this
// If template is passed when creating Vue instance,
let template = options.template
if (template) {
// If template is #templateId, return innerHTML of templateId
if (typeof template === 'string') {
if (template.charAt(0) = = =The '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`.this)}}}else if (template.nodeType) {
// If template is dom, return its innerHTML directly
template = template.innerHTML
} else {
if(process.env.NODE_ENV ! = ='production') {
warn('invalid template option:' + template, this)}return this}}else if (el) {
// If el is not passed in, the outerHTML of EL is fetched
template = getOuterHTML(el)
}
Copy the code
parse
Look at the parse method and see that some variables are defined
- Stack ==> This stack stores all the nodes with open tabs, which we will later call the outterStack
- CurrentParent ==> This refers to the nearest parent element of the currently open tag
- Root ==> Finally returned root node
/** * Convert HTML string to AST. */
function parse (template, options) {
var stack = [];
var root;
var currentParent;
function closeElement (element) {}function trimEndingWhitespace (el) {
}
parseHTML(template, {
warn: warn$2.expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
start: function start (tag, attrs, unary, start$1, end) {
// ...
},
end: function end (tag, start, end$1) {
// ...
},
chars: function chars (text, start, end) {
// ...
},
comment: function comment (text, start, end) {
// ...}});return root
}
Copy the code
From the following code, you can see that the parse function does two main things.
- Defines its own store variables and functions,
- Perform the parseHTML
If you look at the input parameter to parseHTML, in addition to the options passed in, there are four functions that act as hooks that change the outterStack and currentParent values under certain circumstances.
parseHTML
ParseHTML can be understood as a walk function that iterates through all the Template strings
- Different tags are identified
- Handle corresponding tag
- Advance (n) Advances n characters
- Change outer’s variable by calling the corresponding hook function
The following code will be briefly looked at, followed by a step-by-step look at examples
function parseHTML (html, options) {
var stack = [];
var expectHTML = options.expectHTML;
var isUnaryTag$$1 = options.isUnaryTag || no;
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
var index = 0;
var last, lastTag;
while (html) {
last = html;
if(! lastTag || ! isPlainTextElement(lastTag)) {var textEnd = html.indexOf('<');
if (textEnd === 0) {
/ /... Other tags
// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
// Start tag:
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue}}var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {
rest = html.slice(textEnd);
while(! endTag.test(rest) && ! startTagOpen.test(rest) && ! comment.test(rest) && ! conditionalComment.test(rest) ) { next = rest.indexOf('<'.1);
if (next < 0) { break }
textEnd += next;
rest = html.slice(textEnd);
}
text = html.substring(0, textEnd);
}
if (textEnd < 0) {
text = html;
}
if (text) {
advance(text.length);
}
if(options.chars && text) { options.chars(text, index - text.length, index); }}else {
var endTagLength = 0;
var stackedTag = lastTag.toLowerCase();
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?) (< / ' + stackedTag + '[^ >] * >)'.'i'));
var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length;
if(! isPlainTextElement(stackedTag) && stackedTag ! = ='noscript') {
text = text
.replace(/
/g.'$1') / / # 7298
.replace(/
/g.'$1');
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1);
}
if (options.chars) {
options.chars(text);
}
return ' '
});
index += html.length - rest$1.length;
html = rest$1;
parseEndTag(stackedTag, index - endTagLength, index);
}
if (html === last) {
options.chars && options.chars(html);
break
}
}
parseEndTag();
}
Copy the code
Watch parseHTML:
- First you’ll see some variables defined in function as well
Stack: Holds an element that is not closed. Since the stack has the same name as the one above, we call the stack innerStack to distinguish it from the other. Index: the index pointer that the current string rotates to
- Loop HTML
The example analysis
Take a look at how to generate ast for multi-layer DOM. You can combine the flow chart and source code debug to see.
Assuming that the template is
<div id="demo"><div id="child">childValue</div></div>
new Vue({
el: '#demo',
})
Copy the code
Because the first string is <, the start tag is included
// Start tag:
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
Copy the code
Here, two methods are executed, parseStartTag:
var startTagOpen = new RegExp(("^" " + qnameCapture));
function parseStartTag () {
var start = html.match(startTagOpen);
// start = ["<div","div"]
if (start) {
var match = {
tagName: start[1].attrs: [].start: index
};
advance(start[0].length);
var end, attr;
// The actual code is written like this, I have done the decomposition, convenient to see
// while (! (end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
// verify that it is not followed by > or />
end = html.match(startTagClose);
// Verify that there are no attributes
attr = html.match(dynamicArgAttribute);
var isAttrMatch = (attr || html.match(attribute));
// If it is not followed by > or /> and has attributes,
// Loop to get attributes and push them into attrs of match
while(! end && isAttrMatch) { attr.start = index; advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
// match ====>
// "tagName": "div",
// "attrs": [
/ / /
// " id=\"demo\"",
// "id",
/ / "=",
// "demo",
// null,
// null
/ /]
/ /,
// "start": 0
// }
// If it is > or />
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
return match
}
// match ====>
/ / {
// "tagName": "div",
// "attrs": [
/ / /
// " id=\"demo\"",
// "id",
/ / "=",
// "demo",
// null,
// null
/ /]
/ /,
// "start": 0,
// "unarySlash": "",
// "end": 15
// }}}Copy the code
In the code, you can see that parseStartTag is actually
- will
<div id="demo">
Convert to object, this object contains
{
tagName,
attrs // The property array needs to be parsed
start // Start index
end // End index
}
Copy the code
- And parseStartTag points the index pointer to the end
I get startTagMatch, so what do I do
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
Copy the code
HandleStartTag:
- Processing attrs
- The current node is pushed into the innerStack
- Execute the hook function start
function handleStartTag (match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
var l = match.attrs.length;
var attrs = new Array(l);
for (var i = 0; i < l; i++) {
/ / processing attrs
}
// attrs ===>
/ / /
/ / {
// "name": "id",
// "value": "demo",
// "start": 5,
// "end": 14
/ /}
// ]
if(! unary) { stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end });
// Push to innerStack ==>
/ / /
/ / {
// "tag": "div",
// "lowerCasedTag": "div",
// "attrs": [
/ / {
// "name": "id",
// "value": "demo",
// "start": 5,
// "end": 14
/ /}
/ /,
// "start": 0,
// "end": 15
// }
// ]
lastTag = tagName;
}
// execute the start hook
if(options.start) { options.start(tagName, attrs, unary, match.start, match.end); }}Copy the code
And what does the start function do
- Create an element with createASTElement
- Element processing (instruction parsing, etc.)
- Current elment assigned to cuurentParent
- Elment pushes to the outerStack
function start (tag, attrs, unary, start$1, end) {
var element = createASTElement(tag, attrs, currentParent);
// createASTElement will be based on
AttrsList generates a map and assigns it to attrsMap
// 2. Attach currentParent to the parent of the current element based on the currentParent passed in
// 3. Type type 1 to indicate the label node
// 4. Generate the format of the final required child node
// element ==>
/ / {
// "type": 1,
// "tag": "div",
// "attrsList": [
/ / {
// "name": "id",
// "value": "demo",
// "start": 5,
// "end": 14
/ /}
/ /,
// "attrsMap": {
// "id": "demo"
/ /},
// "rawAttrsMap": {},
// "children": []
// }
{
if (options.outputSourceRange) {
element.start = start$1;
element.end = end;
element.rawAttrsMap = element.attrsList.reduce(function (cumulated, attr) {
cumulated[attr.name] = attr;
returncumulated }, {}); }}// 这里追加start, end, rawAttrsMap
/ / {
// "id": {
// "name": "id",
// "value": "demo",
// "start": 5,
// "end": 14
/ /}
// }
// apply pre-transforms
// preTransforms does some parsing and binding of VUE directives
for (var i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element;
}
// structural directives
// TODO:Generate an execution method for for if once
processFor(element);
processIf(element);
processOnce(element);
// If there is no root, it is now the first node. Assign elment to root
if(! root) { root = element; }// Assign the current elment to currentParent;
currentParent = element;
// Push current elment onto the stack
stack.push(element);
}
Copy the code
So, so far,
[{"tag": "div"."lowerCasedTag": "div"."attrs": [{"name": "id"."value": "demo"."start": 5."end": 14}]."start": 0."end": 15
},
{
"tag": "div"."lowerCasedTag": "div"."attrs": [{"name": "id"."value": "child"."start": 20."end": 30}]."start": 15."end": 31}]Copy the code
See outerStack again
[{"type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
"id": "demo"
},
"rawAttrsMap": {... },"children": []."start": 0."end": 15
},
{
"type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
"id": "child"
},
"rawAttrsMap": {... },"parent": {
"type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
"id": "demo"
},
"rawAttrsMap": {... },"children": []."start": 0."end": 15
},
"children": []."start": 15."end": 31}]Copy the code
CurrentParent points to the child elment.
Then you parse the childValue parseHTML
- Index pointer to
childValue
At the end- Execute the chars hook function
var textEnd = html.indexOf('<');
if (textEnd >= 0) {
// ...
text = html.substring(0, textEnd);
}
// text == 'childValue'
if (text) {
advance(text.length);
}
// Execute the hook function chars
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
Copy the code
Implementation of chars:
- Generate a text node
- Push textNode into the current CurrentParent.Children
function chars (text, start, end) {
if(! currentParent) {return
}
var children = currentParent.children;
// ...
if (text) {
var res;
var child;
if(! inVPre && text ! = =' ' && (res = parseText(text, delimiters))) {
child = {
type: 2.expression: res.expression,
tokens: res.tokens,
text: text
};
} else if(text ! = =' '| |! children.length || children[children.length -1].text ! = =' ') {
// type indicates a text node
child = {
type: 3.text: text
};
}
if(child) { child.start = start; child.end = end; children.push(child); }}}Copy the code
After completing into execution currentParent. Children. Push (textNode), currentParent this time is:
{
"type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
"id": "child"
},
"rawAttrsMap": {... },"parent": {
"type": 1."tag": "div"."attrsList": [...]. ."attrsMap": {
"id": "demo"
},
"rawAttrsMap": {... },"children": []."start": 0."end": 15
},
// Text is pushed in children
"children": [{"type": 3."text": "childValue"."start": 31."end": 41}]."start": 15."end": 31
}
Copy the code
- Index pointer to
</div>
At the end- Perform parseEndTag
// End tag:
var endTagMatch = html.match(endTag);
// endTagMatch ==>
/ / /
// "",
// "div"
// ]
if (endTagMatch) {
var curIndex = index;
// The index pointer moves to the end
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
Copy the code
parseEndTag
- Find the innerStack element closest to the top of the stack with the same name as the current tag
- I’m going to put this element at the top of the stack and I’m going to call the end hook function for all the elements
- Push these elements out of the innerStack
function parseEndTag (tagName, start, end) {
var pos, lowerCasedTagName;
// Find the innerStack node object pos whose lowerCasedTagName is equal to the innerStack nearest the top
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break}}}else {
pos = 0;
}
if (pos >= 0) {
for (var i = stack.length - 1; i >= pos; i--) {
// Then execute the end hook function
if(options.end) { options.end(stack[i].tag, start, end); }}// Push the closed tag out of the innerStack, and the processing is complete
stack.length = pos;
lastTag = pos && stack[pos - 1].tag; }}Copy the code
End hook function:
- Push element out of outterStack
- CurrentParent points to the element’s parent node
- Rewrite the derived element. End
- Perform closeElement (element)
function end (tag, start, end$1) {
// Find the top elment in the outerStack
var element = stack[stack.length - 1];
// Push this elment out of the stack
stack.length -= 1;
// currentParent points to the new top of the stack
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
element.end = end$1;
}
closeElement(element);
},
Copy the code
CloseElement function
- Instructions for processing vue, etc., mounted on element (ignored)
- If you have currentParent currentParent. Children. Push (element)
function closeElement (element) {
if(! inVPre && ! element.processed) { element = processElement(element, options); }if(currentParent && ! element.forbidden) {if (element.elseif || element.else) {
Elseif / / processing
processIfConditions(element, currentParent);
} else {
/ / handle slot
if (element.slotScope) {
var name = element.slotTarget || '"default"'; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element; }// Push the current element into the children of currentParent
currentParent.children.push(element);
// Assign currentParent to element.parent;element.parent = currentParent; }}}Copy the code
The last also performs the closing procedure described above. And finally root, the first element, is
{
attrs: [{...}]attrsList: [{...}]attrsMap: {id: "demo"}
children: [{
attrs: [{...}]attrsList: [{...}]attrsMap: {id: "child"}
children: [{
end: 41
start: 31
text: "childValue"
type: 3
}]
end: 47
parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... }plain: false
rawAttrsMap: {id: {... }}start: 15
tag: "div"
type: 1
}]
end: 53
parent: undefined
plain: false
rawAttrsMap: {id: {... }}start: 0
tag: "div"
type: 1
}
Copy the code
Vue Parse basic process
Variable bindings
We discussed how the parent node generates the AST. We know that all the information in the template tag will eventually be converted to a key in the AST. What will the final AST of the variable in the VUE look like if there is a DOM
<div id="demo" :class="calssName"><div v-if="c">{{a + b}}</div><div v-else>{{a-b}}</div><div v-for="item in branches">{{item}}</div></div>
Copy the code
The resulting AST looks like this. You can focus on the parts I pointed out, where _s = toString();
{
attrs: [{...}]attrsList: [{...}]attrsMap: {id: "demo"To:class:"calssName"}
children: (3) [
{
attrsList: []
attrsMap: {v-if: "c"}
children: [{
end: 57
/ / 👇
expression: "_s(a + b)"
start: 48
text: "{{a + b}}"
/ / 👇
tokens: [{@binding: "a + b"}]
type: 2
}]
end: 63
/ / 👇
if: "c"
/ / 👇
ifConditions: [{
// <div v-if="c">{{a + b}}</div>
block: {type: 1.tag: "div".attrsList: Array(0), attrsMap: {... },rawAttrsMap: {... },... }exp: "c"
}, {
// <div v-else>{{a-b}}</div>
block: {type: 1.tag: "div".attrsList: Array(0), attrsMap: {... },rawAttrsMap: {... },... }exp: undefined
}]
parent: {... }plain: true
rawAttrsMap: {v-if: {... }}start: 34
tag: "div"
type: 1}, {alias: "item"
attrsList: []
attrsMap: {v-for: "item in branches"}
children: [{
end: 101
/ / 👇
expression: "_s(item)"
start: 93
text: "{{item}}"
/ / 👇
tokens: [{@binding: "item"}]
type: 2
}]
end: 107
/ / 👇
for: "branches"
parent: {... }plain: true
rawAttrsMap: {v-for: {... }}start: 63
tag: "div"
type: 1}]/ / 👇
classBinding: "calssName"
end: 158
parent: undefined
plain: false
rawAttrsMap: {:class: {name: ":class".value: "calssName".start: 15.end: 33}
id: {name: "id".value: "demo".start: 5.end: 14}}start: 0
tag: "div"
type: 1
}
Copy the code