Lexer(词法分析器)
Lexer负责将源代码字符串转换为标记序列(tokens),并进行数据清洗,去除无效字符如空格和换行。
清洗规则
- 去除无效字符(如无意义的空格和换行)。
- 保留有意义的空格(例如类选择器之间的空格)。
词法分割
将清洗后的字符串分割成tokens
Parser(语法分析器)
Parser中文是语法分析,拿到Lexer的结果Tokens后,需要将Tokens转为AST,得到一棵具有语法含义的抽象语法树。
Lexer实现方案1 正则匹配
Lexer其中一种代码实现是使用正则匹配,但是由于Lexer相对复杂,使用正则很容易写得很长,变得可读性差,并且后期难以进行性能调优,例如下面Vue2的Lexer实现。
// Regular Expressions for parsing tags and attributes
var attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)[[^=]+?][^\s"'<>/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var ncname = "[a-zA-Z_][\-\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
var qnameCapture = "((?:" + ncname + "\:)?" + ncname + ")";
var startTagOpen = new RegExp(("^<" + qnameCapture));
var startTagClose = /^\s*(/?)>/;
var endTag = new RegExp(("^<\/" + qnameCapture + "[^>]*>"));
var doctype = /^<!DOCTYPE [^>]+>/i;
// #7298: escape - to avoid being passed as HTML comment when inlined in page
var comment = /^<!--/;
var conditionalComment = /^<![/;
Lexer实现方案2 有限状态机
Vue3里面使用有限状态机替换了正则匹配进行改进,并且整体编译性能得到提升,并且可读性好
案例,使用有限状态机方式实现
一道萌时科技 moego 的面试题
const input = '<div><h1>Title</h1><p>Description here</p></div>'
const output = {
"tagName": "div",
"children": [
{
"tagName": "h1",
"children": [
"Title"
]
},
{
"tagName": "p",
"children": [
"Description here"
]
}
]
}
实现
// 定义TokenType
const TokenType = {
startTag: 'startTag',
endTag: 'endTag',
text: 'text',
eof: 'eof'
};
// Lexer类
class Lexer {
constructor(input) {
this.text = input;
this.pos = 0;
this.current_char = this.text[this.pos];
}
// 步进器: 功能是进⾏字符串的下标偏移,并获取当前下标指向的下标
advance() {
this.pos += 1;
if (this.pos >= this.text.length) {
this.current_char = null;
} else {
this.current_char = this.text[this.pos];
}
}
_get_next_token() {
while (this.current_char !== null) {
// 遇到空格的时候,需要判断该空格是否有意义,有意思的空格返回⼀个token type 为 space的token
if (this.current_char === ' ') {
this.advance();
continue;
}
// \n,⽬前都是⽆意义的,可以直接步进器跳过
if (this.current_char === '\n') {
this.advance();
continue;
}
if (this.current_char === '<') {
this.advance();
if (this.current_char === '/') {
// 结束标签
this.advance();
let tagName = '';
while (isAlpha(this.current_char) || this.current_char === '-' || this.current_char === '_') {
tagName += this.current_char;
this.advance();
}
// 跳过属性部分,直到 '>'
while (this.current_char !== '>' && this.current_char !== null) {
this.advance();
}
if (this.current_char !== '>') {
throw new Error('Invalid end tag');
}
this.advance();
return new Token(TokenType.endTag, tagName);
} else {
// 开始标签
let tagName = '';
while (isAlpha(this.current_char) || this.current_char === '-' || this.current_char === '_') {
tagName += this.current_char;
this.advance();
}
// 跳过属性部分,直到 '>'
while (this.current_char !== '>' && this.current_char !== null) {
this.advance();
}
if (this.current_char !== '>') {
throw new Error('Invalid start tag');
}
this.advance();
return new Token(TokenType.startTag, tagName);
}
} else {
// 文本
let text = '';
while (this.current_char !== null && this.current_char !== '<') {
text += this.current_char;
this.advance();
}
if (text.trim() !== '') {
return new Token(TokenType.text, text.trim());
} else {
continue;
}
}
}
return new Token(TokenType.eof, null);
}
}
// 辅助函数
function isAlpha(char) {
return /[a-zA-Z0-9_-]/.test(char);
}
// Token类
class Token {
constructor(type, value) {
this.type = type;
this.value = value;
}
}
// Parser函数
function parseHTML(input) {
const lexer = new Lexer(input);
const stack = [];
const root = { tagName: 'root', children: [] };
stack.push(root);
let currentToken = lexer._get_next_token();
while (currentToken.type !== TokenType.eof) {
if (currentToken.type === TokenType.startTag) {
const newNode = { tagName: currentToken.value, children: [] };
stack.push(newNode);
stack[stack.length - 2].children.push(newNode);
currentToken = lexer._get_next_token();
} else if (currentToken.type === TokenType.endTag) {
stack.pop();
currentToken = lexer._get_next_token();
} else if (currentToken.type === TokenType.text && currentToken.value !== '') {
stack[stack.length - 1].children.push(currentToken.value);
currentToken = lexer._get_next_token();
} else {
currentToken = lexer._get_next_token();
}
}
return root.children[0];
}
// 测试代码
const input = '<div class="container"><h1>Title</h1><p>Description here</p></div>';
const output = parseHTML(input);
console.log(JSON.stringify(output, null, 2));