expression-compiler 实现原理与关键代码
1. 整体架构
expression-compiler 是小程序开发框架中的表达式编译器,用于处理模板中的数据绑定表达式(如 、NaN)。它将这些表达式编译成可在运行时高效执行的 JavaScript 代码。
编译器采用经典的三阶段编译流程:
- 词法分析(Tokenizer):将表达式字符串拆分为标记(Token)序列
- 语法分析(Parser):将标记序列转换为抽象语法树(AST)
- 代码生成(Generator):将抽象语法树转换为 JavaScript 代码
2. 核心实现
2.1 编译入口
compiler 函数作为主入口,协调整个编译过程:
typescript
export const compiler = (input: string, options: Options = {}) => {
// 1. 词法分析:将输入字符串转换为标记流
const tokens = tokenizer(input, options);
// 2. 语法分析:将标记流构建为抽象语法树
const ast = parser(tokens);
// 3. 代码生成:将抽象语法树转换为 JavaScript 代码
const { expression, ...rest } = generator(ast);
// 4. 验证生成的表达式是否有语法错误
const code =
"function **EXPRESSION**(v){}; function **EXP**(v){}; function **ARG**(v){}; return " +
expression;
try {
new Function("ctx", code);
} catch (e) {
throw new SyntaxError(`Invalid expression: '${input}'`);
}
return { ...rest, expression };
};
2.2 词法分析器 (Tokenizer)
词法分析器负责识别表达式中的各种语法元素,如变量、运算符、分隔符等,并将它们转换为标记:
typescript
export function tokenizer(input: string, options?: Options): TNode[] {
// 创建解析上下文
const context = createParseContext(input);
const nodes: TNode[] = [];
// 逐字符分析输入字符串
while (!isEnd(context)) {
// 识别表达式边界 {{ ... }}
if (isExpressionStart(context)) {
const node = tokenizerExp(context);
pushNode(nodes, node);
} else if (isExpressionEnd(context)) {
const node = { type: NodeTypes.Expression, close: true };
pushNode(nodes, node);
advanceBy(context, close.length);
}
// 识别更多的标记类型...
else if (getCurrentChar(context) === SYMBOLS.LEFT_PAREN) {
const node = tokenizerGroup(context);
pushNode(nodes, node);
}
// 其他标记类型的处理...
else {
// 处理普通表达式内容
const node = tokenizerExpNode(context);
pushNode(nodes, node);
}
}
return nodes;
}
词法分析器定义了多种标记类型,包括:
- Identifier:标识符(变量名)
- Literal:字符串字面量
- NumberLiteral:数字字面量
- Operator:运算符
- Expression:表达式边界
- 以及其他类型,如对象、数组、模板字符串等
2.3 语法分析器 (Parser)
语法分析器将标记流转换为抽象语法树,识别表达式的结构和语义:
typescript
export function parser(tokens: TNode[]): AstNode {
let current = 0;
const tokensLen = tokens.length;
function walk(): AstNode {
let token = tokens[current];
const { isSpreadTarget } = token;
const node: AstNode = {};
if (isSpreadTarget) {
node.isSpreadTarget = true;
}
// 根据不同的标记类型构建 AST 节点
if (token.type === NodeTypes.Expression && token.open) {
// 处理表达式
Object.assign(node, { type: AstTypes.Expression, params: [] });
token = tokens[++current];
while (token.type !== NodeTypes.Expression) {
const astNode = walk();
node.params.push(astNode);
token = tokens[current];
}
current++;
} else if (token.type === NodeTypes.Identifier) {
// 处理标识符和属性访问
const nextToken = getNextTokenNotSpace();
if (
(nextToken &&
[NodeTypes.Dot, NodeTypes.ArrayExpression].includes(
nextToken.type
)) ||
!nextToken
) {
// 处理属性访问链,如 a.b.c 或 a[b]
Object.assign(node, {
type: AstTypes.DataExpression,
elements: [token.text],
});
token = tokens[++current];
while (
token &&
[
NodeTypes.MemberIdentifier,
NodeTypes.MethodIdentifier,
NodeTypes.Dot,
NodeTypes.ArrayExpression,
].includes(token.type)
) {
// 处理属性访问链的各部分
// ...
}
} else {
// 处理单独的标识符
current++;
if (/^([.\d]+)$/.test(token.text)) {
// 数字
Object.assign(node, {
type: AstTypes.NumberLiteral,
value: token.text,
});
} else if (/^(false|true)$/.test(token.text)) {
// 布尔值
Object.assign(node, {
type: AstTypes.BooleanLiteral,
value: token.text,
});
} else if (BUILTIN_OBJECT.test(token.text)) {
// 内置对象
Object.assign(node, {
type: AstTypes.BuiltinLiteral,
value: token.text,
});
} else {
// 变量
Object.assign(node, {
type: AstTypes.DataExpression,
elements: [token.text],
});
}
}
}
// 处理更多标记类型...
return node;
}
// 开始解析
return walk();
}
语法分析器引入了更多的 AST 类型,包括:
- DataExpression:数据访问表达式(如 user)
- IndexExpression:索引访问表达式(如 array[index])
- ObjectExpression:对象表达式(如 {a: 1, b: 2})
- ArrayExpression:数组表达式(如 [1, 2, 3])
2.4 代码生成器 (Generator)
代码生成器将 AST 转换为可执行的 JavaScript 代码,并收集表达式中使用的变量名:
typescript
export function generator(node: AstNode) {
exps.clear();
const expression = codeGenerator(node);
return {
expression,
exps: Array.from(exps).filter((n) => !["i18n", "I18n"].includes(n)),
};
}
function codeGenerator(
node: AstNode | string,
siblingNodes?: Array<AstNode | string>,
nodeIndex?: number,
expType?: ExpType
): string {
// 根据节点类型生成代码
const type = node.type;
if (type === AstTypes.Text) {
// 文本节点生成字符串字面量
return JSON.stringify(node.value);
} else if (type === AstTypes.DataExpression) {
// 数据访问表达式,如 user
const codeFrame = node.elements.map((item, index) => {
return codeGenerator(item, node.elements, index, ExpType.DataExpression);
});
exps.add(JSON.parse(codeFrame[0])); // 收集变量名
const code = `__EXP__(${codeFrame.join(",")})`;
return code;
} else if (type === AstTypes.Expression) {
// 完整表达式
const nodes = node.params;
let codeFrame = nodes
.map((node, index) => codeGenerator(node, nodes, index))
.join("");
if (!codeFrame.trim()) {
codeFrame = "undefined";
}
return `__EXPRESSION__(${codeFrame})`;
}
// 处理更多 AST 节点类型...
}
代码生成器具有一些特殊的处理:
- 收集表达式中使用的变量名,用于后续的依赖追踪
- 使用特殊函数 EXP 包装属性访问,实现安全的属性访问
- 使用特殊函数 EXPRESSION 包装整个表达式
- 对方法调用添加可选链操作符 ?.,避免空对象调用方法时的错误
3. 核心技术特点
3.1 安全的属性访问
expression-compiler 通过将属性访问表达式转换为特殊函数调用,实现了安全的属性访问,避免了常见的 "Cannot read property of undefined" 错误:
javascript
// 原始表达式
{{ user.profile }}
// 编译后
**EXPRESSION**(**EXP**("user", "profile"))
在运行时,EXP 函数会检查每一层属性是否存在,避免访问 undefined 的属性。
3.2 方法调用安全
对于方法调用,编译器添加了可选链操作符 ?.,确保在方法不存在时不会抛出错误:
javascript
// 原始表达式
{{ user.getFullName() }}
// 编译后
**EXPRESSION**(**EXP**("user", "getFullName")?.())
3.3 表达式变量收集
编译器会收集表达式中使用的所有变量名,用于后续的依赖追踪和响应式更新:
javascript
// 收集变量名
exps.add(JSON.parse(codeFrame[0]));
// 返回结果
return {
expression,
exps: Array.from(exps).filter((n) => !["i18n", "I18n"].includes(n)),
};
这使得框架可以知道哪些数据变化会影响当前表达式,从而精确地触发更新。
3.4 错误处理和语法验证
编译器通过尝试创建函数来验证生成的 JavaScript 代码是否有语法错误:
javascript
const code =
"function **EXPRESSION**(v){}; function **EXP**(v){}; function **ARG**(v){}; return " +
expression;
try {
new Function("ctx", code);
} catch (e) {
throw new SyntaxError(`Invalid expression: '${input}'`);
}
这确保了只有语法正确的表达式才会被编译通过。
4. 使用示例
4.1 简单的数据绑定
javascript
// 模板中的表达式
{{ user }}
// 编译后的代码
**EXPRESSION**(**EXP**("user"))
// 收集的变量
["user"]
4.2 复杂表达式
javascript
// 模板中的表达式
{{ user.age > 18 ? '成年' : '未成年' }}
// 编译后的代码
**EXPRESSION**(**EXP**("user", "age") > 18 ? "成年" : "未成年")
// 收集的变量
["user"]
4.3 方法调用
javascript
// 模板中的表达式
{{ user.getName(firstName, lastName) }}
// 编译后的代码
**EXPRESSION**(**EXP**("user", "getName")?.(firstName, lastName))
// 收集的变量
["user", "firstName", "lastName"]
5. 总结
expression-compiler 是小程序框架中关键的表达式处理组件,它通过词法分析、语法分析和代码生成三个阶段,将模板中的表达式转换为可安全执行的 JavaScript 代码。其主要特点包括:
- 完整的编译流程:包含词法分析、语法分析和代码生成三个阶段
- 安全的属性访问:避免访问 undefined 的属性时报错
- 变量依赖收集:收集表达式中使用的变量,用于响应式更新
- 丰富的表达式支持:支持对象、数组、方法调用等复杂表达式
- 语法错误检测:在编译阶段就能发现表达式的语法错误