编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST
这个过程是比较复杂的,它会用到大量正则表达式对字符串解析

例子

  1. <ul :class="bindCls" class="list" v-if="isShow">
  2. <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
  3. </ul>

经过 parse 过程后,生成的 AST 如下

  1. ast = {
  2. 'type': 1,
  3. 'tag': 'ul',
  4. 'attrsList': [],
  5. 'attrsMap': {
  6. ':class': 'bindCls',
  7. 'class': 'list',
  8. 'v-if': 'isShow'
  9. },
  10. 'if': 'isShow',
  11. 'ifConditions': [{
  12. 'exp': 'isShow',
  13. 'block': // ul ast element
  14. }],
  15. 'parent': undefined,
  16. 'plain': false,
  17. 'staticClass': 'list',
  18. 'classBinding': 'bindCls',
  19. 'children': [{
  20. 'type': 1,
  21. 'tag': 'li',
  22. 'attrsList': [{
  23. 'name': '@click',
  24. 'value': 'clickItem(index)'
  25. }],
  26. 'attrsMap': {
  27. '@click': 'clickItem(index)',
  28. 'v-for': '(item,index) in data'
  29. },
  30. 'parent': // ul ast element
  31. 'plain': false,
  32. 'events': {
  33. 'click': {
  34. 'value': 'clickItem(index)'
  35. }
  36. },
  37. 'hasBindings': true,
  38. 'for': 'data',
  39. 'alias': 'item',
  40. 'iterator1': 'index',
  41. 'children': [
  42. 'type': 2,
  43. 'expression': '_s(item)+":"+_s(index)'
  44. 'text': '{{item}}:{{index}}',
  45. 'tokens': [
  46. {'@binding':'item'},
  47. ':',
  48. {'@binding':'index'}
  49. ]
  50. ]
  51. }]
  52. }

生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent 指向它的父节点,children 指向它的所有子节点

整体流程

