第二章:标签与文本节点解析函数组详解

🧩 函数 1:onText(content, start, end)

arduino 复制代码
function onText(content: string, start: number, end: number) {
  if (__BROWSER__) {
    const tag = stack[0] && stack[0].tag
    if (tag !== 'script' && tag !== 'style' && content.includes('&')) {
      content = currentOptions.decodeEntities!(content, false)
    }
  }
  const parent = stack[0] || currentRoot
  const lastNode = parent.children[parent.children.length - 1]
  if (lastNode && lastNode.type === NodeTypes.TEXT) {
    // merge
    lastNode.content += content
    setLocEnd(lastNode.loc, end)
  } else {
    parent.children.push({
      type: NodeTypes.TEXT,
      content,
      loc: getLoc(start, end),
    })
  }
}

📖 功能说明

当模板中出现普通文本或插值内容时(如 "Hello"{{ msg }}),由 tokenizer 触发 ontext 事件,这个函数就会被调用,用于创建或合并文本节点


🔍 拆解步骤

步骤 描述
如果运行在浏览器端,并且内容中包含 HTML 实体(如 <),则调用 decodeEntities 进行解码。
找出当前节点的父级(stack[0] 表示当前打开的标签节点,否则为根节点)。
检查上一个子节点是否也是文本节点,如果是则合并文本内容。
否则新建一个 TEXT 类型节点,推入 children 数组。

🧠 原理

Vue 编译器会尽量合并相邻文本节点,以减少虚拟 DOM 节点数量。例如:

css 复制代码
<div>hello {{ name }}</div>

将被解析为单一文本节点:

scss 复制代码
TEXT: "hello " + INTERPOLATION("name")

📌 小结

✅ 解码 HTML 实体

✅ 合并连续文本

✅ 保留精确位置(getLoc()


🧩 函数 2:endOpenTag(end)

scss 复制代码
function endOpenTag(end: number) {
  if (tokenizer.inSFCRoot) {
    currentOpenTag!.innerLoc = getLoc(end + 1, end + 1)
  }
  addNode(currentOpenTag!)
  const { tag, ns } = currentOpenTag!
  if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
    inPre++
  }
  if (currentOptions.isVoidTag(tag)) {
    onCloseTag(currentOpenTag!, end)
  } else {
    stack.unshift(currentOpenTag!)
    if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) {
      tokenizer.inXML = true
    }
  }
  currentOpenTag = null
}

📖 功能说明

用于处理"开始标签结束"的逻辑(如解析到 >/>)。

当一个标签如 <div> 解析完属性部分时,endOpenTag() 会被触发。


🔍 拆解步骤

步骤 描述
若当前处于单文件组件 (SFC) 模式,则记录模板内层位置 (innerLoc)。
调用 addNode() 将当前标签节点插入 AST。
检查是否是 <pre> 标签,若是则增加 inPre 层级(保持空格)。
若标签是自闭合(<br/>),立即关闭该节点。
否则将节点入栈 stack,表示成为当前上下文。
若命名空间为 SVG 或 MathML,则进入 XML 模式。
清空 currentOpenTag,准备下一个节点。

💡 原理

Vue 使用栈(stack)来维护嵌套标签:

css 复制代码
<div>
  <span></span>
</div>

解析 <div> 时入栈,解析到 </div> 时出栈。

每个节点的层级关系由 stack 决定。


🧩 函数 3:onCloseTag(el, end, isImplied = false)

ini 复制代码
function onCloseTag(el: ElementNode, end: number, isImplied = false) {
  if (isImplied) {
    setLocEnd(el.loc, backTrack(end, CharCodes.Lt))
  } else {
    setLocEnd(el.loc, lookAhead(end, CharCodes.Gt) + 1)
  }
  ...
  const { tag, ns, children } = el
  if (!inVPre) {
    if (tag === 'slot') el.tagType = ElementTypes.SLOT
    else if (isFragmentTemplate(el)) el.tagType = ElementTypes.TEMPLATE
    else if (isComponent(el)) el.tagType = ElementTypes.COMPONENT
  }
  ...
  if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
    inPre--
  }
  if (currentVPreBoundary === el) {
    inVPre = tokenizer.inVPre = false
    currentVPreBoundary = null
  }
}

📖 功能说明

负责闭合标签的收尾工作

在模板解析到 </div></template> 等标签结束时被调用。


🔍 逻辑拆解

步骤 描述
调整节点位置 (setLocEnd),区分隐式闭合或正常闭合。
若为 SFC 模式,修正 innerLoc 的结束坐标。
根据标签内容推断节点类型(普通元素 / 模板 / 组件 / 插槽)。
压缩子节点空白符(调用 condenseWhitespace)。
处理 <pre> 模式与换行忽略规则。
若退出 v-pre 指令范围,则重置解析标志。
若命名空间从 SVG 返回到 HTML,则关闭 XML 模式。
在兼容模式下(Vue 2.x),检查 v-ifv-fortemplate 的语法兼容性。

🧠 原理讲解

