1、第一步将模板解析成ast语法树
// 模板编译原理
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
export function parserHTML(html) {
// 可以不停的截取模板, 直到把模板全部解析完毕
let stack = []
let root = null;
// 构建父子关系
function createASTElement(tag,attrs,parent) {
return {
tag,
type: 1, // 元素
children: [],
parent,
attrs
}
}
function start (tag,attrs) { // [div,p]
// 遇到开始标签 就取栈中的最后一个作为节点
let parent = stack[stack.length-1]
let element = createASTElement(tag,attrs,parent)
if (root === null) { // 说明当前节点就是根节点
root = element
}
if (parent) {
element.parent = parent // 更新p的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) {
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)
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、要有属性 2、不是开始标签的结束标签
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
} else {
return false
}
}
while(html) {
// 解析标签和文本
let index = html.indexOf('<')
debugger
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;
}
}
if (index > 0) { // 文本
let chars = html.substring(0,index)
text(chars)
advance(chars.length)
}
}
// console.log('root',root)
return root
}
2、第二步根据生成的ast树生成代码
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
function genProps(attrs) {
let str = '';
for(let i = 0; i < attrs.length; i++) {
let attr = attrs[i]
if (attr.name === 'style') {
let styles = {}
attr.value.replace(/([^;:]+):([^;:]+)/g,function () {
styles[arguments[1]] = arguments[2]
})
attr.value = styles
}
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}')` // 说明就是普通文本
// 说明有表达式 我需要做一个表达式和普通值的拼接['aaaa',_s(name),'bbbb'].join('+')
// _v('aaa'+_s(name)+'bbb')
let lastIndex = defaultTagRE.lastIndex = 0 // 每次都需要重置为0
let tokens = []
let match;
while(match = defaultTagRE.exec(text)) { // 如果正则+ g配合exec就会有一个问题lastIndex的问题 每次匹配完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('+')})`
}
}
function genChildren(ast) {
let children = ast.children
if (children) {
return `${children.map(c=>gen(c)).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
}
注意正则 + g的配合使用造成的lastIndex问题,需要将lastIndex手动置为0
3、第三步根据生成的code
利用 new Function + with
生成render函数
export function compileToFunction (template) {
// 1、将模板变成ast语法树
let ast = parserHTML(template)
// 2、代码生成
let code = generate(ast)
let render = `with(this){return ${code}}`;
let renderFn = new Function(render);
return renderFn
}