parse定义在src/compiler/parser/index.js中

  1. /**
  2. * Convert HTML string to AST.
  3. */
  4. export function parse (
  5. template: string,
  6. options: CompilerOptions
  7. ): ASTElement | void {
  8. warn = options.warn || baseWarn
  9. platformIsPreTag = options.isPreTag || no
  10. platformMustUseProp = options.mustUseProp || no
  11. platformGetTagNamespace = options.getTagNamespace || no
  12. const isReservedTag = options.isReservedTag || no
  13. maybeComponent = (el: ASTElement) => !!(
  14. el.component ||
  15. el.attrsMap[':is'] ||
  16. el.attrsMap['v-bind:is'] ||
  17. !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
  18. )
  19. transforms = pluckModuleFunction(options.modules, 'transformNode')
  20. preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  21. postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
  22. delimiters = options.delimiters
  23. const stack = []
  24. const preserveWhitespace = options.preserveWhitespace !== false
  25. const whitespaceOption = options.whitespace
  26. let root
  27. let currentParent
  28. let inVPre = false
  29. let inPre = false
  30. let warned = false
  31. function warnOnce (msg, range) {
  32. if (!warned) {
  33. warned = true
  34. warn(msg, range)
  35. }
  36. }
  37. function closeElement (element) {
  38. trimEndingWhitespace(element)
  39. if (!inVPre && !element.processed) {
  40. element = processElement(element, options)
  41. }
  42. // tree management
  43. if (!stack.length && element !== root) {
  44. // allow root elements with v-if, v-else-if and v-else
  45. if (root.if && (element.elseif || element.else)) {
  46. if (process.env.NODE_ENV !== 'production') {
  47. checkRootConstraints(element)
  48. }
  49. addIfCondition(root, {
  50. exp: element.elseif,
  51. block: element
  52. })
  53. } else if (process.env.NODE_ENV !== 'production') {
  54. warnOnce(
  55. `Component template should contain exactly one root element. ` +
  56. `If you are using v-if on multiple elements, ` +
  57. `use v-else-if to chain them instead.`,
  58. { start: element.start }
  59. )
  60. }
  61. }
  62. if (currentParent && !element.forbidden) {
  63. if (element.elseif || element.else) {
  64. processIfConditions(element, currentParent)
  65. } else {
  66. if (element.slotScope) {
  67. // scoped slot
  68. // keep it in the children list so that v-else(-if) conditions can
  69. // find it as the prev node.
  70. const name = element.slotTarget || '"default"'
  71. ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  72. }
  73. currentParent.children.push(element)
  74. element.parent = currentParent
  75. }
  76. }
  77. // final children cleanup
  78. // filter out scoped slots
  79. element.children = element.children.filter(c => !(c: any).slotScope)
  80. // remove trailing whitespace node again
  81. trimEndingWhitespace(element)
  82. // check pre state
  83. if (element.pre) {
  84. inVPre = false
  85. }
  86. if (platformIsPreTag(element.tag)) {
  87. inPre = false
  88. }
  89. // apply post-transforms
  90. for (let i = 0; i < postTransforms.length; i++) {
  91. postTransforms[i](element, options)
  92. }
  93. }
  94. function trimEndingWhitespace (el) {
  95. // remove trailing whitespace node
  96. if (!inPre) {
  97. let lastNode
  98. while (
  99. (lastNode = el.children[el.children.length - 1]) &&
  100. lastNode.type === 3 &&
  101. lastNode.text === ' '
  102. ) {
  103. el.children.pop()
  104. }
  105. }
  106. }
  107. function checkRootConstraints (el) {
  108. if (el.tag === 'slot' || el.tag === 'template') {
  109. warnOnce(
  110. `Cannot use <${el.tag}> as component root element because it may ` +
  111. 'contain multiple nodes.',
  112. { start: el.start }
  113. )
  114. }
  115. if (el.attrsMap.hasOwnProperty('v-for')) {
  116. warnOnce(
  117. 'Cannot use v-for on stateful component root element because ' +
  118. 'it renders multiple elements.',
  119. el.rawAttrsMap['v-for']
  120. )
  121. }
  122. }
  123. parseHTML(template, {
  124. warn,
  125. expectHTML: options.expectHTML,
  126. isUnaryTag: options.isUnaryTag,
  127. canBeLeftOpenTag: options.canBeLeftOpenTag,
  128. shouldDecodeNewlines: options.shouldDecodeNewlines,
  129. shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
  130. shouldKeepComment: options.comments,
  131. outputSourceRange: options.outputSourceRange,
  132. start (tag, attrs, unary, start, end) {
  133. // check namespace.
  134. // inherit parent ns if there is one
  135. const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
  136. // handle IE svg bug
  137. /* istanbul ignore if */
  138. if (isIE && ns === 'svg') {
  139. attrs = guardIESVGBug(attrs)
  140. }
  141. let element: ASTElement = createASTElement(tag, attrs, currentParent)
  142. if (ns) {
  143. element.ns = ns
  144. }
  145. if (process.env.NODE_ENV !== 'production') {
  146. if (options.outputSourceRange) {
  147. element.start = start
  148. element.end = end
  149. element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
  150. cumulated[attr.name] = attr
  151. return cumulated
  152. }, {})
  153. }
  154. attrs.forEach(attr => {
  155. if (invalidAttributeRE.test(attr.name)) {
  156. warn(
  157. `Invalid dynamic argument expression: attribute names cannot contain ` +
  158. `spaces, quotes, <, >, / or =.`,
  159. {
  160. start: attr.start + attr.name.indexOf(`[`),
  161. end: attr.start + attr.name.length
  162. }
  163. )
  164. }
  165. })
  166. }
  167. if (isForbiddenTag(element) && !isServerRendering()) {
  168. element.forbidden = true
  169. process.env.NODE_ENV !== 'production' && warn(
  170. 'Templates should only be responsible for mapping the state to the ' +
  171. 'UI. Avoid placing tags with side-effects in your templates, such as ' +
  172. `<${tag}>` + ', as they will not be parsed.',
  173. { start: element.start }
  174. )
  175. }
  176. // apply pre-transforms
  177. for (let i = 0; i < preTransforms.length; i++) {
  178. element = preTransforms[i](element, options) || element
  179. }
  180. if (!inVPre) {
  181. processPre(element)
  182. if (element.pre) {
  183. inVPre = true
  184. }
  185. }
  186. if (platformIsPreTag(element.tag)) {
  187. inPre = true
  188. }
  189. if (inVPre) {
  190. processRawAttrs(element)
  191. } else if (!element.processed) {
  192. // structural directives
  193. processFor(element)
  194. processIf(element)
  195. processOnce(element)
  196. }
  197. if (!root) {
  198. root = element
  199. if (process.env.NODE_ENV !== 'production') {
  200. checkRootConstraints(root)
  201. }
  202. }
  203. if (!unary) {
  204. currentParent = element
  205. stack.push(element)
  206. } else {
  207. closeElement(element)
  208. }
  209. },
  210. end (tag, start, end) {
  211. const element = stack[stack.length - 1]
  212. // pop stack
  213. stack.length -= 1
  214. currentParent = stack[stack.length - 1]
  215. if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
  216. element.end = end
  217. }
  218. closeElement(element)
  219. },
  220. chars (text: string, start: number, end: number) {
  221. if (!currentParent) {
  222. if (process.env.NODE_ENV !== 'production') {
  223. if (text === template) {
  224. warnOnce(
  225. 'Component template requires a root element, rather than just text.',
  226. { start }
  227. )
  228. } else if ((text = text.trim())) {
  229. warnOnce(
  230. `text "${text}" outside root element will be ignored.`,
  231. { start }
  232. )
  233. }
  234. }
  235. return
  236. }
  237. // IE textarea placeholder bug
  238. /* istanbul ignore if */
  239. if (isIE &&
  240. currentParent.tag === 'textarea' &&
  241. currentParent.attrsMap.placeholder === text
  242. ) {
  243. return
  244. }
  245. const children = currentParent.children
  246. if (inPre || text.trim()) {
  247. text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
  248. } else if (!children.length) {
  249. // remove the whitespace-only node right after an opening tag
  250. text = ''
  251. } else if (whitespaceOption) {
  252. if (whitespaceOption === 'condense') {
  253. // in condense mode, remove the whitespace node if it contains
  254. // line break, otherwise condense to a single space
  255. text = lineBreakRE.test(text) ? '' : ' '
  256. } else {
  257. text = ' '
  258. }
  259. } else {
  260. text = preserveWhitespace ? ' ' : ''
  261. }
  262. if (text) {
  263. if (!inPre && whitespaceOption === 'condense') {
  264. // condense consecutive whitespaces into single space
  265. text = text.replace(whitespaceRE, ' ')
  266. }
  267. let res
  268. let child: ?ASTNode
  269. if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  270. child = {
  271. type: 2,
  272. expression: res.expression,
  273. tokens: res.tokens,
  274. text
  275. }
  276. } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
  277. child = {
  278. type: 3,
  279. text
  280. }
  281. }
  282. if (child) {
  283. if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
  284. child.start = start
  285. child.end = end
  286. }
  287. children.push(child)
  288. }
  289. }
  290. },
  291. comment (text: string, start, end) {
  292. // adding anything as a sibling to the root node is forbidden
  293. // comments should still be allowed, but ignored
  294. if (currentParent) {
  295. const child: ASTText = {
  296. type: 3,
  297. text,
  298. isComment: true
  299. }
  300. if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
  301. child.start = start
  302. child.end = end
  303. }
  304. currentParent.children.push(child)
  305. }
  306. }
  307. })
  308. return root
  309. }

