Vue3核心源码解析第九课 模板解析

发布时间:2022-08-19 11:58

12 模板解析:构造 AST 的完整流程是怎样的?(上)

Vue.js 3.0 的编译场景分服务端 SSR 编译web 编译,本文我们只分析 web 的编译。

我们先来看 web 编译的入口 compile 函数,分析它的实现原理:

function compile(template, options = {}) { 
  return baseCompile(template, extend({}, parserOptions, options, { 
    nodeTransforms: [...DOMNodeTransforms, ...(options.nodeTransforms || [])], 
    directiveTransforms: extend({}, DOMDirectiveTransforms, options.directiveTransforms || {}), 
    transformHoist:  null 
  })) 
} 

compile 函数支持两个参数,第一个参数 template 是待编译的模板字符串,第二个参数 options 是编译的一些配置信息。

compile 内部通过执行 baseCompile 方法完成编译工作,可以看到 baseCompile 在参数 options 的基础上又扩展了一些配置。对于这些编译相关的配置,我们后面会在具体的场景具体分析。

接下来,我们来看一下 baseCompile 的实现:

function baseCompile(template,  options = {}) { 
  const prefixIdentifiers = false 
  // 解析 template 生成 AST 
  const ast = isString(template) ? baseParse(template, options) : template 
  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset() 
  // AST 转换 
  transform(ast, extend({}, options, { 
    prefixIdentifiers, 
    nodeTransforms: [ 
      ...nodeTransforms, 
      ...(options.nodeTransforms || []) 
    ], 
    directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} 
    ) 
  })) 
  // 生成代码 
  return generate(ast, extend({}, options, { 
    prefixIdentifiers 
  })) 
}     

可以看到,baseCompile 函数主要做三件事情:解析 template 生成 ASTAST 转换生成代码

这一节课我们的目标就是解析 template 生成 AST 背后的实现原理

生成 AST 抽象语法树

你可以在百度百科中看到 AST 的定义,这里我就不赘述啦,对应到我们的 template,也可以用 AST 去描述它,比如我们有如下 template:

class="app"> <hello> <p>{{ msg }}p> hello> <p>This is an appp>

它经过第一步解析后,生成相应的 AST 对象:

{ 
  "type": 0, 
  "children": [ 
    { 
      "type": 1, 
      "ns": 0, 
      "tag": "div", 
      "tagType": 0, 
      "props": [ 
        { 
          "type": 6, 
          "name": "class", 
          "value": { 
            "type": 2, 
            "content": "app", 
            "loc": { 
              "start": { 
                "column": 12, 
                "line": 1, 
                "offset": 11 
              }, 
              "end": { 
                "column": 17, 
                "line": 1, 
                "offset": 16 
              }, 
              "source": "\"app\"" 
            } 
          }, 
          "loc": { 
            "start": { 
              "column": 6, 
              "line": 1, 
              "offset": 5 
            }, 
            "end": { 
              "column": 17, 
              "line": 1, 
              "offset": 16 
            }, 
            "source": "class=\"app\"" 
          } 
        } 
      ], 
      "isSelfClosing": false, 
      "children": [ 
        { 
          "type": 3, 
          "content": " 这是一段注释 ", 
          "loc": { 
            "start": { 
              "column": 3, 
              "line": 2, 
              "offset": 20 
            }, 
            "end": { 
              "column": 18, 
              "line": 2, 
              "offset": 35 
            }, 
            "source": "" 
          } 
        }, 
        { 
          "type": 1, 
          "ns": 0, 
          "tag": "hello", 
          "tagType": 1, 
          "props": [], 
          "isSelfClosing": false, 
          "children": [ 
            { 
              "type": 1, 
              "ns": 0, 
              "tag": "p", 
              "tagType": 0, 
              "props": [], 
              "isSelfClosing": false, 
              "children": [ 
                { 
                  "type": 5, 
                  "content": { 
                    "type": 4, 
                    "isStatic": false, 
                    "isConstant": false, 
                    "content": "msg", 
                    "loc": { 
                      "start": { 
                        "column": 11, 
                        "line": 4, 
                        "offset": 56 
                      }, 
                      "end": { 
                        "column": 14, 
                        "line": 4, 
                        "offset": 59 
                      }, 
                      "source": "msg" 
                    } 
                  }, 
                  "loc": { 
                    "start": { 
                      "column": 8, 
                      "line": 4, 
                      "offset": 53 
                    }, 
                    "end": { 
                      "column": 17, 
                      "line": 4, 
                      "offset": 62 
                    }, 
                    "source": "{{ msg }}" 
                  } 
                } 
              ], 
              "loc": { 
                "start": { 
                  "column": 5, 
                  "line": 4, 
                  "offset": 50 
                }, 
                "end": { 
                  "column": 21, 
                  "line": 4, 
                  "offset": 66 
                }, 
                "source": "

{{ msg }}

"
} } ], "loc": { "start": { "column": 3, "line": 3, "offset": 38 }, "end": { "column": 11, "line": 5, "offset": 77 }, "source": "\n

{{ msg }}

\n
"
} }, { "type": 1, "ns": 0, "tag": "p", "tagType": 0, "props": [], "isSelfClosing": false, "children": [ { "type": 2, "content": "This is an app", "loc": { "start": { "column": 6, "line": 6, "offset": 83 }, "end": { "column": 20, "line": 6, "offset": 97 }, "source": "This is an app" } } ], "loc": { "start": { "column": 3, "line": 6, "offset": 80 }, "end": { "column": 24, "line": 6, "offset": 101 }, "source": "

This is an app

"
} } ], "loc": { "start": { "column": 1, "line": 1, "offset": 0 }, "end": { "column": 7, "line": 7, "offset": 108 }, "source": "
\n \n \n

{{ msg }}

\n
\n

This is an app

\n
"
} } ], "helpers": [], "components": [], "directives": [], "hoists": [], "imports": [], "cached": 0, "temps": 0, "loc": { "start": { "column": 1, "line": 1, "offset": 0 }, "end": { "column": 7, "line": 7, "offset": 108 }, "source": "
\n \n \n

{{ msg }}

\n
\n

This is an app

\n
"
} }

