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

🧩 函数 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、{{ }} 等)。

相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万4 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋4 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁4 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95274 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大4 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师4 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学4 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端