• 【Vue.js 3.0源码】模板解析构造 AST 抽象语法树完整流程


    自我介绍:大家好,我是吉帅振的网络日志;微信公众号:吉帅振的网络日志;前端开发工程师,工作4年,去过上海、北京,经历创业公司,进过大厂,现在郑州敲代码。

    一、前言

    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 
      })) 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    compile 函数支持两个参数,第一个参数 template 是待编译的模板字符串,第二个参数 options 是编译的一些配置信息。compile 内部通过执行 baseCompile 方法完成编译工作,可以看到 baseCompile 在参数 options 的基础上又扩展了一些配置。

    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 
      })) 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    二、解析 template 生成 AST

    {{ msg }}

    This is an app

    //解析后,生成相应的 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\"" } } ], }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    可以看到,AST 是树状结构,对于树中的每个节点,会有 type 字段描述节点的类型,tag 字段描述节点的标签,props 描述节点的属性,loc 描述节点对应代码相关信息,children 指向它的子节点对象数组。当然 AST 中的节点还包含其他的一些属性,我在这里就不一一介绍了,你现在要理解的是 AST 中的节点是可以完整地描述它在模板中映射的节点信息。注意,AST 对象根节点其实是一个虚拟节点,它并不会映射到一个具体节点,另外它还包含了其他的一些属性,这些属性在后续的 AST 转换的过程中会赋值,并在生成代码阶段用到。

    那么,为什么要设计一个虚拟节点呢?因为 Vue.js 3.0 和 Vue.js 2.x 有一个很大的不同——Vue.js 3.0 支持了 Fragment 的语法,即组件可以有多个根节点,比如:

     
     
    
    //这种写法在 Vue.js 2.x 中会报错,提示模板只能有一个根节点,
    //而 Vue.js 3.0 允许了这种写法。但是对于一棵树而言,必须有一个根节点,
    //所以虚拟节点在这种场景下就非常有用了,它可以作为 AST 的根节点,
    //然后其 children 包含了 img 和 hello 的节点。 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)) 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.创建解析上下文

    // 默认解析配置 
    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 
      } 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    解析上下文实际上就是一个 JavaScript 对象,它维护着解析过程中的上下文,其中 options 表示解析相关配置 ,column 表示当前代码的列号,line 表示当前代码的行号,originalSource 表示最初的原始代码,source 表示当前代码,offset 表示当前代码相对于原始代码的偏移量,inPre 表示当前代码是否在 pre 标签内,inVPre 表示当前代码是否在 v-pre 指令的环境下。在后续解析的过程中,会始终维护和更新这个解析上下文,它能够表示当前解析的状态。创建完解析上下文,接下来就开始解析子节点了。

    2.解析子节点

    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 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    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] === '!') { 
              // 处理 ') { 
                //  缺少结束标签,报错 
                emitError(context, 14 /* MISSING_END_TAG_NAME */, 2) 
                advanceBy(context, 3) 
                continue 
              } 
              else if (/[a-z]/i.test(s[2])) { 
                // 多余的结束标签 
                emitError(context, 23 /* X_INVALID_END_TAG */) 
                parseTag(context, 1 /* End */, parent) 
                continue 
              } 
              else { 
                emitError(context, 12 /* INVALID_FIRST_CHARACTER_OF_TAG_NAME */, 2) 
                node = parseBogusComment(context) 
              } 
            } 
            else if (/[a-z]/i.test(s[1])) { 
              // 解析标签元素节点 
              node = parseElement(context, ancestors) 
            } 
            else if (s[1] === '?') { 
              emitError(context, 21 /* UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME */, 1) 
              node = parseBogusComment(context) 
            } 
            else { 
              emitError(context, 12 /* INVALID_FIRST_CHARACTER_OF_TAG_NAME */, 1) 
            } 
          } 
        } 
        if (!node) { 
          // 解析普通文本节点 
          node = parseText(context, mode) 
        } 
        if (isArray(node)) { 
          // 如果 node 是数组,则遍历添加 
          for (let i = 0; i < node.length; i++) { 
            pushNode(nodes, node[i]) 
          } 
        } 
        else { 
          // 添加单个 node 
          pushNode(nodes, node) 
        } 
      } 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    这些代码看起来很复杂,但它的思路就是自顶向下地去遍历代码,然后根据不同的情况尝试去解析代码,然后把生成的 node 添加到 AST nodes 数组中。在解析的过程中,解析上下文 context 的状态也是在不断发生变化的,我们可以通过 context.source 拿到当前解析剩余的代码 s,然后根据 s 不同的情况走不同的分支处理逻辑。在解析的过程中,可能会遇到各种错误,都会通过 emitError 方法报错。

    三、创建 AST 根节点

    function createRoot(children, loc = locStub) {
      return {
        type: 0 /* ROOT */,
        children,
        helpers: [],
        components: [],
        directives: [],
        hoists: [],
        imports: [],
        cached: 0,
        temps: 0,
        codegenNode: undefined,
        loc
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    createRoot 的实现非常简单,它就是返回一个 JavaScript 对象,作为 AST 根节点。其中 type 表示它是一个根节点类型,children 是我们前面解析的子节点数组。

    四、总结

    掌握 Vue.js 编译过程的第一步,即把 template 解析生成 AST 对象。整个解析过程是一个自顶向下的分析过程,也就是从代码开始,通过语法分析,找到对应的解析处理逻辑,创建 AST 节点,处理的过程中也在不断前进代码,更新解析上下文,最终根据生成的 AST 节点数组创建 AST 根节点。

  • 相关阅读:
    HarmonyOS/OpenHarmony原生应用-ArkTS万能卡片组件Stack
    googleTest V1.12.1的基础用法
    【Python】高级语法——闭包和装饰器
    Nginx动静分离、缓存配置、性能调优、集群配置
    正则表示式——6.处理比较复杂的正则表示法
    MQ 之 RoketMQ(下载、安装、快速启动、控制台、集群部署)
    WordPress SQLite Docker 镜像封装细节
    【推荐】数字化转型和数据治理资料合集124篇
    【车载开发系列】HexView文件合并
    为什么 ConcurrentHashMap 中 key 不允许为null
  • 原文地址:https://blog.csdn.net/qq_42451979/article/details/125889158