可以看到,AST 是树状结构,对于树中的每个节点,会有 type 字段描述节点的类型,tag 字段描述节点的标签,props 描述节点的属性,loc 描述节点对应代码相关信息,children 指向它的子节点对象数组。

当然 AST 中的节点还包含其他的一些属性,我在这里就不一一介绍了,你现在要理解的是 AST 中的节点是可以完整地描述它在模板中映射的节点信息

注意,AST 对象根节点其实是一个虚拟节点它并不会映射到一个具体节点,另外它还包含了其他的一些属性,这些属性在后续的 AST 转换的过程中会赋值,并在生成代码阶段用到。

那么,为什么要设计一个虚拟节点呢?

因为 Vue.js 3.0 和 Vue.js 2.x 有一个很大的不同——Vue.js 3.0 支持了 Fragment 的语法,即组件可以有多个根节点,比如:

"./logo.jpg"> 
<hello :msg="msg">hello> 

这种写法在 Vue.js 2.x 中会报错,提示模板只能有一个根节点,而 Vue.js 3.0 允许了这种写法。但是对于一棵树而言,必须有一个根节点,所以虚拟节点在这种场景下就非常有用了,它可以作为 AST 的根节点,然后其 children 包含了 img 和 hello 的节点。

好了,到这里你已经大致了解了 AST,那么接下来我们看一下如何根据模板字符串来构建这个 AST 对象吧。

先来看一下 baseParse 的实现:

function baseParse(content, options = {}) { 
    // 创建解析上下文 
    const context = createPa  rserContext(content, options) 
    const start = getCursor(context) 
    // 解析子节点,并创建 AST  
    return createRoot(parseChildren(context, 0 /* DATA */, []), getSelection(context, start)) 
} 

baseParse 主要就做三件事情:创建解析上下文解析子节点创建 AST 根节点

创建解析上下文

首先,我们来分析创建解析上下文的过程,先来看 createParserContext 的实现:

// 默认解析配置 
const defaultParserOptions = { 
  delimiters: [`{{`, `}}`], 
  getNamespace: () => 0 /* HTML */, 
  getTextMode: () => 0 /* DATA */, 
  isVoidTag: NO, 
  isPreTag: NO, 
  isCustomElement: NO, 
  decodeEntities: (rawText) => rawText.replace(decodeRE, (_, p1) => decodeMap[p1]), 
  onError: defaultOnError 
} 
function createParserContext(content, options) { 
  return { 
    options: extend({}, defaultParserOptions, options), 
    column: 1, 
    line: 1, 
    offset: 0, 
    originalSource: content, 
    source: content, 
    inPre: false, 
    inVPre: false 
  } 
} 

解析上下文实际上就是一个 JavaScript 对象,它维护着解析过程中的上下文,其中 options 表示解析相关配置 ,column 表示当前代码的列号,line 表示当前代码的行号,originalSource 表示最初的原始代码,source 表示当前代码,offset 表示当前代码相对于原始代码的偏移量,inPre 表示当前代码是否在 pre 标签内,inVPre 表示当前代码是否在 v-pre 指令的环境下。

在后续解析的过程中,会始终维护和更新这个解析上下文,它能够表示当前解析的状态。

创建完解析上下文,接下来就开始解析子节点了。

解析子节点

我们先来看一下 parseChildren 函数的实现:

function parseChildren(context, mode, ancestors) { 
  const parent = last(ancestors) 
  const ns = parent ? parent.ns : 0 /* HTML */ 
  const nodes = [] 

// 自顶向下分析代码,生成 nodes

let removedWhitespace = false
// 空白字符管理

return removedWhitespace ? nodes.filter(Boolean) : nodes
}

parseChildren 的目的就是解析并创建 AST 节点数组。它有两个主要流程,第一个是自顶向下分析代码,生成 AST 节点数组 nodes;第二个是空白字符管理,用于提高编译的效率。

首先,我们来看生成 AST 节点数组的流程:

function parseChildren(context, mode, ancestors) { 
  // 父节点 
  const parent = last(ancestors) 
  const ns = parent ? parent.ns : 0 /* HTML */ 
  const nodes = [] 
  // 判断是否遍历结束 
  while (!isEnd(context, mode, ancestors)) { 
    const s = context.source 
    let node = undefined 
    if (mode === 0 /* DATA */ || mode === 1 /* RCDATA */) { 
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) { 
        // 处理 {{ 插值代码 
        node = parseInterpolation(context, mode) 
      } 
      else if (mode === 0 /* DATA */ && s[0] === '<') { 
        // 处理 < 开头的代码 
        if (s.length === 1) { 
          // s 长度为 1,说明代码结尾是 <,报错 
          emitError(context, 5 /* EOF_BEFORE_TAG_NAME */, 1) 
        } 
        else if (s[1] === '!') { 
          // 处理 
          if (startsWith(s, '