诗号:六道同坠,魔劫万千,引渡如来。

TODO ShapeFlags 的溯源和用途?

涉及模块: runtime-core

标签(组件)种类(element, component, slot, template)

标签解析时的 TagType 检测

  1. element,原生标签类型,默认值(如: div ,结合 options.isNativeTag())

  2. component 类型

    • !options.isNativeTag() 类型

    • v-is 指令的

    • core component 类型的([Teleport, Suspense, KeepAlive BaseTransition])

    • options.isBuiltInComponent() 指定的类型

    • 大写字母开头的标签(如: <Comp></Comp>)

    • 标签名直接是 component 的(<component></component>)

  3. slot 类型

  4. template 类型

这些类型的定义和解析均在 parseTag(context, type, parent) 函数中完成

源码:

 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

  function parseTag(
      context: ParserContext,
      type: TagType,
      parent: ElementNode | undefined
  ): ElementNode {

      // ...省略,这里我们之关系 tagType

      let tagType = ElementTypes.ELEMENT
      const options = context.options
      if (!context.inVPre && !options.isCustomElement(tag)) {

          const hasVIs = props.some(
              p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
          )
          if (options.isNativeTag && !hasVIs) {
              // 1. 如果非原生(isNativeTag 范畴内的),视为组件类型,优先级最高
              if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
          } else if (
              // 2. 有 v-is 指令的直接视为组件类型
              hasVIs ||
                  // 3. vue 内置的核心组件<Teleport, Suspense, KeepAlive BaseTransition>
                  isCoreComponent(tag) ||
                  // 4. 内置组件,由开发者定义的内置类型?
                  (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
                  // 5. 标签名以大写字母开头的视为 组件类型
                  /^[A-Z]/.test(tag) ||
                  // 6. 标签名直接是 component 的
                  tag === 'component'
          ) {
              tagType = ElementTypes.COMPONENT
          }

          if (tag === 'slot') {
              tagType = ElementTypes.SLOT
          } else if (
              tag === 'template' &&
                  props.some(p => {
                      return (
                          p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
                      )
                  })
          ) {
              tagType = ElementTypes.TEMPLATE
          }
      }

      return {
          type: NodeTypes.ELEMENT,
          ns,
          tag,
          tagType,
          props,
          isSelfClosing,
          children: [],
          loc: getSelection(context, start),
          codegenNode: undefined // to be created during transform phase
      }
  }

指令解析过程

parseChildren(context, mode, ancestors) -> parseElement(context, mode) -> 解析出整个 element parseTag(context, type, parent) -> 解析出标签 parseAttributes(context, type) -> 解析所有属性 parseAttribute(context, nameSet) -> 解析单个属性,结果返回到 props 中

解析的时候会根据映射关系,将缩写转换成名称。

如:

abbrevname
:bind
@on
#slot

处理代码:

 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
  // function: parseAttribute(...)
  // v-dir 或 缩写
  if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
    // ?: 非捕获组
    // 1. (?:^v-([a-z0-9]+))? -> 匹配 v-dir 指令,非贪婪匹配,捕获指令名
    //   称([a-z0=9]+)
    // 2. (?:(?::|^@|^#)([^\.]+))? -> 匹配 :,@,#
    // 3. (.+)?$ 匹配任意字符
    const match = /(?:^v-([a-z0-9]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
      name
    )

    let arg

    // ([a-z0-9]+), ([^\.]+)
    if (match[2]) {
      const startOffset = name.indexOf(match[2])
      const loc = getSelection(
        context,
        getNewPosition(context, start, startOffset),
        getNewPosition(context, start, startOffset + match[2].length)
      )

      let content = match[2]
      let isStatic = true // 静态属性名

      // 动态属性名解析
      if (content.startsWith('[')) {
        isStatic = false

        if (!content.endsWith(']')) {
          // 如果是动态属性名,必须是 [varName] 形式
          emitError(
            context,
            ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
          )
        }

        content = content.substr(1, content.length - 2)
      }

      arg = {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content,
        isStatic,
        isConstant: isStatic,
        loc
      }
    }

    // 属性是否被引号包起来
    if (value && value.isQuoted) {
      const valueLoc = value.loc
      valueLoc.start.offset++
      valueLoc.start.column++
      valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
      // 取引号内的所有内容
      valueLoc.source = valueLoc.source.slice(1, -1)
    }

    return {
      type: NodeTypes.DIRECTIVE,
      // : -> v-bind, @ -> v-on, # -> v-slot 的缩写
      name:
      match[1] ||
        (name.startsWith(':') ? 'bind' : name.startsWith('@') ? 'on' : 'slot'),
      exp: value && {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: value.content,
        isStatic: false,
        isConstant: false,
        loc: value.loc
      },
      arg,
      // 修饰符处理, v-bind.m1.m2 -> .m1.m2 -> ['m1', 'm2']
      modifiers: match[3] ? match[3].substr[1].split('.') : [],
      loc
    }
  }

属性解析的顺序是,先解析属性值,然后解析指令名称(name),参数(arg),修饰符(modifiers)。

这里有完整的解析流程图,可以清晰完整的知道属性,指令解析整个过程。

RCDATA/CDATA 类型解析

示例:

 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
  const ast = baseParse(code, {
    getNamespace: (tag, parent) => {
      const ns = parent ? parent.ns : Namespaces.HTML;
      if (ns === Namespaces.HTML) {
        // 在 parseChildren while 中将进入 
        // if (ns !== Namespaces.HTML) {
        //    node = parseCDATA(context, ancestors);
        //  }
        if (tag === "svg") {
          return Namespaces.HTML + 1;
        }
      }
      return ns;
    },
    getTextMode: ({ tag }) => {
      if (tag === "textarea") {
      // RCDATA 标签内的内容会直接进入 parsText 当做文本解析 
        return TextModes.RCDATA;
      }
      if (tag === "script") {
        return TextModes.RAWTEXT;
      }
      return TextModes.DATA;
    },
    ...options,
    onError: spy,
  });

这两种类型数据的解析关键有几点(详情请移步 🛬🛬🛬 ):

  1. 重写 getTextMode 在里面对有需要的 tag 类型指定其是什么 mode

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
      function parseElement(...) {
        // ...
    
        const mode = context.options.getTextMode(element, parent);
        // RCDATA 模式,它的内容都会被当做文本来处理
        // 如:<textarea></div></textarea> 中的 `</div>` 只是个文本内容
        const children = parseChildren(context, mode, ancestors);
    
        // ...
      }
    
  2. 重写 getNamespace 告知 parseChildren 走哪个分支

    1
    2
    3
    4
    5
    6
    7
    8
    
      else if (s.startsWith("<![CDATA[")) {
        if (ns !== Namespaces.HTML) {
          node = parseCDATA(context, ancestors);
        } else {
          emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT);
          node = parseBogusComment(context);
        }
      }
    

