parse

编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。

这个过程是比较复杂的,它会用到大量正则表达式对字符串解析,如果对正则不是很了解,建议先去补习正则表达式的知识。为了直观地演示 parse 的过程,我们先来看一个例子:

  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 指向它的所有子节点。先对 AST 有一些直观的印象,那么接下来我们来分析一下这个 AST 是如何得到的。

整体流程

首先来看一下 parse 的定义,在 src/compiler/parser/index.js 中:

  1. export function parse (
  2. template: string,
  3. options: CompilerOptions
  4. ): ASTElement | void {
  5. getFnsAndConfigFromOptions(options)
  6. parseHTML(template, {
  7. // options ...
  8. start (tag, attrs, unary) {
  9. let element = createASTElement(tag, attrs)
  10. processElement(element)
  11. treeManagement()
  12. },
  13. end () {
  14. treeManagement()
  15. closeElement()
  16. },
  17. chars (text: string) {
  18. handleText()
  19. createChildrenASTOfText()
  20. },
  21. comment (text: string) {
  22. createChildrenASTOfComment()
  23. }
  24. })
  25. return astRootElement
  26. }

parse 函数的代码很长,贴一遍对同学的理解没有好处,我先把它拆成伪代码的形式,方便同学们对整体流程先有一个大致的了解。接下来我们就来分解分析每段伪代码的作用。

从 options 中获取方法和配置

对应伪代码:

  1. getFnsAndConfigFromOptions(options)

parse 函数的输入是 templateoptions,输出是 AST 的根节点。template 就是我们的模板字符串,而 options 实际上是和平台相关的一些配置,它的定义在 src/platforms/web/compiler/options 中:

  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. }

这些属性和方法之所以放到 platforms 目录下是因为它们在不同的平台(web 和 weex)的实现是不同的。

我们用伪代码 getFnsAndConfigFromOptions 表示了这一过程,它的实际代码如下:

  1. warn = options.warn || baseWarn
  2. platformIsPreTag = options.isPreTag || no
  3. platformMustUseProp = options.mustUseProp || no
  4. platformGetTagNamespace = options.getTagNamespace || no
  5. transforms = pluckModuleFunction(options.modules, 'transformNode')
  6. preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  7. postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
  8. delimiters = options.delimiters

这些方法和配置都是后续解析时候需要的,可以不用去管它们的具体作用,我们先往后看。

解析 HTML 模板

对应伪代码:

  1. parseHTML(template, options)

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

  1. export function parseHTML (html, options) {
  2. let lastTag
  3. while (html) {
  4. if (!lastTag || !isPlainTextElement(lastTag)){
  5. let textEnd = html.indexOf('<')
  6. if (textEnd === 0) {
  7. if(matchComment) {
  8. advance(commentLength)
  9. continue
  10. }
  11. if(matchDoctype) {
  12. advance(doctypeLength)
  13. continue
  14. }
  15. if(matchEndTag) {
  16. advance(endTagLength)
  17. parseEndTag()
  18. continue
  19. }
  20. if(matchStartTag) {
  21. parseStartTag()
  22. handleStartTag()
  23. continue
  24. }
  25. }
  26. handleText()
  27. advance(textLength)
  28. } else {
  29. handlePlainTextElement()
  30. parseEndTag()
  31. }
  32. }
  33. }

由于 parseHTML 的逻辑也非常复杂,因此我也用了伪代码的方式表达,整体来说它的逻辑就是循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。

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

为了更加直观地说明 advance 的作用,可以通过一副图表示:
parse - 图1
调用 advance 函数:

  1. advance(4)

