深入 Lyt.js 编译器:.lyt 文件如何增强 HTML 模板能力
基于 @lytjs/compiler 包实际源码,从状态机解析器到三级优化体系,逐层拆解 Lyt.js v6.6.0 编译器的完整实现。
一、编译器全景:6 KB 体积内的完整编译管线
@lytjs/compiler 是 Lyt.js 框架的核心编译包,位于 packages/compiler/ 目录下,构建后的包体积被严格限制在 6 KB 。在如此紧凑的体积内,它实现了从模板字符串到可执行渲染函数的完整编译管线:
模板字符串 → parseHTML() → AST → transform() → optimize() → generate() → 渲染函数代码
整个编译器由以下核心模块组成:
| 文件 | 职责 |
|---|---|
parser/html-parser.ts | 基于状态机的 HTML 解析器 |
ast/nodes.ts | AST 节点类型定义 |
transform/transform.ts | AST 语义转换(指令处理) |
transform/optimize.ts | 静态分析与标记 |
codegen/codegen.ts | 代码生成(AST → h() 调用) |
sfc/ | 单文件组件支持(parse/compile/scopeCSS) |
typescript.ts | TypeScript 类型声明生成 |
wasm-compiler.ts | WASM 模拟层 |
patch-flags.ts | PatchFlag 位标记定义 |
block-tree.ts | Block Tree 优化 |
transform-static-hoist.ts | 静态提升 |
optimize-output.ts | Tree-shaking 友好输出 |
统一入口 compile() 函数串联了四个阶段:
// packages/compiler/src/index.ts
export function compile(template: string, options: CompileOptions = {}): CompileResult {
// 阶段 1:解析 — 将模板字符串解析为 AST
const ast = parseHTML(template);
// 阶段 2:转换 — 对 AST 进行语义转换
transform(ast, options.transform);
// 阶段 3:优化 — 静态分析,标记静态子树
const hoistResult = optimize(ast);
// 阶段 4:代码生成 — 将 AST 转换为渲染函数代码
const codegenResult = generate(ast, options.codegen);
return { code: codegenResult.code, ast, hoistResult, helpers: codegenResult.helpers };
}
二、状态机解析器:6 个状态驱动 HTML 解析
解析器是编译器的第一道关卡。Lyt.js 没有使用正则表达式或第三方 HTML 解析库,而是手写了一个基于状态机的解析器,定义了 6 个解析状态:
// packages/compiler/src/parser/html-parser.ts
const enum ParseState {
TEXT, // 文本内容
TAG_OPEN, // 遇到 '<'
TAG_NAME, // 解析标签名
ATTRIBUTE, // 解析属性
TAG_CLOSE, // 解析闭合标签 '</'
COMMENT, // 解析注释
}
解析器维护一个 ParserContext 上下文对象,包含模板字符串、当前字符位置、行列号、父元素栈、各类缓冲区等状态。主循环根据当前状态分发到对应的处理函数:
export function parseHTML(template: string): RootNode {
const ctx = new ParserContext(template);
while (!ctx.isEOF()) {
switch (ctx.state) {
case ParseState.TEXT: parseText(ctx); break;
case ParseState.TAG_OPEN: parseTagOpen(ctx); break;
case ParseState.TAG_NAME: parseTagName(ctx); break;
case ParseState.ATTRIBUTE: parseAttribute(ctx); break;
case ParseState.TAG_CLOSE: parseTagClose(ctx); break;
case ParseState.COMMENT: parseComment(ctx); break;
}
}
flushTextBuffer(ctx);
return ctx.root;
}
2.1 文本解析与插值处理
在 TEXT 状态下,解析器逐字符读取,遇到 < 切换到标签解析,遇到 {{ 则收集完整的表达式文本:
function parseText(ctx: ParserContext): void {
if (ctx.textBuffer === '') {
ctx.textStart = ctx.pos;
}
while (!ctx.isEOF()) {
const ch = ctx.peek();
if (ch === '<') {
flushTextBuffer(ctx);
ctx.advance();
ctx.state = ParseState.TAG_OPEN;
return;
}
// 遇到 {{ 收集整个表达式
if (ch === '{' && ctx.peekAt(1) === '{') {
ctx.textBuffer += ctx.advance(); // {
ctx.textBuffer += ctx.advance(); // {
while (!ctx.isEOF()) {
const c = ctx.peek();
ctx.textBuffer += ctx.advance();
if (c === '}' && ctx.peek() === '}') {
ctx.textBuffer += ctx.advance();
break;
}
}
continue;
}
ctx.textBuffer += ctx.advance();
}
}
2.2 属性与指令识别
属性解析阶段是模板增强的关键入口。createAttributeOrDirective() 函数根据属性名的格式前缀,智能识别普通属性和各类指令:
function createAttributeOrDirective(rawName: string, rawValue: string | null, loc: Position) {
// v- 前缀指令
if (rawName.startsWith('v-')) {
const rest = rawName.slice(2);
if (rest.startsWith('on')) { /* → 事件指令 */ }
if (rest.startsWith('bind')) { /* → 绑定指令 */ }
if (rest.startsWith('slot')) { /* → 插槽指令 */ }
if (rest === 'model') { /* → 双向绑定 */ }
if (rest === 'ref') { /* → 引用指令 */ }
// v-if, v-each 等通过白名单匹配
if (DIRECTIVE_NAMES.has(directiveName)) { /* → 结构指令 */ }
}
// : 简写(动态属性)
if (rawName.startsWith(':')) { /* → bind 指令 */ }
// @ 简写(事件绑定)
if (rawName.startsWith('@')) { /* → on 指令 */ }
// # 简写(插槽)
if (rawName.startsWith('#')) { /* → slot 指令 */ }
// 普通属性
return createAttributeNode(rawName, rawValue, loc);
}
指令名称通过白名单严格控制:
const DIRECTIVE_NAMES = new Set(['if', 'each', 'bind', 'on', 'slot', 'ref']);
三、AST 节点体系:5 种节点承载全部语义
解析器输出的 AST 由 5 种节点类型组成,通过联合类型 ASTNode 统一管理:
// packages/compiler/src/ast/nodes.ts
export type ASTNode = RootNode | ElementNode | TextNode | AttributeNode | DirectiveNode;
每种节点的核心结构:
RootNode — 根节点,包含顶层子节点和辅助函数集合:
export interface RootNode extends BaseNode {
type: 'Root';
children: (ElementNode | TextNode)[];
helpers: Set<string>; // 编译过程中收集的辅助函数
}
ElementNode — 元素节点,携带标签、属性、子节点、指令和静态标记:
export interface ElementNode extends BaseNode {
type: 'Element';
tag: string;
props: AttributeNode[];
children: (ElementNode | TextNode)[];
isComponent: boolean; // 大写开头自动识别为组件
directives: DirectiveNode[];
staticFlag: number; // -1 未分析 / 0 动态 / 1 静态
isSelfClosing: boolean;
}
TextNode — 文本节点,支持表达式插值检测:
export interface TextNode extends BaseNode {
type: 'Text';
content: string;
isExpression: boolean; // 是否包含 {{ }} 插值
staticFlag: number;
}
DirectiveNode — 指令节点,定义了六大指令的联合类型:
export interface DirectiveNode extends BaseNode {
type: 'Directive';
name: 'if' | 'each' | 'bind' | 'on' | 'slot' | 'ref';
value: string; // 指令值(表达式)
arg: string; // 指令参数
modifiers: string[]; // 修饰符
}
所有节点都携带 Position 位置信息,用于错误定位和 source-map 生成。
四、六大指令:插件化转换架构
转换阶段是编译器的核心。Lyt.js 采用插件化转换架构 ,将六大指令分别实现为独立的转换插件:
// packages/compiler/src/transform/transform.ts
const builtInTransforms: NodeTransform[] = [
transformIfDirective,
transformEachDirective,
transformBindDirective,
transformOnDirective,
transformSlotDirective,
transformRefDirective,
];
转换器支持用户自定义插件,通过 TransformOptions.nodeTransforms 注入,用户插件会在内置插件之前执行。每个转换插件可以返回一个退出回调函数,实现"进入-退出"两阶段处理。
4.1 if — 条件渲染
transformIfDirective 将 v-if 指令转换为条件渲染元数据,在代码生成阶段会输出为三元表达式:
function transformIfDirective(node: ASTNode, context: TransformContext): void {
if (node.type !== 'Element') return;
const ifDirective = node.directives.find(d => d.name === 'if');
if (!ifDirective) return;
Object.assign(node, {
ifCondition: ifDirective.value,
ifBranches: [] as Array<{ condition: string; node: ElementNode }>,
});
context.root.helpers.add('createConditionalVNode');
node.directives = node.directives.filter(d => d !== ifDirective);
}
模板 <div v-if="show">hello</div> 最终生成:
(_ctx.show ? (h('div', null, 'hello')) : null)
4.2 each — 列表渲染
transformEachDirective 使用正则表达式解析迭代语法,支持三种写法:
// 匹配 "(item, index) in/of collection" 或 "item in/of collection"
const match = expr.match(
/^\s*(?:\((\w+)\s*,\s*(\w+)\)|(\w+))\s+(?:in|of)\s+(\S+)\s*$/
);
支持的表达式格式:
<li v-each="item in items">{{ item }}</li>
<li v-each="(item, index) in items">{{ index }}: {{ item }}</li>
<li v-each="item of items">{{ item }}</li>
代码生成阶段输出为 renderList() 调用:
renderList(_ctx.items, (item) => h('li', null, _ctx.item))
4.3 bind — 属性绑定与双向绑定
transformBindDirective 处理动态属性绑定,当 arg 为 'model' 时标记为双向绑定:
function transformBindDirective(node: ASTNode, context: TransformContext): void {
const bindDirectives = node.directives.filter(d => d.name === 'bind');
const bindings: Array<{ arg: string; value: string; isModel: boolean }> = [];
for (const bind of bindDirectives) {
const isModel = bind.arg === 'model';
bindings.push({ arg: bind.arg, value: bind.value, isModel });
if (isModel) {
context.root.helpers.add('createModelBinding');
}
}
Object.assign(node, { bindings });
}
双向绑定在代码生成阶段会输出 model 属性对象:
{ model: { value: _ctx.inputValue, callback: $event => _ctx.inputValue = $event } }
4.4 on — 事件绑定
transformOnDirective 提取事件名、处理函数和修饰符,修饰符在代码生成时转换为对应的 DOM API 调用:
// 修饰符处理
const modifierHandlers = event.modifiers.map(mod => {
switch (mod) {
case 'stop': return '$event.stopPropagation()';
case 'prevent': return '$event.preventDefault()';
case 'capture': return null; // 在 props 中标记
case 'once': return null; // 运行时处理
default: return null;
}
}).filter(Boolean);
支持的事件修饰符:.stop、.prevent、.capture、.once、.self、.passive,以及按键修饰符。
4.5 slot — 插槽系统
transformSlotDirective 处理默认插槽、具名插槽和作用域插槽:
function transformSlotDirective(node: ASTNode, context: TransformContext): void {
const slotDirective = node.directives.find(d => d.name === 'slot');
Object.assign(node, {
slotInfo: {
name: slotDirective.arg || 'default',
value: slotDirective.value,
},
});
context.root.helpers.add('renderSlot');
}
4.6 ref — 模板引用
ref 指令支持静态引用和动态引用两种模式。动态引用通过正则检测表达式中的特殊字符:
// packages/compiler/src/directives/ref.ts
const isDynamic = /\./.test(value) || /\[/.test(value) || /\(/.test(value);
- 静态引用
v-ref="myEl"→ref: 'myEl' - 动态引用
v-ref="form.input"→ref: _ctx.form.input
五、代码生成:CSP 安全的渲染函数
代码生成阶段将优化后的 AST 转换为可执行的渲染函数代码字符串。Lyt.js 的代码生成有几个关键设计决策:
5.1 不使用 with 语句
这是与 Vue 2 最大的区别之一。Vue 2 使用 with 语句让模板直接访问组件实例属性,但这违反了 CSP(Content Security Policy)。Lyt.js 生成的代码通过 _ctx. 前缀显式访问上下文:
// packages/compiler/src/codegen/codegen.ts
function wrapExpression(expr: string): string {
// 已经是 _ctx.xxx 形式
if (expr.startsWith('_ctx.')) return expr;
// 纯标识符 → _ctx.name
if (/^\w+(\.\w+)*$/.test(expr)) return `_ctx.${expr}`;
// 函数调用 → _ctx.fn()
const fnCallMatch = expr.match(/^(\w+(?:\.\w+)*)\s*\(/);
if (fnCallMatch) return `_ctx.${expr}`;
// 箭头函数 → 直接返回
if (expr.includes('=>')) return expr;
// 复杂表达式:替换裸标识符为 _ctx.xxx(排除关键字和全局对象)
// ...
}
wrapExpression 内部维护了三个集合来精确判断哪些标识符需要加前缀:
const JS_KEYWORDS = new Set(['true', 'false', 'null', 'undefined', 'this', 'if', 'for', ...]);
const GLOBALS = new Set(['console', 'window', 'document', 'Math', 'JSON', 'Date', ...]);
const SPECIAL_IDENTS = new Set(['$event', '$refs', '$el', '$emit', '$slots', ...]);
5.2 组件标签不加引号
代码生成时,原生 HTML 标签加引号,组件标签(大写开头)不加引号:
const tag = node.isComponent ? node.tag : `'${node.tag}'`;
ctx.push(`h(${tag}, ...`);
5.3 智能表达式包装
对于包含混合文本和表达式的插值,如 "Hello {{ name }}, age is {{ age }}",生成字符串拼接代码:
'Hello ' + _ctx.name + ', age is ' + _ctx.age
5.4 完整编译示例
输入模板:
<div class="container">
<h1 v-if="showTitle">{{ title }}</h1>
<ul>
<li v-each="item in items">{{ item.name }}</li>
</ul>
<input v-bind:model="inputValue" />
<button @click="handleSubmit">Submit</button>
</div>
输出渲染函数代码:
h('div', { 'class': 'container' }, [
(_ctx.showTitle ? (h('h1', null, _ctx.title)) : null),
h('ul', null, renderList(_ctx.items, (item) => h('li', null, _ctx.item.name))),
h('input', { model: { value: _ctx.inputValue, callback: $event => _ctx.inputValue = $event } }),
h('button', { 'onClick': _ctx.handleSubmit }, 'Submit')
])
运行时使用方式:
const renderFn = new Function('h', '_ctx', 'return ' + code);
const vnode = renderFn(h, proxy);
六、三级编译优化体系
Lyt.js 实现了类似 Vue 3 的三级编译优化,但以更精简的代码实现。
6.1 第一级:静态提升(Static Hoisting)
静态提升分为两个阶段协作完成:
阶段一:标记静态节点 (transform/optimize.ts)
optimize() 函数递归遍历 AST,为每个节点设置 staticFlag:
export function optimize(ast: RootNode): HoistResult {
const result: HoistResult = { hoistedNodes: [], hoistedNames: [] };
let hoistCounter = 0;
for (const child of ast.children) {
markStatic(child, result, () => {
hoistCounter++;
return `_hoisted_${hoistCounter}`;
});
}
return result;
}
静态判定标准非常严格——元素节点必须满足以下全部条件才被标记为静态(staticFlag = 1):
- 没有指令(v-if、v-each 等)
- 没有条件渲染(ifCondition)
- 没有循环渲染(eachInfo)
- 没有动态绑定(bindings)
- 没有事件绑定(events)
- 没有插槽(slotInfo)
- 没有引用(refInfo)
- 所有属性都不是动态的
- 所有子节点也都是静态的
阶段二:执行提升操作 (transform-static-hoist.ts)
analyzeStatic() 在标记完成后执行实际的提升操作,核心策略是收集连续的静态子节点 :
function walkAndHoist(node, hoistedNodes, hoistedNames, nameGenerator) {
// 遍历子节点,寻找连续的静态子节点组
while (i < node.children.length) {
if (isHoistableNode(child)) {
const staticStart = i;
while (i < node.children.length && isHoistableNode(node.children[i])) i++;
const staticCount = i - staticStart;
if (staticCount >= 2) {
// 多个连续静态节点 → 合并为 __static_container__
const container = createStaticContainer(node.children.slice(staticStart, i));
hoistedNodes.push(container);
} else if (staticCount === 1 && singleChild.children.length > 0) {
// 单个复杂静态节点也提升
hoistedNodes.push(singleChild);
}
}
}
}
连续的静态节点会被包装为 __static_container__ 虚拟容器,整体提升到渲染函数外部,只创建一次。
6.2 第二级:Block Tree
Block Tree 的核心思想是:一个 Block 只追踪其内部的动态子节点 ,重新渲染时跳过所有静态子节点。
// packages/compiler/src/block-tree.ts
export function createBlock(tag, props, children, patchFlag): Block {
const vnode: VNode = {
type: tag, tag, props, children, patchFlag,
dynamicChildren: [], // 只收集动态子节点
isBlock: true,
};
return { vnode, dynamicChildren: [] };
}
运行时通过 enterBlock / exitBlock 管理当前 Block 上下文,带有 patchFlag 的 VNode 会被自动收集到当前 Block 的 dynamicChildren 中:
export function trackDynamicChild(vnode: VNode): void {
if (currentBlock && vnode.patchFlag && vnode.patchFlag > 0) {
currentBlock.dynamicChildren.push(vnode);
}
}
6.3 第三级:Patch Flags
Patch Flags 使用位掩码精确标记节点的动态特征,运行时 diff 算法根据标记只更新变化的部分:
// packages/compiler/src/patch-flags.ts
export enum CompilerPatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 2, // 动态 class
STYLE = 4, // 动态 style
PROPS = 8, // 动态 props
FULL_PROPS = 16, // 动态 keys + props
EVENT = 32, // 动态事件
SLOTS = 64, // 动态插槽
STABLE_FRAGMENT = 128, // 稳定片段
KEYED_FRAGMENT = 256, // 带 key 的片段
UNKEYED_FRAGMENT = 512, // 不带 key 的片段
NEED_PATCH = 1024, // 强制补丁
}
七、在 v6.6.0 中的位置
在 Lyt.js v6.6.0 的 8 层架构中,@lytjs/compiler 位于 L1 核心原语层,是整个框架的基础构建块之一。它被以下层级依赖:
L0: 基础工具层
↓
L1: 核心原语层 (@lytjs/compiler, @lytjs/reactivity, @lytjs/vdom)
↓
L2: 渲染引擎层 (@lytjs/renderer, @lytjs/component)
↓
L3: 核心运行时层 (@lytjs/core)
编译器生成的渲染函数会被 renderer 包使用,结合 reactivity 包的响应式系统,实现高效的 DOM 更新。