Vue 解析器的闭合逻辑非常严谨,它不仅仅做结构配对,还会:

  • 根据标签与属性自动推断节点语义类型
  • 检查标签未闭合、错误嵌套
  • 支持v-pre、v-if、v-for 等特殊行为恢复

🧩 函数 4:addNode(node)

scss 复制代码
function addNode(node: TemplateChildNode) {
  (stack[0] || currentRoot).children.push(node)
}

📖 功能说明

将一个新节点(文本 / 注释 / 元素)追加到当前父节点的 children 中。


💡 原理

  • 如果栈中有元素 → 当前节点是栈顶元素的子节点;
  • 如果栈为空 → 当前节点属于根节点。

这就是 Vue AST 树的层级生成方式。


🧩 函数 5:lookAhead(index, c) & backTrack(index, c)

css 复制代码
function lookAhead(index: number, c: number) {
  let i = index
  while (currentInput.charCodeAt(i) !== c && i < currentInput.length - 1) i++
  return i
}

function backTrack(index: number, c: number) {
  let i = index
  while (currentInput.charCodeAt(i) !== c && i >= 0) i--
  return i
}

📖 功能说明

字符级查找工具函数,用于从当前位置前后扫描指定字符。


📍 典型用途

  • lookAhead: 查找闭合符号,如 >
  • backTrack: 向后查找 < 起始位置(用于隐式闭合修正)。

💡 原理

基于 charCodeAt() 实现高性能字符扫描,避免正则带来的性能开销。


🧩 函数 6:condenseWhitespace(nodes)

ini 复制代码
function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
  const shouldCondense = currentOptions.whitespace !== 'preserve'
  ...
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (node.type === NodeTypes.TEXT) {
      if (!inPre) {
        if (isAllWhitespace(node.content)) {
          ...
        } else if (shouldCondense) {
          node.content = condense(node.content)
        }
      } else {
        node.content = node.content.replace(windowsNewlineRE, '\n')
      }
    }
  }
  return removedWhitespace ? nodes.filter(Boolean) : nodes
}

📖 功能说明

用于规范化或删除节点间多余的空白符。


🔍 逻辑拆解

情况 处理方式
<pre> 保留原样,只统一换行符格式
含连续空格 压缩为单一空格 ' '
全是空白且无必要 删除该节点
模式为 'preserve' 完全保留原样

🧠 设计理念

Vue 模板默认不保留无意义的空白符,提升渲染性能。

<pre> 等特殊标签需要忠实还原源文本。


🧩 函数 7:isAllWhitespace(str) & hasNewlineChar(str)

php 复制代码
function isAllWhitespace(str: string) { ... }
function hasNewlineChar(str: string) { ... }

📖 功能说明

辅助判断函数,用于空白符检测与换行判断。


💡 应用

condenseWhitespaceonCloseTag 等处控制换行与文本清理逻辑。


🧩 函数 8:condense(str)

ini 复制代码
function condense(str: string) {
  let ret = ''
  let prevCharIsWhitespace = false
  for (let i = 0; i < str.length; i++) {
    if (isWhitespace(str.charCodeAt(i))) {
      if (!prevCharIsWhitespace) {
        ret += ' '
        prevCharIsWhitespace = true
      }
    } else {
      ret += str[i]
      prevCharIsWhitespace = false
    }
  }
  return ret
}

📖 功能说明

压缩字符串内的连续空格,使 "a b c""a b c"


💡 设计动机

保证 AST 文本节点内容简洁一致,避免模板渲染时产生额外空白。


✅ 小结

至此,我们已讲解完:

  • 节点创建与闭合(onText、onCloseTag、endOpenTag)
  • 节点插入(addNode)
  • 空白处理与辅助扫描函数

这些函数是 baseParse 的"结构层"核心,负责构建出 Vue 的 AST 形状。


📘 下一章预告

第三章我们将进入更复杂的解析逻辑:

🔹 指令解析(ondirnameondirargondirmodifier

🔹 属性与值解析(onattribdataonattribend

🔹 插值解析(oninterpolation

这些函数涉及 Vue 模板语法的灵魂部分(v-if、v-for、v-bind、{{ }} 等)。

相关推荐
excel5 小时前
Vue 3 编译器源码深度解析:codegen.ts 模块详解
前端
一个假的前端男5 小时前
uniapp vue2 三端瀑布流
前端·javascript·uni-app
excel5 小时前
Vue 编译器中 walkIdentifiers 源码深度解析
前端
excel5 小时前
一文看懂 Vue 编译器里的插槽处理逻辑(buildSlots.ts)
前端
excel5 小时前
Vue 编译器源码解析:错误系统(errors.ts)
前端
余道各努力,千里自同风5 小时前
uni-app 请求封装
前端·uni-app
excel5 小时前
Vue 编译器核心 AST 类型系统与节点工厂函数详解
前端
excel5 小时前
Vue 编译器核心:baseCompile 源码深度解析
前端
excel5 小时前
Vue 编译核心:transformMemo 源码深度解析
前端