得到结果:
parse - 图2
匹配的过程中主要利用了正则表达式,如下:

  1. const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
  2. const ncname = '[a-zA-Z_][\\w\\-\\.]*'
  3. const qnameCapture = `((?:${ncname}\\:)?${ncname})`
  4. const startTagOpen = new RegExp(`^<${qnameCapture}`)
  5. const startTagClose = /^\s*(\/?)>/
  6. const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
  7. const doctype = /^<!DOCTYPE [^>]+>/i
  8. const comment = /^<!\--/
  9. const conditionalComment = /^<!\[/

通过这些正则表达式,我们可以匹配注释节点、文档类型节点、开始闭合标签等。

  • 注释节点、文档类型节点
    对于注释节点和文档类型节点的匹配,如果匹配到我们仅仅做的是做前进即可。
  1. if (comment.test(html)) {
  2. const commentEnd = html.indexOf('-->')
  3. if (commentEnd >= 0) {
  4. if (options.shouldKeepComment) {
  5. options.comment(html.substring(4, commentEnd))
  6. }
  7. advance(commentEnd + 3)
  8. continue
  9. }
  10. }
  11. if (conditionalComment.test(html)) {
  12. const conditionalEnd = html.indexOf(']>')
  13. if (conditionalEnd >= 0) {
  14. advance(conditionalEnd + 2)
  15. continue
  16. }
  17. }
  18. const doctypeMatch = html.match(doctype)
  19. if (doctypeMatch) {
  20. advance(doctypeMatch[0].length)
  21. continue
  22. }

对于注释和条件注释节点,前进至它们的末尾位置;对于文档类型节点,则前进它自身长度的距离。

  • 开始标签
  1. const startTagMatch = parseStartTag()
  2. if (startTagMatch) {
  3. handleStartTag(startTagMatch)
  4. if (shouldIgnoreFirstNewline(lastTag, html)) {
  5. advance(1)
  6. }
  7. continue
  8. }

首先通过 parseStartTag 解析开始标签:

  1. function parseStartTag () {
  2. const start = html.match(startTagOpen)
  3. if (start) {
  4. const match = {
  5. tagName: start[1],
  6. attrs: [],
  7. start: index
  8. }
  9. advance(start[0].length)
  10. let end, attr
  11. while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
  12. advance(attr[0].length)
  13. match.attrs.push(attr)
  14. }
  15. if (end) {
  16. match.unarySlash = end[1]
  17. advance(end[0].length)
  18. match.end = index
  19. return match
  20. }
  21. }
  22. }

对于开始标签,除了标签名之外,还有一些标签相关的属性。函数先通过正则表达式 startTagOpen 匹配到开始标签,然后定义了 match 对象,接着循环去匹配开始标签中的属性并添加到 match.attrs 中,直到匹配的开始标签的闭合符结束。如果匹配到闭合符,则获取一元斜线符,前进到闭合符尾,并把当前索引赋值给 match.end

parseStartTag 对开始标签解析拿到 match 后,紧接着会执行 handleStartTagmatch 做处理:

  1. function handleStartTag (match) {
  2. const tagName = match.tagName
  3. const unarySlash = match.unarySlash
  4. if (expectHTML) {
  5. if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
  6. parseEndTag(lastTag)
  7. }
  8. if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
  9. parseEndTag(tagName)
  10. }
  11. }
  12. const unary = isUnaryTag(tagName) || !!unarySlash
  13. const l = match.attrs.length
  14. const attrs = new Array(l)
  15. for (let i = 0; i < l; i++) {
  16. const args = match.attrs[i]
  17. if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
  18. if (args[3] === '') { delete args[3] }
  19. if (args[4] === '') { delete args[4] }
  20. if (args[5] === '') { delete args[5] }
  21. }
  22. const value = args[3] || args[4] || args[5] || ''
  23. const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
  24. ? options.shouldDecodeNewlinesForHref
  25. : options.shouldDecodeNewlines
  26. attrs[i] = {
  27. name: args[1],
  28. value: decodeAttr(value, shouldDecodeNewlines)
  29. }
  30. }
  31. if (!unary) {
  32. stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
  33. lastTag = tagName
  34. }
  35. if (options.start) {
  36. options.start(tagName, attrs, unary, match.start, match.end)
  37. }
  38. }

handleStartTag 的核心逻辑很简单,先判断开始标签是否是一元标签,类似 <img>、<br/> 这样,接着对 match.attrs 遍历并做了一些处理,最后判断如果非一元标签,则往 stack 里 push 一个对象,并且把 tagName 赋值给 lastTag。至于 stack 的作用,稍后我会介绍。

最后调用了 options.start 回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。

  • 闭合标签
  1. const endTagMatch = html.match(endTag)
  2. if (endTagMatch) {
  3. const curIndex = index
  4. advance(endTagMatch[0].length)
  5. parseEndTag(endTagMatch[1], curIndex, index)
  6. continue
  7. }

