模版编译
挂载模版
将vue实例挂载在html模版上,通常有三种方式:1、render()函数;2、template模版;3、根据el元素路径进行挂载。优先级如上顺序。最终都会转变成使用render函数编译模版。
Vue.prototype.$mount = function(el){
const opt = this.$options;
el = document.querySelector(el);
// 如果不存在render属性
if(!opt.render){
let template = opt.template;
// 如果不存在tempalte,但是有el;
if(!template && el){
template = el.outerHTML;
}
// 最终还是需要将template模版转化成render函数
if(template){
console.log(template);
let render = complileToFunction(template);
opt.render = render;
}
}
}
先对html模版解析生成ast语法树,其实就是使用正则表达式对html字符串进行匹配,匹配的过程中生成一个树状结构的对象,使用js对象来表示一个html结构,那么每个js对象自然也包含tagName、属性、父元素、子元素等。然后再利用ast树生成代码。
生成ast树
借用栈结构来构建父子关系,前一个元素是后一个元素的父节点。
<div>
<ul>
<li> </li>
</ul>
<p></p>
</div>
/**
* 原理:假设一棵结构如上。
* 当遇到开始标签时,将标签压入栈中,一直到遇到结束标签之前[div, ul, li],此时前者都是后者的父节点,在下一次操作
* 遇到结束</li>就弹出元素,此时栈中[div, ul],再遇到结束标签</ul>继续弹出元素,此时栈中[div],再遇到开始标签<p>,
* 压入栈中[div, p], 最后遇到结束标签</p></div>,依次弹出元素,最后栈为空了[]。
*/
// 以下为源码的正则
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束 >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性 形如 id=true id='app' id="app"
// 将html模版解析为ast语法树
function parseHtml(html){
const stack= [];
let root = null; // 根节点
function start(tagName, attrs){
const element = createASTElement(tagName, attrs);
if(!root){
// 根节点为空,说明当前是根节点
root = element;
}else{
const parent = stack[stack.length - 1]
element.parent = parent;
parent.children.push(element);
}
stack.push(element);
}
function end(tagName){
let endTag = stack.pop();
if(endTag.tag != tagName){
console.log('标签出错')
}
}
function text(chars){
chars = chars.replace(/\s/g, '');
const parent = stack[stack.length - 1];
if(chars){
parent.children.push({
text: chars,
type: 2,
parent,
});
}
}
function createASTElement(tag, attrs, type = 1, parent = null){
return {
tag,
type,
children: [],
parent,
attrs,
}
}
// 前进截取
function advance(len){
html = html.substring(len);
}
// 解析开始标签
function parseStartTag(){
const start = html.match(startTagOpen);
// 没有匹配到开始标签
if(!start) return false;
const match = {
tagName: start[1],
attrs: [],
}
// 减掉前面已匹配过的字符串,截取剩下的字符串
advance(start[0].length);
let end, attr;
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);
}
if(end){
advance(end[0].length);
}
return match;
}
// 不停的截取模版,直到模版为空
while(html){
let index = html.indexOf('<');
if(index == 0){
// 解析开始标签和属性
const startTagMatch = parseStartTag();
if(startTagMatch){ // 开始标签
start(startTagMatch.tagName, startTagMatch.attrs);
continue
}
let endTagMatch;
if(endTagMatch = html.match(endTag)){ // 结束标签
end(endTagMatch[1]);
advance(endTagMatch[0].length);
continue
}
break;
}
// 文本
if(index > 0){
let chars = html.substring(0, index);
text(chars);
advance(chars.length)
}
}
return root;
}
export default parseHtml;
利用ast树生成代码
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {{ }} 捕获花括号里面的内容
function genProps(attrs){
// 返回形式 ‘{key: value, key: value}’
let str = "";
attrs.forEach(obj => {
if(obj.name === 'style'){
// 改造style的值为一个对象
const style = {};
obj.value.replace(/([^;:]+):([^;:]+)/g, function(){
style[arguments[1]] = arguments[2];
})
obj.value = style;
}
str += `${obj.name}:${JSON.stringify(obj.value)},`;
});
return `{${str.slice(0, -1)}}`;
}
function genChildren(el){
let children = el.children;
if(children){
return `${children.map(item => gen(item)).join(',')}`;
}
return false;
}
function gen(el){
if(el.type == 1){
// 如果是元素就递归生成
return generate(el);
}else{
let text = el.text;
if(!defaultTagRE.test(text)) return `_v('${text}')`; // 说明只是普通文本
// 否者就是有表达式
let tokens = [];
let lastIndex = (defaultTagRE.lastIndex = 0); // lastIndex 记录的是上一次匹配到内容的下一个坐标,所以需要重置一下
let match = null;
while(match = defaultTagRE.exec(text)){
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('+')})`;
}
}
/**
*
* @param {*} ast
* @returns code
* 将ast树生成代码
*/
export function generate(ast){
let code = `_c('${ast.tag}', ${
ast.attrs.length ? genProps(ast.attrs) : 'undefined'
}${
ast.children.length ? `,${genChildren(ast)}` : ''
})`
return code;
}