从options中获取方法和配置

parse函数的输入是template和options(和平台相关的一些配置),输出是AST的根节点(模板字符串)
定义在src/platforms/web/compiler/options中
放到这个目录是因为它们在不同的平台(web和weex)的实现是不同的

  1. import {
  2. isPreTag,
  3. mustUseProp,
  4. isReservedTag,
  5. getTagNamespace
  6. } from '../util/index'
  7. import modules from './modules/index'
  8. import directives from './directives/index'
  9. import { genStaticKeys } from 'shared/util'
  10. import { isUnaryTag, canBeLeftOpenTag } from './util'
  11. export const baseOptions: CompilerOptions = {
  12. expectHTML: true,
  13. modules,
  14. directives,
  15. isPreTag,
  16. isUnaryTag,
  17. mustUseProp,
  18. canBeLeftOpenTag,
  19. isReservedTag,
  20. getTagNamespace,
  21. staticKeys: genStaticKeys(modules)
  22. }

实现

  1. warn = options.warn || baseWarn
  2. platformIsPreTag = options.isPreTag || no
  3. platformMustUseProp = options.mustUseProp || no
  4. platformGetTagNamespace = options.getTagNamespace || no
  5. const isReservedTag = options.isReservedTag || no
  6. maybeComponent = (el: ASTElement) => !!(
  7. el.component ||
  8. el.attrsMap[':is'] ||
  9. el.attrsMap['v-bind:is'] ||
  10. !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
  11. )
  12. transforms = pluckModuleFunction(options.modules, 'transformNode')
  13. preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  14. postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
  15. delimiters = options.delimiters