先通过正则 endTag 匹配到闭合标签,然后前进到闭合标签末尾,然后执行 parseEndTag 方法对闭合标签做解析。

  1. function parseEndTag (tagName, start, end) {
  2. let pos, lowerCasedTagName
  3. if (start == null) start = index
  4. if (end == null) end = index
  5. if (tagName) {
  6. lowerCasedTagName = tagName.toLowerCase()
  7. }
  8. if (tagName) {
  9. for (pos = stack.length - 1; pos >= 0; pos--) {
  10. if (stack[pos].lowerCasedTag === lowerCasedTagName) {
  11. break
  12. }
  13. }
  14. } else {
  15. pos = 0
  16. }
  17. if (pos >= 0) {
  18. for (let i = stack.length - 1; i >= pos; i--) {
  19. if (process.env.NODE_ENV !== 'production' &&
  20. (i > pos || !tagName) &&
  21. options.warn
  22. ) {
  23. options.warn(
  24. `tag <${stack[i].tag}> has no matching end tag.`
  25. )
  26. }
  27. if (options.end) {
  28. options.end(stack[i].tag, start, end)
  29. }
  30. }
  31. stack.length = pos
  32. lastTag = pos && stack[pos - 1].tag
  33. } else if (lowerCasedTagName === 'br') {
  34. if (options.start) {
  35. options.start(tagName, [], true, start, end)
  36. }
  37. } else if (lowerCasedTagName === 'p') {
  38. if (options.start) {
  39. options.start(tagName, [], false, start, end)
  40. }
  41. if (options.end) {
  42. options.end(tagName, start, end)
  43. }
  44. }
  45. }

parseEndTag 的核心逻辑很简单,在介绍之前我们回顾一下在执行 handleStartTag 的时候,对于非一元标签(有 endTag)我们都把它构造成一个对象压入到 stack 中,如图所示:
parse - 图3
那么对于闭合标签的解析,就是倒序 stack,找到第一个和当前 endTag 匹配的元素。如果是正常的标签匹配,那么 stack 的最后一个元素应该和当前的 endTag 匹配,但是考虑到如下错误情况:

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

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

最后调用了 options.end 回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。

  • 文本
  1. let text, rest, next
  2. if (textEnd >= 0) {
  3. rest = html.slice(textEnd)
  4. while (
  5. !endTag.test(rest) &&
  6. !startTagOpen.test(rest) &&
  7. !comment.test(rest) &&
  8. !conditionalComment.test(rest)
  9. ) {
  10. next = rest.indexOf('<', 1)
  11. if (next < 0) break
  12. textEnd += next
  13. rest = html.slice(textEnd)
  14. }
  15. text = html.substring(0, textEnd)
  16. advance(textEnd)
  17. }
  18. if (textEnd < 0) {
  19. text = html
  20. html = ''
  21. }
  22. if (options.chars && text) {
  23. options.chars(text)
  24. }

接下来判断 textEnd 是否大于等于 0 的,满足则说明到从当前位置到 textEnd 位置都是文本,并且如果 < 是纯文本中的字符,就继续找到真正的文本结束的位置,然后前进到结束的位置。

再继续判断 textEnd 小于 0 的情况,则说明整个 template 解析完毕了,把剩余的 html 都赋值给了 text

最后调用了 options.chars 回调函数,并传 text 参数,这个回调函数的作用稍后我会详细介绍。

因此,在循环解析整个 template 的过程中,会根据不同的情况,去执行不同的回调函数,下面我们来看看这些回调函数的作用。

处理开始标签

对应伪代码:

  1. start (tag, attrs, unary) {
  2. let element = createASTElement(tag, attrs)
  3. processElement(element)
  4. treeManagement()
  5. }

