解析模板,生成 AST 的过程 parse 的目的 把 template 解析成 AST,AST 是一个对象,parse 过程包括,从 option 中取参数,parseHTML,retrun rootElement parseHTML 对 template 进行正则匹配,每匹配到一个标签或属性都会 advance,并剔除已经匹配的字符串,在匹配的过程中,解析出开始标签,闭合标签,属性(attrs…

解析模板,生成 AST 的过程

parse 的目的

  1. 把 template 解析成 AST,AST 是一个对象,parse 过程包括,从 option 中取参数,parseHTML,retrun rootElement
  2. parseHTML 对 template 进行正则匹配,每匹配到一个标签或属性都会 advance,并剔除已经匹配的字符串,在匹配的过程中,解析出开始标签,闭合标签,属性(attrs,key,value 形式),文本,扩展解析内容,比如标记 type,parent 指向,拿到 v-if v-for,根据分隔符{{}}取出表达式等,一直到 template 的为空串,最终返回一个 ROOT(根 ast 节点)
  3. AST 有三种类型,type 为 1 的是普通元素,type 为 2 是表达式,type3 是纯文本

整体流程

  1. parse 定义
// src/compiler/parser/index.js
// 接收两个参数,template是模板字符串,options是平台相关的参数
export function parse(template: string, options: CompilerOptions): ASTElement | void {
  // 从 options 中获取方法和配置
  getFnsAndConfigFromOptions(options)
  /*
    // 从 options 中获取方法和配置
    warn = options.warn || baseWarn
    platformIsPreTag = options.isPreTag || no
    platformMustUseProp = options.mustUseProp || no
    platformGetTagNamespace = options.getTagNamespace || no
    transforms = pluckModuleFunction(options.modules, 'transformNode')
    preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
    postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
  */

  delimiters = options.delimiters
  parseHTML(template, {
    start(tag, attrs, unary) {
      // 创建ast
      // 每一个ast都是一个js对象,有type 表示 AST 元素类型,tag 表示标签名,attrsList 表示属性列表,attrsMap 表示属性映射表,parent 表示父的 AST 元素,children 表示子 AST 元素集合
      let element = createASTElement(tag, attrs)
      // 处理ast:扩展ast属性
      // 拿到v-for指定,解析出for,alias等属性到ast对象上
      // 拿到v-if v-else v-else-if指令等
      processElement(element)
      // ast管理
      // 如果有currentParent,则把ast push到currentParent.children中
      // 如果当前不是一个一元标签,需要把ast push到stack中,并把ast元素赋给currentParent
      treeManagement()
    },

    end() {
      // 处理了尾部空格问题,把stack弹出一个出栈,把stack最后一个元素赋给currentParent,保证了遇到闭合标签,正确更新stack
      treeManagement()
      // 更新状态
      closeElement()
    },
    // 处理文本内容,多个表达式中间的字符串
    // 根据分隔符{{}},匹配到表达式
    chars(text: string) {
      handleText()
      createChildrenASTOfText()
    },
    comment(text: string) {
      createChildrenASTOfComment()
    }
  })
  // parseHTML伪代码
  /*
    // 循环解析template,用正则匹配,直到整个template被解析完毕,匹配过程中利用advance不断前进整个模板字符串,直到字符串末尾
    export function parseHTML (html, options) {
      let lastTag
      while (html) {
        if (!lastTag || !isPlainTextElement(lastTag)){
          let textEnd = html.indexOf('<')
          if (textEnd === 0) {
            if(matchComment) {
              advance(commentLength)
              continue
            }
            if(matchDoctype) {
              advance(doctypeLength)
              continue
            }
            if(matchEndTag) {
              advance(endTagLength)
              parseEndTag()
              continue
            }
            if(matchStartTag) {
              parseStartTag()
              handleStartTag()
              continue
            }
          }
          handleText()
          advance(textLength)
        } else {
          handlePlainTextElement()
          parseEndTag()
        }
      }
    }
  */
  return astRootElement
}

options:配置参数

// src/platforms/web/compiler/options
import { isPreTag, mustUseProp, isReservedTag, getTagNamespace } from '../util/index'

import modules from './modules/index'
import directives from './directives/index'
import { genStaticKeys } from 'shared/util'
import { isUnaryTag, canBeLeftOpenTag } from './util'

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

解析过程中的正则

// 匹配到注释节点,文档类型节点,开闭合标签等
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/

解析过程

  1. advance 函数
function advance(n) {
  index += n
  html = html.substring(n)
}
  1. 注释节点的解析
if (comment.test(html)) {
  const commentEnd = html.indexOf('-->')

  if (commentEnd >= 0) {
    if (options.shouldKeepComment) {
      options.comment(html.substring(4, commentEnd))
    }
    advance(commentEnd + 3)
    continue
  }
}
if (conditionalComment.test(html)) {
  const conditionalEnd = html.indexOf(']>')

  if (conditionalEnd >= 0) {
    advance(conditionalEnd + 2)
    continue
  }
}
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
  advance(doctypeMatch[0].length)
  continue
}
  1. 开始标签的解析
