深入 Lyt.js 编译器:.lyt 文件如何增强 HTML 模板能力

113 阅读7分钟

深入 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.tsAST 节点类型定义
transform/transform.tsAST 语义转换(指令处理)
transform/optimize.ts静态分析与标记
codegen/codegen.ts代码生成(AST → h() 调用)
sfc/单文件组件支持(parse/compile/scopeCSS)
typescript.tsTypeScript 类型声明生成
wasm-compiler.tsWASM 模拟层
patch-flags.tsPatchFlag 位标记定义
block-tree.tsBlock Tree 优化
transform-static-hoist.ts静态提升
optimize-output.tsTree-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 — 条件渲染

transformIfDirectivev-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 更新。