当解析到开始标签的时候,最后会执行 start 回调函数,函数主要就做 3 件事情,创建 AST 元素,处理 AST 元素,AST 树管理。下面我们来分别来看这几个过程。

  • 创建 AST 元素
  1. // check namespace.
  2. // inherit parent ns if there is one
  3. const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
  4. // handle IE svg bug
  5. /* istanbul ignore if */
  6. if (isIE && ns === 'svg') {
  7. attrs = guardIESVGBug(attrs)
  8. }
  9. let element: ASTElement = createASTElement(tag, attrs, currentParent)
  10. if (ns) {
  11. element.ns = ns
  12. }
  13. export function createASTElement (
  14. tag: string,
  15. attrs: Array<Attr>,
  16. parent: ASTElement | void
  17. ): ASTElement {
  18. return {
  19. type: 1,
  20. tag,
  21. attrsList: attrs,
  22. attrsMap: makeAttrsMap(attrs),
  23. parent,
  24. children: []
  25. }
  26. }

通过 createASTElement 方法去创建一个 AST 元素,并添加了 namespace。可以看到,每一个 AST 元素就是一个普通的 JavaScript 对象,其中,type 表示 AST 元素类型,tag 表示标签名,attrsList 表示属性列表,attrsMap 表示属性映射表,parent 表示父的 AST 元素,children 表示子 AST 元素集合。

  • 处理 AST 元素
  1. if (isForbiddenTag(element) && !isServerRendering()) {
  2. element.forbidden = true
  3. process.env.NODE_ENV !== 'production' && warn(
  4. 'Templates should only be responsible for mapping the state to the ' +
  5. 'UI. Avoid placing tags with side-effects in your templates, such as ' +
  6. `<${tag}>` + ', as they will not be parsed.'
  7. )
  8. }
  9. // apply pre-transforms
  10. for (let i = 0; i < preTransforms.length; i++) {
  11. element = preTransforms[i](element, options) || element
  12. }
  13. if (!inVPre) {
  14. processPre(element)
  15. if (element.pre) {
  16. inVPre = true
  17. }
  18. }
  19. if (platformIsPreTag(element.tag)) {
  20. inPre = true
  21. }
  22. if (inVPre) {
  23. processRawAttrs(element)
  24. } else if (!element.processed) {
  25. // structural directives
  26. processFor(element)
  27. processIf(element)
  28. processOnce(element)
  29. // element-scope stuff
  30. processElement(element, options)
  31. }

首先是对模块 preTransforms 的调用,其实所有模块的 preTransformstransformspostTransforms 的定义都在 src/platforms/web/compiler/modules 目录中,这部分我们暂时不会介绍,之后会结合具体的例子说。接着判断 element 是否包含各种指令通过 processXXX 做相应的处理,处理的结果就是扩展 AST 元素的属性。这里我并不会一一介绍所有的指令处理,而是结合我们当前的例子,我们来看一下 processForprocessIf

  1. export function processFor (el: ASTElement) {
  2. let exp
  3. if ((exp = getAndRemoveAttr(el, 'v-for'))) {
  4. const res = parseFor(exp)
  5. if (res) {
  6. extend(el, res)
  7. } else if (process.env.NODE_ENV !== 'production') {
  8. warn(
  9. `Invalid v-for expression: ${exp}`
  10. )
  11. }
  12. }
  13. }
  14. export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
  15. export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
  16. const stripParensRE = /^\(|\)$/g
  17. export function parseFor (exp: string): ?ForParseResult {
  18. const inMatch = exp.match(forAliasRE)
  19. if (!inMatch) return
  20. const res = {}
  21. res.for = inMatch[2].trim()
  22. const alias = inMatch[1].trim().replace(stripParensRE, '')
  23. const iteratorMatch = alias.match(forIteratorRE)
  24. if (iteratorMatch) {
  25. res.alias = alias.replace(forIteratorRE, '')
  26. res.iterator1 = iteratorMatch[1].trim()
  27. if (iteratorMatch[2]) {
  28. res.iterator2 = iteratorMatch[2].trim()
  29. }
  30. } else {
  31. res.alias = alias
  32. }
  33. return res
  34. }

