Mind mapping
preface
The last article mainly wrote Vue’s data response principle is how to achieve, involving the object of data hijacking, array variation method rewrite, as well as data below the convenient value. Vue source exploration (a) responsive principle.
In this article, we will focus on the principle of Vue template compilation. First, we will review,
var vm = new Vue({
el: '#app'.data: {
message: 'Hello Vue! '.arr: [1.2.3]},template: '<div id="app">{{message}}</div>'
})
console.log(vm.$options.render)
Copy the code
Let’s take a look at the printout, which refers to the official Vue code.
So the template we wrote in options is turned into a render function by Vue, and we can render the page using this function. Vue needs to process the template code in order to make the function of native HTML more powerful. That’s where the template compilation process comes in.
The main purpose of this article is to figure out how Vue internally converts the Template string into a render function.
The body of the
1. Template compiler function entry
import { compileToFunction } from "./compiler"
import { initState } from "./state"
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
// This is an instance of new outside
const vm = this
// Put the user's options on the VM so that they are available in other methods
vm.$options = options // The $options option is available for subsequent methods
// Options contains a number of options el data props
initState(vm)
// Template compiler entry
if(vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Vue.prototype.$mount = function(el){
// Get the current instance, or property, inside the constructor's prototype method, and add parameters to the property dynamically.
const vm = this
const options = vm.$options
el = document.querySelector(el) // Get the real element
vm.$el = el
// The user did not pass the render function
if(! options.render){// The user did not pass the template string
let template = options.template
if(! template && el) { template = el.outerHTML }// Get the HTML of the user-passed EL element as template and compile it into the render function
let render = compileToFunction(template)
// Put the render function on top of options.
options.render = render
}
}
}
Copy the code
To generate the render function, we first need to get the AST code generated by the template parsing.
2, template compilation core entrance
import { generate } from "./generate";
import { parserHTML } from "./parser";
export function compileToFunction(template) {
// Make the template an AST
let ast = parserHTML(template)
// Code optimization, static node marking
// Code generation
let code = generate(ast)
let render = new Function(`with(this){return ${code}} `);
console.log(render.toString())
}
Copy the code
3. Convert template to AST
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // Match the aa-xxx of the label name
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `; // aa:aa-xxx
const startTagOpen = new RegExp(` ^ <${qnameCapture}`); // This re can match the first tag name that matches the result (index first) [1]
const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `); // Match at the end of the tag [1]
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /; // Match the attribute
/ / [1] attribute key [3] | | [4] | | [5] the value of the attribute a = 1 a = '1' a = ""
const startTagClose = /^\s*(\/?) >/; // Match tag end /> >
const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g; // {{ xxx }}
// Vue3 compiles much better than vue2, with fewer regex's
export function parserHTML(html) {
// You can continue to intercept templates until all templates are parsed
let stack = [];
let root = null;
// I want to build a father-son relationship
function createASTElement(tag, attrs, parent = null) {
return {
tag,
type: 1./ / element
children: [],
parent,
attrs
}
}
function start(tag, attrs) { // [div,p]
// The parent node is the last one in the stack
let parent = stack[stack.length - 1];
let element = createASTElement(tag, attrs, parent);
if (root == null) { // The current node is the root node
root = element
}
if (parent) {
element.parent = parent; // point to parent with the parent property of new p
parent.children.push(element);
}
stack.push(element)
}
function end(tagName) {
// When the end tag matches, it is removed from the stack
let endTag = stack.pop();
if(endTag.tag ! = tagName) {console.log('Wrong label')}}function text(chars) {
let parent = stack[stack.length - 1];
chars = chars.replace(/\s/g."");
if (chars) {
parent.children.push({
type: 2.text: chars
})
}
}
function advance(len) {
html = html.substring(len);
}
function parseStartTag() {
const start = html.match(startTagOpen); / / 4.30 to continue
if (start) {
const match = {
tagName: start[1].attrs: []
}
advance(start[0].length);
let end;
let attr;
while(! (end = html.match(startTagClose)) && (attr = html.match(attribute))) {// 1 must have attribute 2, not the end tag of the beginning
match.attrs.push({ name: attr[1].value: attr[3] || attr[4] || attr[5]}); advance(attr[0].length);
} // <div id="app" a=1 b=2 >
if (end) {
advance(end[0].length);
}
return match;
}
return false;
}
while (html) {
// Parse labels and text
let index = html.indexOf('<');
if (index == 0) {
const startTagMatch = parseStartTag()
if (startTagMatch) { // Start tag
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
let endTagMatch;
if (endTagMatch = html.match(endTag)) { // End tag
end(endTagMatch[1]);
advance(endTagMatch[0].length);
continue; }}/ / text
if (index > 0) { / / text
let chars = html.substring(0, index) //<div></div>
text(chars);
advance(chars.length)
}
}
return root;
}
Copy the code
Summary: using regular expressions, match the start and end tags, and use the stack to record to match to the label of the ast structure, when meet start tag to the ast node into the stack, take the last element of the on the top of the stack as the parent node to join the element node, when the match to the end tag, the current label node is removed from the stack. Advance continuously intercepts matching strings until all strings are parsed.
All this code does is convert the template into a JS object
<div id="app">
{{message}}
</div>
Copy the code
The transformation results are as follows.
{
"tag": "div"."type": 1.// Element node
"children": [{"type": 2.// Text node
"text": "{{message}}"}]."parent": null."attrs": [{"name": "id"."value": "app"}}]Copy the code
For now, you can go to the # Vue Template Explorer to get an idea of what the final render function will look like.
Generate render function with AST
const defaultTagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g; // {{ xxx }}
function genProps(attrs) {
// {key:value,key:value,}
let str = ' ';
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === 'style') { // {name:id,value:'app'}
let styles = {}
attr.value.replace(/([^;:]+):([^;:]+)/g.function () {
styles[arguments[1]] = arguments[2];
})
attr.value = styles
}
str += `${attr.name}:The ${JSON.stringify(attr.value)}, `
}
return ` {${str.slice(0, -1)}} `
}
function gen(el) {
if (el.type == 1) {
return generate(el); // If it is an element, it is generated recursively
} else {
let text = el.text; / / {{}}
if(! defaultTagRE.test(text))return `_v('${text}') `; // The description is plain text
/ / that have expression I need to do an expression and average value of splicing [' aaaa '_s (name),' BBB ']. Join (' +)
// _v('aaaa'+_s(name) + 'bbb')
let lastIndex = defaultTagRE.lastIndex = 0;
let tokens = []; // <div> aaa{{bbb}} aaa </div>
let match;
// lastIndex is automatically moved backwards every time a match is made
while (match = defaultTagRE.exec(text)) { // If the re + g is used with exec, there will be a problem with lastIndex
let index = match.index;
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
tokens.push(`_s(${match[1].trim()}) `);
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
return `_v(${tokens.join('+')}) `; // webpack source code csS-loader image processing}}function genChildren(el) {
let children = el.children;
if (children) {
return children.map(item= > gen(item)).join(', ')}return false;
}
// _c(div,{},c1,c2,c3,c4)
export function generate(ast) {
let children = genChildren(ast)
let code = `_c('${ast.tag}',${ast.attrs.length ? genProps(ast.attrs) : 'undefined'
}${children ? `,${children}` : ' '
}) `
return code;
}
Copy the code
conclusion
In fact, in this video, just remember a few important steps, and as for the internal details, you can have time to look at the corresponding internal details.
- Template is converted to ATS.
- Ast compiles into the render function.
Links to a series of articles (constantly updated…)
Vue source exploration (a) responsive principle
Vue source exploration (two) template compilation
A preliminary study of Vue source code (3) Single component mount (rendering)
Vue source exploration (four) dependent (object) collection process
Object asynchronous update nextTick()