一个较完整的 AST 结构:

  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
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
  {
    "type":0, // root 节点
    "children":[ // 节点的子组件列表
      {
        "type":1, // 标签 div
        "ns":0, // html
        "tag":"div", // 标签名
        "tagType":0, // 标签类型:start-0, end-1
        "props":[ // 标签的属性列表,如: v-bind:keyup.prevent.enter
          { // 属性有几个重要的属性:
            // 1. name, 指令名称,v- 及缩写(#, @, :) 会转换成属性名称,如:bind
            // 2. exp 表达式即=号后边的值,
            // 3. arg 参数名,绑定的变量名,可能是动态的
            // 4. 修饰符,modifiers

            "type":7,
            "name":"bind",
            "exp":{
              "type":4,
              "content":"ok", // 表达式内容,
              "isStatic":false,
              "isConstant":false,
              "loc":{
                "start":{
                  "column":34,
                  "line":1,
                  "offset":33
                },
                "end":{
                  "column":36,
                  "line":1,
                  "offset":35
                },
                "source":"ok"
              }
            },
            "arg":{ // 参数,绑定的事件或变量
              "type":4,
              "content":"keyup",
              "isStatic":true, // 支持 v-bind:[varname] 动态属性
              "isConstant":true,
              "loc":{
                "start":{
                  "column":13,
                  "line":1,
                  "offset":12
                },
                "end":{
                  "column":18,
                  "line":1,
                  "offset":17
                },
                "source":"keyup"
              }
            },
            "modifiers":[
              "prevent",
              "enter"
            ],
            "loc":{
              "start":{
                "column":6,
                "line":1,
                "offset":5
              },
              "end":{
                "column":37,
                "line":1,
                "offset":36
              },
              "source":"v-bind:keyup.prevent.enter="ok""
            }
          }
        ],
        "isSelfClosing":false,
        "children":[
          // 如果 <div>...</div> 还有内容这里会递归解析出子节点 ast
        ],
        "loc":{
          "start":{
            "column":1,
            "line":1,
            "offset":0
          },
          "end":{
            "column":44,
            "line":1,
            "offset":43
          },
          "source":"<div v-bind:keyup.prevent.enter="ok"></div>"
        }
      }
    ],
    "loc":{
      "start":{
        "column":1,
        "line":1,
        "offset":0
      },
      "end":{
        "column":44,
        "line":1,
        "offset":43
      },
      "source":"<div v-bind:keyup.prevent.enter="ok"></div>"
    },
    "helpers":[

    ],
    "components":[

    ],
    "directives":[

    ],
    "hoists":[

    ],
    "imports":[

    ],
    "cached":0,
    "temps":0
  }

