在创建vue实例时,会传入el或者template模板,在vue内部会将模板编译成一个渲染函数render。模板编译的过程如下:
compileToFunction将模板转为render函数
function compileToFunction(template) {
let root = parseHTML(template)
let code = generate(root)
let renderFn = new Function(`with(this){return ${code}}`)
return renderFn
}
复制代码
生成AST语法树
AST语法树,是源代码抽象语法结构的树状表现形式。树上的每个节点都表示源代码中的一种结构。
parseHTML
const ncname = '[a-zA-Z_][\\w\\-\\.]*'; // 匹配abc-aaa
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //
const startTagOpen = new RegExp(`^
const endTag = new RegExp(`^]*>`);// 匹配标签结尾
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;// 匹配属性
const startTagClose = /^\s*(\/?)>/;// 匹配标签结束的>
let root = null; // ast的树根
let curParent; // 标识当前父级
let stack = [];
const ELEMENT_TYPE = 1
const TEXT_TYPE = 3;
function parseHTML(html) {
while(html) { // 循环匹配
let textEnd = html.indexOf('
if(textEnd == 0) { // 如果是开头 可以匹配或者 xx>
let startTagMatch = parseStartTag(); // 1. 匹配
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
let endTagMatch = html.match(endTag) // 2.匹配
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]);
continue;
}
}
let text;
if (textEnd >= 0) { // 3.匹配文本
text = html.substring(0, textEnd)
}
if (text) {
advance(text.length);
chars(text)
}
}
// 从html上删除字符串
function advance(n) {
html = html.substring(n)
}
// 解析开始标签
function parseStartTag() {
let start = html.match(startTagOpen); // 匹配
if(start){
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length);
let end, attr;
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { // 匹配m=n
advance(attr[0].length);
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5]
})
}
if (end) { // 匹配>
advance(end[0].length);
return match
}
}
}
return root
}
复制代码
start + createASTElement处理tagName和attrs
// 创建ast语法树
function createASTElement(tagname, attrs) {
return {
tag: tagname,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
function start(tagName, attrs) {
let element = createASTElement(tagName, attrs);
if (!root) {
root = element
}
curParent = element;
stack.push(element)
}
复制代码
chars处理文本
function chars(text){
text = text.replace(/\s/g, '');
if(text) {
curParent.children.push({
text,
type: TEXT_TYPE
})
}
}
复制代码
end处理父子关系
定义全局变量stack和cuParent;
当start时,保存curParent = element和stack.push(element)
当end时,通过pop获取stack最后一项,它为element;它的父级为新stack的最后一项;建立双向父子关系
function end(tagName) {
let element = stack.pop();
curParent = stack[stack.length - 1]
if (curParent) {
element.parent = curParent;
curParent.children.push(element)
}
}
复制代码
将AST语法树generate为模板code--模板引擎
转为render函数是一个字符串拼接的过程。如果是元素,用_c包裹;如果是文本,用_v包裹;如果是变量,用_s包裹
generate:将AST转为模板字符串
function generate(el){
const {tag, attrs, children} = el
let newChildren = genChildren(el)
// _c创建元素
let code = `_c('${tag}', ${
attrs.length > 0 ? genProps(attrs) : 'undefined'
}, ${
newChildren ? newChildren : ''
})`;
console.log(code, 'code')
return code
}
复制代码
genProps:将属性数组转为属性子符串
function genProps(attrs){
let str = '';
for(let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === 'style') {
let obj = {};
attr.value.split(';').forEach(item => {
let [key, value] = item.split(':')
obj[key] = value
});
attr.value = JSON.stringify(obj);
str += `${attr.name}:${attr.value},`
} else {
str += `${attr.name}:'${attr.value}',`
}
}
return `{${str.slice(0, -1)}}`
}
复制代码
genChildren:将children转为字符串children
function genChildren(el) {
let {children} = el;
if(children && children.length) {
return `${children.map(c => genC(c)).join(',')}`
} else {
return false
}
}
复制代码
genC:根据child类型不同,进行不同转译
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{任意字符|换行}}
function genC(node) {
if (node.type === 1) { //元素时,递归调用genenrate
return generate(node)
} else if (node.type === 3) {//文本时,区分普通文本和变量
let text = node.text;
let tokens = [];
let match, index;
let lastIndex = defaultTagRE.lastIndex = 0;
while(match = defaultTagRE.exec(text)) {
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('+')})`
}
}
复制代码
生成render
通过with改变作用域
通过newFunction生成新的function
let renderFn = new Function(`with(this){return ${code}}`)
复制代码