解析 HTML 模板

对template模板的解析主要是通过parseHTML函数
定义在src/compiler/parser/html-parser中

  1. export function parseHTML (html, options) {
  2. const stack = []
  3. const expectHTML = options.expectHTML
  4. const isUnaryTag = options.isUnaryTag || no
  5. const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  6. let index = 0
  7. let last, lastTag
  8. while (html) {
  9. last = html
  10. // Make sure we're not in a plaintext content element like script/style
  11. if (!lastTag || !isPlainTextElement(lastTag)) {
  12. let textEnd = html.indexOf('<')
  13. if (textEnd === 0) {
  14. // Comment: 注释节点和文档类型节点
  15. // 对于注释节点和文档类型节点的匹配,如果匹配到做前进即可
  16. if (comment.test(html)) {
  17. const commentEnd = html.indexOf('-->')
  18. if (commentEnd >= 0) {
  19. if (options.shouldKeepComment) {
  20. options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
  21. }
  22. // 对于注释节点,前进至它们的末尾位置
  23. advance(commentEnd + 3)
  24. continue
  25. }
  26. }
  27. // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
  28. if (conditionalComment.test(html)) {
  29. const conditionalEnd = html.indexOf(']>')
  30. if (conditionalEnd >= 0) {
  31. // 对于条件注释节点,前进至它们的末尾位置
  32. advance(conditionalEnd + 2)
  33. continue
  34. }
  35. }
  36. // Doctype:
  37. const doctypeMatch = html.match(doctype)
  38. if (doctypeMatch) {
  39. // 对于文档类型节点,则前进它自身长度的距离
  40. advance(doctypeMatch[0].length)
  41. continue
  42. }
  43. // End tag: 结束标签
  44. const endTagMatch = html.match(endTag) // 通过正则endTag匹配到闭合标签
  45. if (endTagMatch) {
  46. const curIndex = index
  47. advance(endTagMatch[0].length) // 前进到闭合标签末尾
  48. parseEndTag(endTagMatch[1], curIndex, index) // 对闭合标签做解析
  49. continue
  50. }
  51. // Start tag: 开始标签
  52. const startTagMatch = parseStartTag() // 解析开始标签
  53. if (startTagMatch) {
  54. handleStartTag(startTagMatch)
  55. if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
  56. advance(1)
  57. }
  58. continue
  59. }
  60. }
  61. // 文本
  62. let text, rest, next
  63. if (textEnd >= 0) { // 满足则说明从当前位置到textEnd位置都是文本
  64. rest = html.slice(textEnd)
  65. while (
  66. !endTag.test(rest) &&
  67. !startTagOpen.test(rest) &&
  68. !comment.test(rest) &&
  69. !conditionalComment.test(rest)
  70. ) {
  71. // < in plain text, be forgiving and treat it as text
  72. // 如果 < 是纯文本的字符就继续找到真正的文本结束的位置
  73. next = rest.indexOf('<', 1)
  74. if (next < 0) break
  75. textEnd += next
  76. rest = html.slice(textEnd)
  77. }
  78. text = html.substring(0, textEnd) // 前进到结束的位置
  79. }
  80. if (textEnd < 0) { // 满足则说明整个template解析完毕了,把剩余的html都赋值给了text
  81. text = html
  82. }
  83. if (text) {
  84. advance(text.length)
  85. }
  86. // 调用options.chars回调函数并传text参数
  87. if (options.chars && text) {
  88. options.chars(text, index - text.length, index)
  89. }
  90. } else {
  91. let endTagLength = 0
  92. const stackedTag = lastTag.toLowerCase()
  93. const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
  94. const rest = html.replace(reStackedTag, function (all, text, endTag) {
  95. endTagLength = endTag.length
  96. if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
  97. text = text
  98. .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
  99. .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
  100. }
  101. if (shouldIgnoreFirstNewline(stackedTag, text)) {
  102. text = text.slice(1)
  103. }
  104. if (options.chars) {
  105. options.chars(text)
  106. }
  107. return ''
  108. })
  109. index += html.length - rest.length
  110. html = rest
  111. parseEndTag(stackedTag, index - endTagLength, index)
  112. }
  113. if (html === last) {
  114. options.chars && options.chars(html)
  115. if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
  116. options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
  117. }
  118. break
  119. }
  120. }
  121. // Clean up any remaining tags
  122. parseEndTag()
  123. function advance (n) {
  124. index += n
  125. html = html.substring(n)
  126. }
  127. // 解析开始标签
  128. function parseStartTag () {
  129. // 通过正则匹配到开始标签
  130. const start = html.match(startTagOpen)
  131. if (start) {
  132. // 定义match对象
  133. const match = {
  134. tagName: start[1],
  135. attrs: [],
  136. start: index
  137. }
  138. advance(start[0].length)
  139. let end, attr
  140. // 循环去匹配开始标签中的属性并添加到match.attrs中,直到匹配的开始标签的闭合符结束
  141. while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
  142. attr.start = index
  143. advance(attr[0].length)
  144. attr.end = index
  145. match.attrs.push(attr)
  146. }
  147. // 匹配到闭合符则获取一元斜线符,前进到闭合符尾,并把当前索引赋值给match.end
  148. if (end) {
  149. match.unarySlash = end[1]
  150. advance(end[0].length)
  151. match.end = index
  152. return match
  153. }
  154. }
  155. }
  156. // 对match处理
  157. function handleStartTag (match) {
  158. const tagName = match.tagName
  159. const unarySlash = match.unarySlash
  160. if (expectHTML) {
  161. if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
  162. parseEndTag(lastTag)
  163. }
  164. if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
  165. parseEndTag(tagName)
  166. }
  167. }
  168. // 判断开始标签是否是一元标签,类似<img>、<br />
  169. const unary = isUnaryTag(tagName) || !!unarySlash
  170. // 对match.attrs遍历并做了一些处理
  171. const l = match.attrs.length
  172. const attrs = new Array(l)
  173. for (let i = 0; i < l; i++) {
  174. const args = match.attrs[i]
  175. const value = args[3] || args[4] || args[5] || ''
  176. const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
  177. ? options.shouldDecodeNewlinesForHref
  178. : options.shouldDecodeNewlines
  179. attrs[i] = {
  180. name: args[1],
  181. value: decodeAttr(value, shouldDecodeNewlines)
  182. }
  183. if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
  184. attrs[i].start = args.start + args[0].match(/^\s*/).length
  185. attrs[i].end = args.end
  186. }
  187. }
  188. // 非一元标签,则往stack里push一个对象,并且把tagName赋值给lastTag
  189. if (!unary) {
  190. stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
  191. lastTag = tagName
  192. }
  193. // 调用options.start回调函数并传入一些参数
  194. if (options.start) {
  195. options.start(tagName, attrs, unary, match.start, match.end)
  196. }
  197. }
  198. // 对闭合标签做解析
  199. // 倒序stack,就是找到第一个和当前endTag匹配的元素
  200. function parseEndTag (tagName, start, end) {
  201. let pos, lowerCasedTagName
  202. if (start == null) start = index
  203. if (end == null) end = index
  204. // Find the closest opened tag of the same type
  205. if (tagName) {
  206. lowerCasedTagName = tagName.toLowerCase()
  207. for (pos = stack.length - 1; pos >= 0; pos--) {
  208. if (stack[pos].lowerCasedTag === lowerCasedTagName) {
  209. break
  210. }
  211. }
  212. } else {
  213. // If no tag name is provided, clean shop
  214. pos = 0
  215. }
  216. if (pos >= 0) {
  217. // Close all the open elements, up the stack
  218. for (let i = stack.length - 1; i >= pos; i--) {
  219. if (process.env.NODE_ENV !== 'production' &&
  220. (i > pos || !tagName) &&
  221. options.warn
  222. ) {
  223. options.warn(
  224. `tag <${stack[i].tag}> has no matching end tag.`,
  225. { start: stack[i].start, end: stack[i].end }
  226. )
  227. }
  228. if (options.end) {
  229. options.end(stack[i].tag, start, end)
  230. }
  231. }
  232. // Remove the open elements from the stack
  233. stack.length = pos
  234. lastTag = pos && stack[pos - 1].tag
  235. } else if (lowerCasedTagName === 'br') {
  236. if (options.start) {
  237. options.start(tagName, [], true, start, end)
  238. }
  239. } else if (lowerCasedTagName === 'p') {
  240. if (options.start) {
  241. options.start(tagName, [], false, start, end)
  242. }
  243. if (options.end) {
  244. options.end(tagName, start, end)
  245. }
  246. }
  247. }
  248. }