辅助代码

这章主要是一些辅助代码。

parseUrl(url)

parseUrl 实现模拟:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { parse: uriParse } = require('url')
function parseUrl(url) {
  const firstChar = url.charAt(0)
  if (firstChar === '~') {
    const secondChar = url.charAt(1)
    url = url.slice(secondChar === '/' ? 2 : 1)
  }
  return parseUriParts(url)
}

function parseUriParts(urlString) {
  // A TypeError is thrown if urlString is not a string
  // @see https://nodejs.org/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost
  return uriParse(typeof urlString === 'string' ? urlString : '', false, true)
}

console.log('1. ~/ccc/tmp -> ', parseUrl('~/ccc/tmp'))
console.log('2. ~ccc/tmp/test.png -> ', parseUrl('~ccc/tmp/test.png'))
console.log('3. /ccc/tmp ->', parseUrl('/ccc/tmp'))
console.log('4. @ccc/tmp ->', parseUrl('@ccc/tmp'))
console.log('5. ~@svg/file.svg#fragment -> ', parseUrl('~@svg/file.svg#fragment'))
console.log('6. https://www.cheng92.com ->', parseUrl('https://www.cheng92.com'))
1. ~/ccc/tmp ->  Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: null,
  query: null,
  pathname: 'ccc/tmp',
  path: 'ccc/tmp',
  href: 'ccc/tmp'
}
2. ~ccc/tmp/test.png ->  Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: null,
  query: null,
  pathname: 'ccc/tmp/test.png',
  path: 'ccc/tmp/test.png',
  href: 'ccc/tmp/test.png'
}
3. /ccc/tmp -> Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: null,
  query: null,
  pathname: '/ccc/tmp',
  path: '/ccc/tmp',
  href: '/ccc/tmp'
}
4. @ccc/tmp -> Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: null,
  query: null,
  pathname: '@ccc/tmp',
  path: '@ccc/tmp',
  href: '@ccc/tmp'
}
5. ~@svg/file.svg#fragment ->  Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: '#fragment',
  search: null,
  query: null,
  pathname: '@svg/file.svg',
  path: '@svg/file.svg',
  href: '@svg/file.svg#fragment'
}
6. https://www.cheng92.com -> Url {
  protocol: 'https:',
  slashes: true,
  auth: null,
  host: 'www.cheng92.com',
  port: null,
  hostname: 'www.cheng92.com',
  hash: null,
  search: null,
  query: null,
  pathname: '/',
  path: '/',
  href: 'https://www.cheng92.com/'
}
undefined