const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(lastTag, html)) {
    advance(1)
  }
  continue
}

首先通过 parseStartTag 解析开始标签

function parseStartTag() {
  // 先通过正则startTagOpen,匹配到开始标签,
  const start = html.match(startTagOpen)
  if (start) {
    // 定义match对象
    const match = {
      tagName: start[1],
      attrs: [],
      start: index
    }
    advance(start[0].length)
    let end, attr
    // 循环匹配开始标签中的属性,并添加到match.attrs中,直到匹配的开始标签闭合符结束
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
      advance(attr[0].length)
      match.attrs.push(attr)
    }
    // 如果匹配到闭合符,获取一元斜线符,前进到闭合符尾,并把当前索引赋给match.end
    if (end) {
      match.unarySlash = end[1]
      advance(end[0].length)
      match.end = index
      return match
    }
  }
}

再通过 handleStartTag 对 match 做处理

function handleStartTag(match) {
  const tagName = match.tagName
  const unarySlash = match.unarySlash

  if (expectHTML) {
    if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
      parseEndTag(lastTag)
    }
    if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
      parseEndTag(tagName)
    }
  }
  // 先判断开始标签是否一元标签,类似img,br
  const unary = isUnaryTag(tagName) || !!unarySlash
  // 对match.attrs进行处理
  const l = match.attrs.length
  const attrs = new Array(l)
  for (let i = 0; i < l; i++) {
    const args = match.attrs[i]
    if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
      if (args[3] === '') {
        delete args[3]
      }
      if (args[4] === '') {
        delete args[4]
      }
      if (args[5] === '') {
        delete args[5]
      }
    }
    const value = args[3] || args[4] || args[5] || ''
    const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines
    attrs[i] = {
      name: args[1],
      value: decodeAttr(value, shouldDecodeNewlines)
    }
  }
  // 如果不是一元标签,往stack push一个对象,并把tagName赋值给lastTag
  if (!unary) {
    stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
    lastTag = tagName
  }

  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}
  1. 闭合标签的解析
// 先通过endTag匹配到闭合标签
const endTagMatch = html.match(endTag)
// 前进到标签末尾,执行parseEndTag
if (endTagMatch) {
  const curIndex = index
  advance(endTagMatch[0].length)
  parseEndTag(endTagMatch[1], curIndex, index)
  continue
}

parseEndTag

// handleStartTag压栈,parseEndTag出栈,且需要保证第一个和前端ending是正常的匹配,如<div><span></span></div>
// 如果是<div><span></div>,把<span>弹出,
function parseEndTag(tagName, start, end) {
  let pos, lowerCasedTagName
  if (start == null) start = index
  if (end == null) end = index

  if (tagName) {
    lowerCasedTagName = tagName.toLowerCase()
  }

  if (tagName) {
    for (pos = stack.length - 1; pos >= 0; pos--) {
      if (stack[pos].lowerCasedTag === lowerCasedTagName) {
        break
      }
    }
  } else {
    pos = 0
  }

  if (pos >= 0) {
    for (let i = stack.length - 1; i >= pos; i--) {
      if (process.env.NODE_ENV !== 'production' && (i > pos || !tagName) && options.warn) {
        options.warn(`tag <${stack[i].tag}> has no matching end tag.`)
      }
      if (options.end) {
        options.end(stack[i].tag, start, end)
      }
    }
    stack.length = pos
    lastTag = pos && stack[pos - 1].tag
  } else if (lowerCasedTagName === 'br') {
    if (options.start) {
      options.start(tagName, [], true, start, end)
    }
  } else if (lowerCasedTagName === 'p') {
    if (options.start) {
      options.start(tagName, [], false, start, end)
    }
    if (options.end) {
      options.end(tagName, start, end)
    }
  }
}
  1. 解析文字
