【Lexer 实现方案】

396 阅读2分钟

Lexer(词法分析器)

Lexer负责将源代码字符串转换为标记序列(tokens),并进行数据清洗,去除无效字符如空格和换行。

清洗规则

  • 去除无效字符(如无意义的空格和换行)。
  • 保留有意义的空格(例如类选择器之间的空格)。

词法分割

将清洗后的字符串分割成tokens

Parser(语法分析器)

Parser中文是语法分析,拿到Lexer的结果Tokens后,需要将Tokens转为AST,得到一棵具有语法含义的抽象语法树。

Lexer实现方案1 正则匹配

Lexer其中一种代码实现是使用正则匹配,但是由于Lexer相对复杂,使用正则很容易写得很长,变得可读性差,并且后期难以进行性能调优,例如下面Vue2Lexer实现。

github.com/vuejs/vue/b…

// 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"
      ]
    }
  ]
}

题目来源:juejin.cn/post/737212…

实现

 // 定义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));

案例2 string to token

github.com/GrammyLi/co…