Skip to content
On this page

模板编译 Vue 中对 template 属性会编译成 render 方法。在线模板编译器

增添新的包compiler-core/package.json

{
    "name": "@vue/compiler-core",
    "version": "1.0.0",
    "description": "@vue/compiler-core",
    "main": "index.js",
    "module": "dist/compiler-core.esm-bundler.js",
    "buildOptions": {
        "name": "VueCompilerCore",
        "compat": true,
        "formats": [
            "esm-bundler",
            "cjs"
        ]
    }
}

我们将开发环境下的打包入口改为 compile-core,这里我们先提供所需要 ast 的节点类型

export function compile(template) {
  // 1.将模板转化成ast语法树
  const ast = parse(template);
  // 2.对ast语法树进行转化
  transform(ast);
  // 3.生成代码
  return generate(ast);
}

生成 ast 语法树

准备语法树相关类型ast.ts

export const enum NodeTypes {
    ROOT, // 根节点
    ELEMENT, // 元素
    TEXT, // 文本
    COMMENT, // 注释
    SIMPLE_EXPRESSION, // 简单表达式
    INTERPOLATION, // 模板表达式
    ATTRIBUTE,
    DIRECTIVE,
    // containers
    COMPOUND_EXPRESSION, // 复合表达式
    IF,
    IF_BRANCH,
    FOR,
    TEXT_CALL, // 文本调用
    // codegen
    VNODE_CALL, // 元素调用
    JS_CALL_EXPRESSION, // js调用表达式
}

创建解析上下文

创建解析上下文,并且根据类型做不同的处理解析。 ast 转化

function createParserContext(content) {
  return {
    line: 1,
    column: 1,
    offset: 0,
    source: content, // source会不停的被截取
    originalSource: content, // 原始内容
  };
}
function isEnd(context) {
  const source = context.source;
  return !source;
}
function parseChildren(context) {
  const nodes = [];
  while (!isEnd(context)) {
    const s = context.source;
    let node;
    if (s.startsWith("{{")) {
      // 处理表达式类型
    } else if (s[0] === "<") {
      // 标签的开头
      if (/[a-z]/i.test(s[1])) {
      } // 开始标签
    }
    if (!node) {
      // 文本的处理
    }
    nodes.push(node);
  }
  return nodes;
}
function parse(template) {
  const context = createParserContext(template);
  return parseChildren(context);
}

处理文本节点

采用假设法获取文本结束位置

function parseText(context) {
  // 123123{{name}}</div>
  const endTokens = ["<", "{{"];
  let endIndex = context.source.length; // 文本的总长度
  // 假设遇到 < 就是文本的结尾 。 在假设遇到{{ 是文本结尾。 最后找离的近的
  // 假设法
  for (let i = 0; i < endTokens.length; i++) {
    const index = context.source.indexOf(endTokens[i], 1);
    if (index !== -1 && endIndex > index) {
      endIndex = index;
    }
  }
}

处理文本内容,删除匹配到的结果,计算最新上下文位置信息

function parseText(context) {
  // ...
  let start = getCursor(context); // 1.获取文本开始位置
  const content = parseTextData(context, endIndex); // 2.处理文本数据

  return {
    type: NodeTypes.TEXT,
    content,
    loc: getSelection(context, start), // 3.获取全部信息
  };
}
function getCursor(context) {
  // 获取当前位置
  let { line, column, offset } = context;
  return { line, column, offset };
}
function parseTextData(context, endIndex) {
  const rawText = context.source.slice(0, endIndex);
  advanceBy(context, endIndex); // 截取内容
  return rawText;
}
function advanceBy(context, endIndex) {
  let s = context.source;
  advancePositionWithMutation(context, s, endIndex); // 更改位置信息
  context.source = s.slice(endIndex);
}
function advancePositionWithMutation(context, s, endIndex) {
  // 更新最新上下文信息
  let linesCount = 0; // 计算行数
  let linePos = -1; // 计算其实行开始位置
  for (let i = 0; i < endIndex; i++) {
    if (s.charCodeAt(i) === 10) {
      // 遇到\n就增加一行
      linesCount++;
      linePos = i; // 记录换行后的字节位置
    }
  }
  context.offset += endIndex; // 累加偏移量
  context.line += linesCount; // 累加行数
  // 计算列数,如果无换行,则直接在原列基础 + 文本末尾位置,否则 总位置减去换行后的字节位置
  context.column =
    linePos == -1 ? context.column + endIndex : endIndex - linePos;
}
function getSelection(context, start) {
  const end = getCursor(context);
  return {
    start,
    end,
    source: context.originalSource.slice(start.offset, end.offset),
  };
}

转化成最终ast节点结果,标记ast节点类型

处理表达式节点

获取表达式中的变量,计算表达式的位置信息