processFor 就是从元素中拿到 v-for 指令的内容,然后分别解析出 foraliasiterator1iterator2 等属性的值添加到 AST 的元素上。就我们的示例 v-for="(item,index) in data" 而言,解析出的的 fordataaliasitemiterator1index,没有 iterator2

  1. function processIf (el) {
  2. const exp = getAndRemoveAttr(el, 'v-if')
  3. if (exp) {
  4. el.if = exp
  5. addIfCondition(el, {
  6. exp: exp,
  7. block: el
  8. })
  9. } else {
  10. if (getAndRemoveAttr(el, 'v-else') != null) {
  11. el.else = true
  12. }
  13. const elseif = getAndRemoveAttr(el, 'v-else-if')
  14. if (elseif) {
  15. el.elseif = elseif
  16. }
  17. }
  18. }
  19. export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  20. if (!el.ifConditions) {
  21. el.ifConditions = []
  22. }
  23. el.ifConditions.push(condition)
  24. }

processIf 就是从元素中拿 v-if 指令的内容,如果拿到则给 AST 元素添加 if 属性和 ifConditions 属性;否则尝试拿 v-else 指令及 v-else-if 指令的内容,如果拿到则给 AST 元素分别添加 elseelseif 属性。

  • AST 树管理
    我们在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,我们也要为它们建立父子关系,就像 DOM 元素的父子关系那样。

AST 树管理相关代码如下:

  1. function checkRootConstraints (el) {
  2. if (process.env.NODE_ENV !== 'production') {
  3. if (el.tag === 'slot' || el.tag === 'template') {
  4. warnOnce(
  5. `Cannot use <${el.tag}> as component root element because it may ` +
  6. 'contain multiple nodes.'
  7. )
  8. }
  9. if (el.attrsMap.hasOwnProperty('v-for')) {
  10. warnOnce(
  11. 'Cannot use v-for on stateful component root element because ' +
  12. 'it renders multiple elements.'
  13. )
  14. }
  15. }
  16. }
  17. // tree management
  18. if (!root) {
  19. root = element
  20. checkRootConstraints(root)
  21. } else if (!stack.length) {
  22. // allow root elements with v-if, v-else-if and v-else
  23. if (root.if && (element.elseif || element.else)) {
  24. checkRootConstraints(element)
  25. addIfCondition(root, {
  26. exp: element.elseif,
  27. block: element
  28. })
  29. } else if (process.env.NODE_ENV !== 'production') {
  30. warnOnce(
  31. `Component template should contain exactly one root element. ` +
  32. `If you are using v-if on multiple elements, ` +
  33. `use v-else-if to chain them instead.`
  34. )
  35. }
  36. }
  37. if (currentParent && !element.forbidden) {
  38. if (element.elseif || element.else) {
  39. processIfConditions(element, currentParent)
  40. } else if (element.slotScope) { // scoped slot
  41. currentParent.plain = false
  42. const name = element.slotTarget || '"default"'
  43. ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  44. } else {
  45. currentParent.children.push(element)
  46. element.parent = currentParent
  47. }
  48. }
  49. if (!unary) {
  50. currentParent = element
  51. stack.push(element)
  52. } else {
  53. closeElement(element)
  54. }

AST 树管理的目标是构建一颗 AST 树,本质上它要维护 root 根节点和当前父节点 currentParent。为了保证元素可以正确闭合,这里也利用了 stack 栈的数据结构,和我们之前解析模板时用到的 stack 类似。

当我们在处理开始标签的时候,判断如果有 currentParent,会把当前 AST 元素 push 到 currentParent.chilldren 中,同时把 AST 元素的 parent 指向 currentParent

接着就是更新 currentParentstack ,判断当前如果不是一个一元标签,我们要把它生成的 AST 元素 push 到 stack 中,并且把当前的 AST 元素赋值给 currentParent

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

处理闭合标签

对应伪代码:

  1. end () {
  2. treeManagement()
  3. closeElement()
  4. }

当解析到闭合标签的时候,最后会执行 end 回调函数:

  1. // remove trailing whitespace
  2. const element = stack[stack.length - 1]
  3. const lastNode = element.children[element.children.length - 1]
  4. if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
  5. element.children.pop()
  6. }
  7. // pop stack
  8. stack.length -= 1
  9. currentParent = stack[stack.length - 1]
  10. closeElement(element)

首先处理了尾部空格的情况,然后把 stack 的元素弹一个出栈,并把 stack 最后一个元素赋值给 currentParent,这样就保证了当遇到闭合标签的时候,可以正确地更新 stack 的长度以及 currentParent 的值,这样就维护了整个 AST 树。

