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

🧩 函数 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-if、v-for、template 的语法兼容性。

🧠 原理讲解

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) { ... }

📖 功能说明

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


💡 应用

在 condenseWhitespace、onCloseTag 等处控制换行与文本清理逻辑。


🧩 函数 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 形状。


📘 下一章预告

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

🔹 指令解析(ondirname、ondirarg、ondirmodifier)

🔹 属性与值解析(onattribdata、onattribend)

🔹 插值解析(oninterpolation)

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

相关推荐
原则猫7 小时前
UDP 到底怎么实现可靠的
前端
Java后端的Ai之路7 小时前
14、ReactJS - 路由(Router)
前端·react.js·状态模式
裕晟资质规划8 小时前
三类涉密资质延续时限差异分析:军工60日与集成印制3个月的边界
java·服务器·前端·网络·安全
ShineWinsu8 小时前
对于JavaScript:JavaScript基础语法的解析
开发语言·前端·javascript·面试·web·基础语法·循环
月夏9 小时前
Vue3 转 React:18 项对照表 + 5 个必踩的坑(附 7 天上手路线)
前端
广州华水科技9 小时前
单北斗GNSS位移监测系统的应用与维护解析
前端
梦曦i9 小时前
uni-storage 0.1.0 发布:TTL过期管理+多实例隔离
前端·storage·uni-app x
猫头_10 小时前
AI 流式传输工程指南:模型吐出半截 JSON,前端如何边收边渲染?
前端·javascript·llm
console.log('npc')11 小时前
第 1 章 项目概述与环境搭建
前端·人工智能·大模型
定栓11 小时前
Webpack迁到Vite第七节(二):全家桶移除 — Babel 全家桶深度分析
前端·webpack·node.js