需求: 实现 jsx 转 js 的编译器 github: github.com/vaynevayne/…
之前
<h1 id='title'><span>hello</span> world </h1>
之后
Rect.createElement("h1",{
id:"title"
}, React.createElement('span',null,"hello"),"world")
编译器工作流:
- 解析(Parse)将源代码转换成抽象语法书 ast
- 遍历(Traversal) 遍历 ast
- 转换(transformation) 编辑 ast,例如js转java
- 代码生成(Code generate) 将 ast 转换成新代码
Parse
分为两个阶段: 词法分析 和 语法分析
- 词法分析: 源代码转 token
- 语法分析: token 转 ast
原始jsx代码
<h1 id='title'><span>hello</span> world </h1>
token:
[
{ type: 'Punctuator', value: '<' },
{ type: 'JSXIdentifier', value: 'h1' },
{ type: 'JSXIdentifier', value: 'id' },
{ type: 'Punctuator', value: '=' },
{ type: 'String', value: "'title'" },
{ type: 'Punctuator', value: '>' },
{ type: 'Punctuator', value: '<' },
{ type: 'JSXIdentifier', value: 'span' },
{ type: 'Punctuator', value: '>' },
{ type: 'JSXText', value: 'hello' },
{ type: 'Punctuator', value: '<' },
{ type: 'Punctuator', value: '/' },
{ type: 'JSXIdentifier', value: 'span' },
{ type: 'Punctuator', value: '>' },
{ type: 'JSXText', value: ' world ' },
{ type: 'Punctuator', value: '<' },
{ type: 'Punctuator', value: '/' },
{ type: 'JSXIdentifier', value: 'h1' },
{ type: 'Punctuator', value: '>' }
]
可以借助 esprima 看一下效果
/**
* 1. 把源代码进行分词, 得到一个token数组
* 2. 把token数组转成一个抽象语法树
*/
let esprima = require("esprima");
let sourceCode = `<h1 id='title'><span>hello</span> world </h1>`;
let ast = esprima.parseModule(sourceCode, { jsx: true, tokens: true }); // 打印token
console.log(ast);
如何分词? 需要借助有限状态机的概念
有限状态机
- 每一个状态都是一个机器, 每个机器都可以接受输入和计算输出
- 机器本身没有状态,每一个机器会根据输入决定下一个状态
先看一个简单的demo, 支持 10+20 的分词
let NUMBERS = /[0-9]/;
const Numeric = "Numeric";
const Punctuator = "Punctuator";
let tokens = [];
let currentToken;
// 确定一个新token
function emit(token) {
currentToken = { type: "", value: "" };
tokens.push(token);
}
/**
*
* start 表示开始状态
* 它是一个函数, 接受一个字符,返回下一个状态函数
*/
function start(char) {
//char =1
if (NUMBERS.test(char)) {
// 如果char是一个数字的话
currentToken = { type: Numeric, value: "" };
}
// 进入新的状态, 收集number数字的状态
return number(char);
}
function number(char) {
if (NUMBERS.test(char)) {
// 如果char是一个数字的话
currentToken.value += char;
} else if (char === "+" || char === "-") {
emit(currentToken);
emit({
type: Punctuator,
value: char,
});
currentToken = { type: Numeric, value: "" };
}
return number;
}
function tokenizer(input) {
// 刚开始的时候是char的状态
let state = start;
for (let char of input) {
state = state(char);
}
if (currentToken.value.length > 0) {
emit(currentToken);
}
}
// 10 + 20
tokenizer("10+20");
console.log(tokens);
/**
* [
* { type: 'Numeric', value: '10' },
* { type: 'Punctuator', value: '+' },
* { type: 'Numeric', value: '20' }
* ]
*/
词法分析实现
// ./tokenTypes
exports.LeftParentheses = "LeftParentheses"; // <
exports.JSXIdentifier = "JSXIdentifier"; //标识符
exports.AttributeKey = "AttributeKey"; // 属性的key
exports.AttributeStringValue = "AttributeStringValue"; // 字符串格式的属性值
exports.AttributeExpressionValue = "AttributeExpressionValue"; // 变量的属性值
exports.RightParentheses = "RightParentheses"; // > 开始标签的结束
exports.JSXText = "JSXText"; // 文本
exports.BackSlash = "BackSlash"; // 反斜杠
// tokenizer.js
const LETTERS = /[a-z0-9]/;
const tokenTypes = require("./tokenTypes");
let currentToken = { type: "", value: "" };
const tokens = [];
function emit(token) {
currentToken = { type: "", value: "" };
tokens.push(token);
}
function start(char) {
if (char === "<") {
emit({ type: tokenTypes.LeftParentheses, value: "<" });
return foundLeftParentheses; // 找到<
}
throw new Error("第一个字符必须是<");
}
// end of fire
function eof() {
if (currentToken.value.length > 0) {
emit(currentToken);
}
}
function foundLeftParentheses(char) {
// h1
console.log("foundLeftParentheses", char);
if (LETTERS.test(char)) {
// 如果char是一个小写字母或数字
currentToken.type = tokenTypes.JSXIdentifier;
currentToken.value += char; // h
return jsxIdentifier; // 继续收集标识符
} else if (char === "/") {
emit({ type: tokenTypes.BackSlash, value: "/" });
console.log("tokens", tokens);
return foundLeftParentheses; // 这里借助左边的来找
}
}
function jsxIdentifier(char) {
if (LETTERS.test(char)) {
currentToken.value += char;
return jsxIdentifier;
} else if (char === " ") {
// 遇到空格
emit(currentToken);
return attribute;
} else if (char === ">") {
// 说明没有属性 直接结束
emit(currentToken);
emit({ type: tokenTypes.RightParentheses, value: ">" });
return foundRightParentheses;
}
// return eof;
}
function attribute(char) {
// i
if (LETTERS.test(char)) {
// 将会是key
currentToken.type = tokenTypes.AttributeKey;
currentToken.value += char;
return attributeKey;
}
throw new TypeError("Error");
}
function attributeKey(char) {
if (LETTERS.test(char)) {
currentToken.value += char;
return attributeKey;
} else if (char === "=") {
// 属性key的名字已经结束了
emit(currentToken);
return attributeValue;
}
}
function attributeValue(char) {
// char = "
if (char === '"') {
currentToken.type = tokenTypes.AttributeStringValue;
currentToken.value = char;
return attributeStringValue; // 开始读字符串属性值
} else if (char === "{") {
currentToken.type = tokenTypes.AttributeExpressionValue;
currentToken.value = char;
return attributeExpressionValue;
}
}
function attributeExpressionValue(char) {
if (LETTERS.test(char)) {
currentToken.value += char;
return attributeExpressionValue;
} else if (char === "}") {
// 说明字符串的值结束了
currentToken.value += char;
emit(currentToken); // {type:'AttributeStringValue', value:'title' }
return tryLeaveAttribute;
}
throw new TypeError("Error");
}
function attributeStringValue(char) {
// t
if (LETTERS.test(char)) {
currentToken.value += char;
return attributeStringValue;
} else if (char === '"') {
// 说明字符串的值结束了
currentToken.value += char;
emit(currentToken); // {type:'AttributeStringValue', value:'title' }
return tryLeaveAttribute;
}
throw new TypeError("Error");
}
// 后面可能是一个新属性,也坑是开始标签的结束
function tryLeaveAttribute(char) {
if (char === " ") {
return attribute; // 后面是空格, 说明后面是一个新属性
} else if (char === ">") {
// '<h1 id="title">
emit({
type: tokenTypes.RightParentheses,
value: ">",
});
return foundRightParentheses;
}
}
function foundRightParentheses(char) {
if (char === "<") {
// '<h1 id="title"><
emit({ type: tokenTypes.LeftParentheses, value: "<" });
return foundLeftParentheses; // 找到<
} else {
// <h1 id='title'><span>h
currentToken.type = tokenTypes.JSXText;
currentToken.value += char;
return jsxText;
}
}
function jsxText(char) {
if (char === "<") {
emit(currentToken); // {type:'JSXText',value:'hello'}
emit({ type: tokenTypes.LeftParentheses, value: "<" });
return foundLeftParentheses;
} else {
currentToken.value += char;
return jsxText;
}
}
function tokenizer(input) {
let state = start;
for (let char of input) {
if (state) state = state(char);
}
return tokens;
}
module.export = {
tokenizer,
};
let sourceCode = `<h1 id="title" name={name}><span>hello</span>world</h1>`;
console.log(tokenizer(sourceCode));