• parse
    • 整体流程
      • 从 options 中获取方法和配置
      • 解析 HTML 模板
      • 处理开始标签
      • 处理闭合标签
      • 处理文本内容
    • 流程图
    • 总结

    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