目录

---------

parse

parse.js 包含 ast 语法树生成的函数

ast 语法树用于解析 vue 中 template 的模板,以便后续虚拟 DOM 及 DIFF 算法做准备

例如有以下模板:

html
PlainText
<div>
  <p>hello</p>
  <p>{{msg}}</p>
</div>

经过 parse 转换后的语法树:

为了更清晰,删掉了 typetagType 属性

JSON
{
  "children": [
    {
      "tag": "div",
      "children": [
        { "content": "\n    " },
        {
          "tag": "p",
          "children": [{ "content": "hello" }]
        },
        { "content": "\n    " },
        {
          "tag": "p",
          "children": [{  "content": { "content": "msg"} }]
        },
        { "content": "\n  " }
      ]
    }
  ],
  "helpers": []
}

ast 生成函数

  1. 一些基础的函数

基础转换函数

虽然 baseParse 代码很少,但却是解析 ast 的入口函数

Typscript
export function baseParse(content: string) {
  const context = createParserContext(content)
  return createRoot(parseChildren(context, []))
}

上面的 baseParse 用到了很多函数,这里逐个讲解:

  1. isEnd 函数

isEnd 函数用于检测标签的节点,例如当我们遇到了结束标签,首先需要看看之前的开始标签,如果是相同标签,那么就应该结束,例如 <div><span></div>,这种情况就应该报错

这里的操作大多是因为:vue 通过不断截取 template 中的字符串,即截取 context.source

Tip:这里只允许存在单个根标签,和 vue3 真实情况不同

Typscript
function isEnd(context: any, ancestors) {
  const s = context.source
  // 遇到了结束标签
  if(context.source.startsWith('</')) {
    for(let i = ancestors.length - 1; i >= 0; -- i) {
      // ancestors 会存储当前位置的标签名
      if(startsWithEndTagtOpen(s, ancestors[i].tag)) {
        return true
      }
    }
  }
  return !context.source
}
  1. advanceBy 函数

上面的结束中,我们知道了 vue3 通过截取字符串生成 ast 语法树,advanceBy 函数就是用来处理字符串究竟要步进多少

Typscript
function advanceBy(context, numberOfCharacters) {
  context.source = context.source.slice(numberOfCharacters)
}

例如,但我们遇到 <div> 标签,只需要将原字符串移动 tag.length + 2 个单位即可

  1. parseChildren 函数

通过循环转换为 ast 语法树:

parseInterpolationparseTagparseElement 都会在内部处理 context.source 的内容,具体是根据自身函数的功能,去截取字符串,例如 paseInterpolation 会步进 '{{'.length = 2 的步数

Typscript
function parseChildren(context, ancestors) {
  const nodes: any = []
  
  while(!isEnd(context, ancestors)) { // isEnd 函数定义在上方,用于判断是否遇到了结束标签
    let node
    const s = context.source
    // 第 1 次判断
    if(startsWith(s, "{{")) {
      // 1. 开头是 {{,表明这是一个插值语法,需要解析它
      node = parseInterpolation(context)
    } else if(s[0] === '<') {
      if(s[1] === '/') {
        if(/[a-z]/i.test(s[2])) {
          // 匹配 </div>
          // 需要改变 context.source 的值,也就是之前所说的逐步移动指针,截取字符串
          parseTag(context, TagType.End)
          // 结束标签就以为已经处理完了,可以跳出本次循环
          continue
        }
      } else if(/[a-z]/i.test(s[1])) {
        node = parseElement(context, ancestors)
      }
    }
    if(!node) {
      node = parseText(context)
    }
    nodes.push(node)
  }
  return nodes
}
  1. 解析函数

这些函数都是为了返回对应的格式数据,例如文本数据,插值数据,标签数据等

理解 parseElement 和 parseChildren 之间的调用

parseElement 和 parseChildren 函数相互调用非常巧妙,也非常难理解,这里笔者做了一个简单的示意图,帮助理解

ancestors 变量的作用

mini-vue 中,ancestors 主要在两个函数用到了:parseElementisEnd

它的作用是,判断标签前后是否一致,暂不考虑自闭合的情况