function parseInterpolation(context) {
  const start = getCursor(context); // 获取表达式的开头位置
  const closeIndex = context.source.indexOf("}}", 2); // 找到结束位置
  advanceBy(context, 2); // 去掉  {{
  const innerStart = getCursor(context); // 计算里面开始和结束
  const innerEnd = getCursor(context);
  const rawContentLength = closeIndex - 2; // 拿到内容
  const preTrimContent = parseTextData(context, rawContentLength);
  const content = preTrimContent.trim();
  const startOffest = preTrimContent.indexOf(content);
  if (startOffest > 0) {
    // 有空格
    advancePositionWithMutation(innerStart, preTrimContent, startOffest); // 计算表达式开始位置
  }
  const endOffset = content.length + startOffest;
  advancePositionWithMutation(innerEnd, preTrimContent, endOffset);
  advanceBy(context, 2);
  return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      isStatic: false,
      content,
      loc: getSelection(context, innerStart, innerEnd), // 需要修改getSelection方法
    },
    loc: getSelection(context, start),
  };
}

处理元素节点

处理标签

获取标签名称,更新标签位置信息

function advanceSpaces(context) {
  const match = /^[ \t\r\n]+/.exec(context.source);
  if (match) {
    advanceBy(context, match[0].length);
  }
}
function parseTag(context) {
  const start = getCursor(context); // 获取开始位置
  const match = /^<\/?([a-z][^ \t\r\n/>]*)/.exec(context.source); // 匹配标签名
  const tag = match[1];
  advanceBy(context, match[0].length); // 删除标签
  advanceSpaces(context); // 删除空格
  const isSelfClosing = context.source.startsWith("/>"); // 是否是自闭合
  advanceBy(context, isSelfClosing ? 2 : 1); // 删除闭合 /> >
  return {
    type: NodeTypes.ELEMENT,
    tag,
    isSelfClosing,
    loc: getSelection(context, start),
  };
}
function parseElement(context) {
  // 1.解析标签名
  let ele = parseTag(context);
  if (context.source.startsWith("</")) {
    parseTag(context); // 解析标签,标签没有儿子,则直接更新标签信息的结束位置
  }
  ele.loc = getSelection(context, ele.loc.start); // 更新最终位置
  return ele;
}

处理子节点

递归处理子节点元素

function isEnd(context) {
    const source = context.source;
    if(context.source.startsWith('</')){ // 如果遇到结束标签说明没有子节点
        return true;
    }
    return !source;
}
function parseElement(context) {
    let ele = parseTag(context);
    const children = parseChildren(context); // 因为结尾标签, 会再次触发parseElement,这里如果是结尾需要停止
    if(context.source.startsWith('</')){
        parseTag(context);
    }
    ele.loc = getSelection(context,ele.loc.start); // 更新最终位置
    (ele as any).children = children; // 添加children
    return ele;
}

处理属性

在处理标签后处理属性

function parseTag(context) {
  const start = getCursor(context);
  const match = /^<\/?([a-z][^ \t\r\n/>]*)/.exec(context.source);
  const tag = match[1];
  advanceBy(context, match[0].length);
  advanceBySpaces(context);
  let props = parseAttributes(context); // 处理属性
  // ......
  return {
    type: NodeTypes.ELEMENT,
    tag,
    isSelfClosing,
    loc: getSelection(context, start),
    props,
  };
}
function parseAttributes(context) {
    const props: any = [];
    while (context.source.length > 0 && !context.source.startsWith('>')) {
        const attr = parseAttribute(context)
        props.push(attr);
        advanceSpaces(context); // 解析一个去空格一个
    }
    return props
}
function parseAttribute(context) {
    const start = getCursor(context);
    const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
    const name = match[0]; // 捕获到属性名
    advanceBy(context, name.length); // 删除属性名

    let value
    if (/^[\t\r\n\f ]*=/.test(context.source)) { // 删除空格 等号
        advanceSpaces(context);
        advanceBy(context, 1);
        advanceSpaces(context);
        value = parseAttributeValue(context); // 解析属性值
    }
    const loc = getSelection(context, start)
    return {
        type: NodeTypes.ATTRIBUTE,
        name,
        value: {
            type: NodeTypes.TEXT,
            content: value.content,
            loc: value.loc
        },
        loc
    }
}
function parseAttributeValue(context) {
    const start = getCursor(context);
    const quote = context.source[0];
    let content
    const isQuoteed = quote === '"' || quote === "'";
    if (isQuoteed) {
        advanceBy(context, 1);
        const endIndex = context.source.indexOf(quote);
        content = parseTextData(context, endIndex);  // 解析引号中间的值
        advanceBy(context, 1);
    }
    return { content, loc: getSelection(context, start) }
}

处理空节点

function parseChildren(context) {
  const nodes: any = [];
  while (!isEnd(context)) {
    //....
  }
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (node.type == NodeTypes.TEXT) {
      // 如果是文本 删除空白文本,其他的空格变为一个
      if (!/[^\t\r\n\f ]/.test(node.content)) {
        nodes[i] = null;
      } else {
        node.content = node.content.replace(/[\t\r\n\f ]+/g, " ");
      }
    }
  }
  return nodes.filter(Boolean);
}

创建根节点

将解析出的节点,再次进行包裹,这样可以支持模板下多个根节点的情况, 也是我们常说的 Fragment

export function createRoot(children, loc) {
  return {
    type: NodeTypes.ROOT,
    children,
    loc,
  };
}
function parse(template) {
  // 标识节点的信息  行 列 偏移量
  const context = createParserContext(template);
  const start = getCursor(context);
  return createRoot(parseChildren(context), getSelection(context, start));
}

Released under the MIT License.