最后执行了 closeElement(elment)

  1. function closeElement (element) {
  2. // check pre state
  3. if (element.pre) {
  4. inVPre = false
  5. }
  6. if (platformIsPreTag(element.tag)) {
  7. inPre = false
  8. }
  9. // apply post-transforms
  10. for (let i = 0; i < postTransforms.length; i++) {
  11. postTransforms[i](element, options)
  12. }
  13. }

closeElement 逻辑很简单,就是更新一下 inVPreinPre 的状态,以及执行 postTransforms 函数,这些我们暂时都不必了解。

处理文本内容

对应伪代码:

  1. chars (text: string) {
  2. handleText()
  3. createChildrenASTOfText()
  4. }

除了处理开始标签和闭合标签,我们还会在解析模板的过程中去处理一些文本内容:

  1. const children = currentParent.children
  2. text = inPre || text.trim()
  3. ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
  4. // only preserve whitespace if its not right after a starting tag
  5. : preserveWhitespace && children.length ? ' ' : ''
  6. if (text) {
  7. let res
  8. if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  9. children.push({
  10. type: 2,
  11. expression: res.expression,
  12. tokens: res.tokens,
  13. text
  14. })
  15. } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
  16. children.push({
  17. type: 3,
  18. text
  19. })
  20. }
  21. }

文本构造的 AST 元素有 2 种类型,一种是有表达式的,type 为 2,一种是纯文本,type 为 3。在我们的例子中,文本就是 :,是个表达式,通过执行 parseText(text, delimiters) 对文本解析,它的定义在 src/compiler/parser/text-parsre.js 中:

  1. const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
  2. const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g
  3. const buildRegex = cached(delimiters => {
  4. const open = delimiters[0].replace(regexEscapeRE, '\\$&')
  5. const close = delimiters[1].replace(regexEscapeRE, '\\$&')
  6. return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
  7. })
  8. export function parseText (
  9. text: string,
  10. delimiters?: [string, string]
  11. ): TextParseResult | void {
  12. const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  13. if (!tagRE.test(text)) {
  14. return
  15. }
  16. const tokens = []
  17. const rawTokens = []
  18. let lastIndex = tagRE.lastIndex = 0
  19. let match, index, tokenValue
  20. while ((match = tagRE.exec(text))) {
  21. index = match.index
  22. // push text token
  23. if (index > lastIndex) {
  24. rawTokens.push(tokenValue = text.slice(lastIndex, index))
  25. tokens.push(JSON.stringify(tokenValue))
  26. }
  27. // tag token
  28. const exp = parseFilters(match[1].trim())
  29. tokens.push(`_s(${exp})`)
  30. rawTokens.push({ '@binding': exp })
  31. lastIndex = index + match[0].length
  32. }
  33. if (lastIndex < text.length) {
  34. rawTokens.push(tokenValue = text.slice(lastIndex))
  35. tokens.push(JSON.stringify(tokenValue))
  36. }
  37. return {
  38. expression: tokens.join('+'),
  39. tokens: rawTokens
  40. }
  41. }

parseText 首先根据分隔符(默认是 {{}})构造了文本匹配的正则表达式,然后再循环匹配文本,遇到普通文本就 push 到 rawTokenstokens 中,如果是表达式就转换成 _s(${exp}) push 到 tokens 中,以及转换成 {@binding:exp} push 到 rawTokens 中。

对于我们的例子 :tokens 就是 [_s(item),'":"',_s(index)]rawTokens 就是 [{'@binding':'item'},':',{'@binding':'index'}]。那么返回的对象如下:

  1. return {
  2. expression: '_s(item)+":"+_s(index)',
  3. tokens: [{'@binding':'item'},':',{'@binding':'index'}]
  4. }

流程图

parse - 图4

总结

那么至此,parse 的过程就分析完了,看似复杂,但我们可以抛开细节理清它的整体流程。parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。其实这里我觉得源码写的不够友好,这种是典型的魔术数字,如果转换成用常量表达会更利于源码阅读。

当 AST 树构造完毕,下一步就是 optimize 优化这颗树。

原文: https://ustbhuangyi.github.io/vue-analysis/compile/parse.html