循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。 在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾

advance 函数

  1. function advance (n) {
  2. index += n
  3. html = html.substring(n)
  4. }

advance-1.png

  1. advance(4)

advance-2.png

匹配的过程中主要利用了正则表达式

  1. // Regular Expressions for parsing tags and attributes
  2. // 匹配文档类型节点
  3. const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
  4. const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
  5. const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
  6. const qnameCapture = `((?:${ncname}\\:)?${ncname})`
  7. // 匹配开始闭合标签
  8. const startTagOpen = new RegExp(`^<${qnameCapture}`)
  9. const startTagClose = /^\s*(\/?)>/
  10. const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
  11. const doctype = /^<!DOCTYPE [^>]+>/i
  12. // #7298: escape - to avoid being passed as HTML comment when inlined in page
  13. // 匹配注释节点
  14. const comment = /^<!\--/
  15. const conditionalComment = /^<!\[/

闭合标签
对于非一元标签(有 endTag)我们都把它构造成一个对象压入到 stack 中
stack.png
对于闭合标签的解析,就是倒序 stack,找到第一个和当前 endTag 匹配的元素
如果是正常的标签匹配,那么 stack 的最后一个元素应该和当前的 endTag 匹配
考虑到如下错误情况

  1. <div><span></div>

当 endTag 为 的时候,从 stack 尾部找到的标签是 ,就不能匹配,因此这种情况会报警告。匹配后把栈到 pos 位置的都弹出,并从 stack 尾部拿到 lastTag

处理开始标签

当解析到开始标签的时候,最后会执行 start 回调函数,函数主要就做 3 件事情

  1. 创建 AST 元素
  2. 处理 AST 元素
  3. AST 树管理

    创建AST元素

    ```javascript // check namespace. // inherit parent ns if there is one const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

// handle IE svg bug / istanbul ignore if / if (isIE && ns === ‘svg’) { attrs = guardIESVGBug(attrs) } // 通过createASTElement方法去创建一个AST元素,并添加namespace let element: ASTElement = createASTElement(tag, attrs, currentParent) if (ns) { element.ns = ns }

  1. 每一个AST元素就是一个普通的JavaScript对象
  2. ```javascript
  3. export function createASTElement (
  4. tag: string,
  5. attrs: Array<ASTAttr>,
  6. parent: ASTElement | void
  7. ): ASTElement {
  8. return {
  9. type: 1, // AST元素类型
  10. tag, // 标签名
  11. attrsList: attrs, // 属性列表
  12. attrsMap: makeAttrsMap(attrs), // 属性映射表
  13. rawAttrsMap: {}, // 初始的属性映射表
  14. parent, // 父的AST元素
  15. children: [] // 子AST元素集合
  16. }
  17. }

处理AST元素

if (isForbiddenTag(element) && !isServerRendering()) {
  element.forbidden = true
  process.env.NODE_ENV !== 'production' && warn(
    'Templates should only be responsible for mapping the state to the ' +
    'UI. Avoid placing tags with side-effects in your templates, such as ' +
    `<${tag}>` + ', as they will not be parsed.',
    { start: element.start }
  )
}

// apply pre-transforms
// 对模块preTransforms调用
for (let i = 0; i < preTransforms.length; i++) {
  element = preTransforms[i](element, options) || element
}
// 判断element是否包含各种指令通过processXXX做相应处理,处理的结果就是扩展AST元素的属性
if (!inVPre) {
  processPre(element)
  if (element.pre) {
    inVPre = true
  }
}
if (platformIsPreTag(element.tag)) {
  inPre = true
}
if (inVPre) {
  processRawAttrs(element)
} else if (!element.processed) {
  // structural directives
  processFor(element)
  processIf(element)
  processOnce(element)
}

processFor

export function processFor (el: ASTElement) {
  let exp
  // 从元素中拿到v-for指令的内容
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const res = parseFor(exp)
    if (res) {
      extend(el, res)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `Invalid v-for expression: ${exp}`,
        el.rawAttrsMap['v-for']
      )
    }
  }
}

