在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
一、分词/语法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token),考虑程序var a = 2这段程序通常会被分解为下面这些词法单元:var、a、=、2。
二、解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个元素逐级嵌套所组成的代表了程序语法结构的树,这个树被称之为抽象语法树(Abstract Syntax Tree)
三、代码生成
将AST转换成可执行代码的过程被称为代码生成。简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令,用来创建一个叫做a的变量,并将一个值存储在a中。
<div id='app'>
<p>hello {{ name }}</p>
world
</div>
转换为下面的render函数
_c('div', {id:'app'}, _c('p', undefined, _v('hello', + _s(name))), _v('world'))
Vue中Parser html实现
parser-html.js
// 模板编译的正则表达式
// Regular Expressions for parsing tags and attributes
// copy from packages\vue-template-compiler\browser.js
// 匹配标签伤的属性 id="app" id='app' id=app
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 标签:abc-aaa
var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z]*";
// <aaa:bbb>
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
// 标签开头的正则,捕获的内容是标签名
var startTagOpen = new RegExp(("^<" + qnameCapture));
// 匹配开始标签的结束符 >
var startTagClose = /^\s*(\/?)>/;
// 匹配结尾标签: </div>
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
var doctype = /^<!DOCTYPE [^>]+>/i;
var defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{ name }}
let root = null // AST语法树的树根
let currentParent // 标识当前父节点
let stack = [] // 栈,用于存储父节点
const ELEMENT_TYPE = 1 // 元素类型
const TEXT_TYPE = 3 // 文本类型
function createASTElement(tagName, attrs) {
return {
tag: tagName,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
/**
* 解析开始标签
* @param {*} tagName
* @param {*} attrs
*/
function start (tagName, attrs) {
// 遇到开始标签就创建一个AST元素
let element = createASTElement(tagName, attrs)
if(!root){
root = element
}
currentParent = element // 把当前元素标记成AST树的父节点
stack.push(element) // 把当前元素放入栈中
}
/**
* 处理文本
* @param {text} text
*/
function chars(text) {
text = text.replace(/\s/g, '')
if(text){
currentParent.children.push({
type: TEXT_TYPE,
text
})
}
}
/**
* 闭合标签
* @param {} tagName
*/
// eg. <div><p></p></div>
function end(tagName) {
// 拿到的是AST对象
let element = stack.pop()
// 标识当前标签是属于这个div的子元素
currentParent = stack[stack.length - 1]
if(currentParent){
element.parent = currentParent
currentParent.children.push(element) //实现了树的父子关系
}
}
export function parseHTML (html) {
// 循环解析html
while(html){
let textEnd = html.indexOf('<')
if(textEnd == 0){
// 如果当前索引为0,则是一个标签【开始标签,或者结束标签】
let startTagMatch = parseStartTag()
if(startTagMatch) {
// 解析开始标签
start(startTagMatch.tagName, startTagMatch.attrs)
continue
}
let endTagMatch = html.match(endTag)
if(endTagMatch) {
// 解析结束标签
advance(endTagMatch[0].length)
end(endTagMatch[1])
continue
}
}
let text;
if(textEnd >= 0) {
text = html.substring(0, textEnd)
}
if(text){
advance(text.length)
chars(text)
}
}
// 截取
function advance(n) {
html = html.substring(n)
}
// 解析开始标签
function parseStartTag() {
let start = html.match(startTagOpen)
// 匹配开始标签
if(start) {
let match = {
tagName: start[1],
attrs: []
}
advance(start[0].length)
let end,attr
// 匹配属性
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
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
}
render函数的字符串拼接
compiler/index.js
import { parseHTML } from './parser-html'
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(":")
if(typeof value === 'string') {
obj[key] = value.trim()
}
})
attr.value = obj
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, -1)}}`
}
// _c('div', {id:'app', style: {color: red; background: blue;}}, _c('p', undefined, _v('hello', + _s(name))), _v('world'))
function generate(el) {
let code = `_c("${el.tag}", ${
el.attrs.length ? genProps(el.attrs) : 'undefined' // undefined => {}
})
`
return code
}
// AST语法树:用对象来描述js的原生语法,虚拟DOM:用对象描述dom节点
export function compileToFunction (template) {
// 解析HTML字符串,将HTML字符串转换成AST语法树对象
let root = parseHTML(template)
// 需要将ast语法树生成最终的render函数,也就是字符串的拼接(模板引擎)
let code = generate(root)
console.log('code===', code)
// <div id='app'><p>hello {{ name }}</p>world</div>
// _c('div', {id:'app'}, _c('p', undefined, _v('hello', + _s(name))), _v('world'))
return function render(){
}
}
code=== _c("div", {id:"app",style:{"color":"red"," background":"blue"}})