let text, rest, next
// 如果textEnd大于等于0,则代表当前位置到textEnd都是文本,如果<是文本,就继续找文本结束的位置
if (textEnd >= 0) {
  rest = html.slice(textEnd)
  while (!endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest)) {
    next = rest.indexOf('<', 1)
    if (next < 0) break
    textEnd += next
    rest = html.slice(textEnd)
  }
  text = html.substring(0, textEnd)
  advance(textEnd)
}
// 如果textEnd小于0,则说明template解析完毕
if (textEnd < 0) {
  text = html
  html = ''
}
if (options.chars && text) {
  options.chars(text)
}

parseHTML 的配置参数

处理开始标签

  1. 创建 AST 元素
// 每一个ast都是一个js对象
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
  attrs = guardIESVGBug(attrs)
}

let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
  element.ns = ns
}

export function createASTElement(tag: string, attrs: Array<Attr>, parent: ASTElement | void): ASTElement {
  return {
    // 元素类型
    type: 1,
    // 标签名
    tag,
    // 属性列表
    attrsList: attrs,
    // 属性映射表
    attrsMap: makeAttrsMap(attrs),
    // 父ast
    parent,
    // 子ast
    children: []
  }
}
  1. 处理 ast 元素
// 扩展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.')
}

// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
  element = preTransforms[i](element, options) || element
}

if (!inVPre) {
  processPre(element)
  if (element.pre) {
    inVPre = true
  }
}
if (platformIsPreTag(element.tag)) {
  inPre = true
}
if (inVPre) {
  processRawAttrs(element)
} else if (!element.processed) {
  // 拿到v-for指定,解析出for,alias等属性到ast对象上
  processFor(element)
  // 拿到v-if v-else v-else-if指令等
  processIf(element)
  processOnce(element)
  // element-scope stuff
  processElement(element, options)
}
  1. ast 树管理
// 如果有currentParent,则把ast push到currentParent.children中
// 如果当前不是一个一元标签,需要把ast push到stack中,并把ast元素赋给currentParent
function checkRootConstraints(el) {
  if (process.env.NODE_ENV !== 'production') {
    if (el.tag === 'slot' || el.tag === 'template') {
      warnOnce(`Cannot use <${el.tag}> as component root element because it may ` + 'contain multiple nodes.')
    }
    if (el.attrsMap.hasOwnProperty('v-for')) {
      warnOnce('Cannot use v-for on stateful component root element because ' + 'it renders multiple elements.')
    }
  }
}

// tree management
if (!root) {
  root = element
  checkRootConstraints(root)
} else if (!stack.length) {
  // allow root elements with v-if, v-else-if and v-else
  if (root.if && (element.elseif || element.else)) {
    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.`)
  }
}
if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
    processIfConditions(element, currentParent)
  } else if (element.slotScope) {
    // scoped slot
    currentParent.plain = false
    const name = element.slotTarget || '"default"'
    ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  } else {
    currentParent.children.push(element)
    element.parent = currentParent
  }
}
if (!unary) {
  currentParent = element
  stack.push(element)
} else {
  closeElement(element)
}

处理闭合标签

  1. treeManagement
// 处理了尾部空格问题,把stack弹出一个出栈,把stack最后一个元素赋给currentParent,保证了遇到闭合标签,正切更新stack
// remove trailing whitespace
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
  element.children.pop()
}
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
  1. closeElement
// 更新状态
function closeElement(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)
  }
}

暂无评论