export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
// 分别解析出for、alias、iterator1、iterator2等属性的值添加到AST的元素上
// v-for="(item,index) in data" 而言,解析出的的 for 是 data,alias 是 item,iterator1 是 index,没有 iterator2
export function parseFor (exp: string): ?ForParseResult {
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return
  const res = {}
  res.for = inMatch[2].trim()
  const alias = inMatch[1].trim().replace(stripParensRE, '')
  const iteratorMatch = alias.match(forIteratorRE)
  if (iteratorMatch) {
    res.alias = alias.replace(forIteratorRE, '').trim()
    res.iterator1 = iteratorMatch[1].trim()
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else {
    res.alias = alias
  }
  return res
}

processIf

function processIf (el) {
  // 从元素中拿v-if指令的内容
  const exp = getAndRemoveAttr(el, 'v-if')
  // 给AST元素添加if属性和ifConditions属性
  if (exp) {
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else { // 尝试拿v-else指令及v-else-if指令的内容
    // 拿到则给AST元素分别添加else和elseif属性
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true 
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

AST树管理

在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,也要为它们建立父子关系,就像 DOM 元素的父子关系那样
AST 树管理的目标是构建一颗 AST 树,本质上它要维护 root 根节点和当前父节点 currentParent
为了保证元素可以正确闭合,这里也利用了 stack栈的数据结构

function checkRootConstraints (el) {
  if (el.tag === 'slot' || el.tag === 'template') {
    warnOnce(
      `Cannot use <${el.tag}> as component root element because it may ` +
      'contain multiple nodes.',
      { start: el.start }
    )
  }
  if (el.attrsMap.hasOwnProperty('v-for')) {
    warnOnce(
      'Cannot use v-for on stateful component root element because ' +
      'it renders multiple elements.',
      el.rawAttrsMap['v-for']
    )
  }
}

function closeElement (element) {
  trimEndingWhitespace(element)
  if (!inVPre && !element.processed) {
    element = processElement(element, options)
  }
  // tree management
  if (!stack.length && element !== root) {
    // allow root elements with v-if, v-else-if and v-else
    if (root.if && (element.elseif || element.else)) {
      if (process.env.NODE_ENV !== 'production') {
        checkRootConstraints(element)
      }
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      warnOnce(
        `Component template should contain exactly one root element. ` +
        `If you are using v-if on multiple elements, ` +
        `use v-else-if to chain them instead.`,
        { start: element.start }
      )
    }
  }
  // 处理开始标签时判断如果有currentParent就把当前AST元素push到currentParent.children中,同时把AST元素的parent指向currentParent
  if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) {
      processIfConditions(element, currentParent)
    } else {
      if (element.slotScope) {
        // scoped slot
        // keep it in the children list so that v-else(-if) conditions can
        // find it as the prev node.
        const name = element.slotTarget || '"default"'
        ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
      }
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }

  // final children cleanup
  // filter out scoped slots
  element.children = element.children.filter(c => !(c: any).slotScope)
  // remove trailing whitespace node again
  trimEndingWhitespace(element)

  // check pre state
  if (element.pre) {
    inVPre = false
  }
  if (platformIsPreTag(element.tag)) {
    inPre = false
  }
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

stack 和 currentParent 除了在处理开始标签的时候会变化,在处理闭合标签的时候也会变化,因此整个 AST 树管理要结合闭合标签的处理逻辑看

处理闭合标签

当解析到闭合标签时会执行end回调函数

end (tag, start, end) {
  const element = stack[stack.length - 1]
  // pop stack
  stack.length -= 1
  currentParent = stack[stack.length - 1] // 把stack最后一个元素赋值给currentParent,保证当遇到闭合标签时可以正确更新stack的长度以及currentParent的值,这样来维护整个AST树
  if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
    element.end = end
  }
  closeElement(element)
},

closeElement(element):更新一下 inVPre 和 inPre 的状态,以及执行 postTransforms 函数

function closeElement (element) {
  trimEndingWhitespace(element)
  if (!inVPre && !element.processed) {
    element = processElement(element, options)
  }
  // tree management
  if (!stack.length && element !== root) {
    // allow root elements with v-if, v-else-if and v-else
    if (root.if && (element.elseif || element.else)) {
      if (process.env.NODE_ENV !== 'production') {
        checkRootConstraints(element)
      }
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      warnOnce(
        `Component template should contain exactly one root element. ` +
        `If you are using v-if on multiple elements, ` +
        `use v-else-if to chain them instead.`,
        { start: element.start }
      )
    }
  }
  if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) {
      processIfConditions(element, currentParent)
    } else {
      if (element.slotScope) {
        // scoped slot
        // keep it in the children list so that v-else(-if) conditions can
        // find it as the prev node.
        const name = element.slotTarget || '"default"'
        ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
      }
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }

  // final children cleanup
  // filter out scoped slots
  element.children = element.children.filter(c => !(c: any).slotScope)
  // remove trailing whitespace node again
  trimEndingWhitespace(element)

  // check pre state
  if (element.pre) {
    inVPre = false
  }
  if (platformIsPreTag(element.tag)) {
    inPre = false
  }
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

处理文本内容

文本构造的 AST 元素有 2 种类型,一种是有表达式的,type 为 2,一种是纯文本,type 为 3

chars (text: string, start: number, end: number) {
  if (!currentParent) {
    if (process.env.NODE_ENV !== 'production') {
      if (text === template) {
        warnOnce(
          'Component template requires a root element, rather than just text.',
          { start }
        )
      } else if ((text = text.trim())) {
        warnOnce(
          `text "${text}" outside root element will be ignored.`,
          { start }
        )
      }
    }
    return
  }
  // IE textarea placeholder bug
  /* istanbul ignore if */
  if (isIE &&
      currentParent.tag === 'textarea' &&
      currentParent.attrsMap.placeholder === text
     ) {
    return
  }
  const children = currentParent.children
  if (inPre || text.trim()) {
    text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
  } else if (!children.length) {
    // remove the whitespace-only node right after an opening tag
    text = ''
  } else if (whitespaceOption) {
    if (whitespaceOption === 'condense') {
      // in condense mode, remove the whitespace node if it contains
      // line break, otherwise condense to a single space
      text = lineBreakRE.test(text) ? '' : ' '
    } else {
      text = ' '
    }
  } else {
    text = preserveWhitespace ? ' ' : ''
  }
  if (text) {
    if (!inPre && whitespaceOption === 'condense') {
      // condense consecutive whitespaces into single space
      text = text.replace(whitespaceRE, ' ')
    }
    let res
    let child: ?ASTNode
    if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
      child = {
        type: 2,
        expression: res.expression,
        tokens: res.tokens,
        text
      }
    } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
      child = {
        type: 3,
        text
      }
    }
    if (child) {
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        child.start = start
        child.end = end
      }
      children.push(child)
    }
  }
},

表达式调用parseText(text, delimiters)对文本解析
定义在src/compiler/parser/text-parser.js中

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g

export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  // 根据分隔符(默认是{{}})构造了文本匹配的正则表达式
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  // 循环匹配文本
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    // 遇到普通文本就push到rawTokens和tokens中
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    // 遇到表达式就转换成_s(${exp}) push到tokens中  转换成{@binging:exp} push到rawTokens中
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

对于例子
tokens 就是 [_s(item),’”:”‘,_s(index)];rawTokens 就是 [{‘@binding’:’item’},’:’,{‘@binding’:’index’}]

return {
 expression: '_s(item)+":"+_s(index)',
 tokens: [{'@binding':'item'},':',{'@binding':'index'}]
}

流程图

parse.png

AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本