前言
vue的模板编译就是
1、将template转化成 ast语法树;
2、优化标记静态节点 (patchFlag,BlockTree)
3、将 ast 转换成 render方法
1、创造一个vue的实例
<div id="app"><span class="a">name</span>{{name}}</div>
const vm = new Vue({
el:'#app',
data() {
return {
name: 'xiaoming',
}
},
});
2、根据情况确定使用模板
看生命周期描述
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options
el = document.querySelector(el);
if(!options.render){ //看是否有render 属性
let template = options.template;
if(!template && el){ // 没有写模板但是写了el ,就去取el作为模板
template = el.outerHTML; // <div id="app"><span>{{name}}</span>{{age}}</div>
}
let render = compileToFunction(template);
options.render = render;
}
}
3、调用 compileToFunction 生成一个render
export function compileToFunction(html) {
// 编译流程有三个部分 1.把模板变成ast语法树
// 2.优化标记静态节点 (patchFlag,BlockTree)
// 3.把ast变成render函数
const ast = parserHTML(html);
// 2.优化标记静态节点
// 3.将ast变成render函数 你要把刚才这棵树 用字符串拼接的方式 变成render函数
const code = generate(ast); // 根据ast生成一个代码字符串
// 通过 new Function + with 的方式,将一个code 字符串 变为可执行的函数
const render = new Function(`with(this){return ${code}}`);
return render;
}
3.1 parserHTML 解析html字符串,将模板变为ast语法树
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; // match匹配的是标签名
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的 分组里放的就是 "b",'b' ,b => (b) 3 | 4 | 5
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 <br/> <div>
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // {{ asdasd }}
export function parserHTML(html) {
function advance(len) {
html = html.substring(len);
}
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length);
let attr;
let end;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] });
advance(attr[0].length);
}
advance(end[0].length);
return match;
}
return false;
}
let root = null;
let stack = [];
let parent = null;
function createAstElement(tag, attrs) {
return {
tag,
type: 1,
attrs,
children: [],
parent: null
}
}
function start(tagName, attrs) { // 匹配到了开始的标签
let element = createAstElement(tagName, attrs);
if (!root) {
root = element
}
let parent = stack[stack.length - 1];
if (parent) {
element.parent = parent; // 当放入span的时候 我就知道div是他的父亲
parent.children.push(element);
}
stack.push(element);
}
function chars(text) { // 匹配到了开始的标签
let parent = stack[stack.length - 1];
text = text.replace(/\s/g,''); // 遇到空格就删除掉
if(text){
parent.children.push({
text,
type:3
});
}
}
function end(tagName) {
stack.pop(); // 每次出去就在栈中删除当前这一项, 这里你可以判断标签是否出错
}
while (html) { // html只能由一个根节点
let textEnd = html.indexOf('<');
if (textEnd == 0) { // 如果遇到< 说明可能是开始标签或者结束标签
const startTagMatch = parseStartTag();
// console.log(startTagMatch)
if (startTagMatch) { // 匹配到了开始标签
start(startTagMatch.tagName, startTagMatch.attrs);
continue
}
// 如果代码走到这里了 说明是结束标签
const endTagMatch = html.match(endTag);
if (endTagMatch) {
end(endTagMatch[1]);
advance(endTagMatch[0].length);
}
}
let text;
if (textEnd > 0) {
text = html.substring(0, textEnd)
}
if (text) {
chars(text);
advance(text.length);
}
}
return root;
}
3.2 调用 generate 函数,将解析完成的html变为函数字符串
遍历 ast 树,拼接成字符串_c("div",{id:"app"},[_c("span",{class:"a"},[_v("name")]),_v(_s(name))])。
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // {{ asdasd }}
function genProps(attrs) {
let str = '';
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === 'style') {
let style = {} // color:red;background:blue
attr.value.replace(/([^;:]+)\:([^;:]+)/g, function() {
style[arguments[1]] = arguments[2]
}); // 如果是sytle 我要将style转换成一个对象
attr.value = style;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0,-1)}}`
}
function gen(el) {
if (el.type == 1) {
return generate(el)
} else {
let text = el.text
if (!defaultTagRE.test(text)) {
return `_v("${text}")`
} else {
let tokens = [];
let match;
let lastIndex = defaultTagRE.lastIndex = 0; // 保证每次正则都是从0 开始匹配的
while (match = defaultTagRE.exec(text)) { // 如果exec + 全局匹配每次执行的时候 都需要还原lastIndex
let index = match.index; // 匹配到后将前面一段放到tokens中
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
tokens.push(`_s(${match[1].trim()})`); // 把当前这一段放到tokens中
lastIndex = index + match[0].length
}
if(lastIndex < text.length){
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join('+')})`
}
}
}
function genChildren(ast) {
let children = ast.children; // _c('div',{},'xxx') _c('div',{},[])
if (children && children.length > 0) {
return children.map(child => gen(child)).join(',')
}
return false;
}
export function generate(ast) {
let children = genChildren(ast)
let code = `_c("${ast.tag}",${
ast.attrs.length? genProps(ast.attrs) : 'undefined'
}${
children? ',['+children+']' : ''
})`
return code;
}