Skip to content

Vue源码解析(三)AST的生成过程 #15

@jmx164491960

Description

@jmx164491960

前言

模板渲染离不开对性能的要求。Vue2.0引入了Diff的概念,通过一个js对象去储存DOM的节点信息。然后每次渲染的时候先比较一下这个js对象,来达到优化的目的。而Diff的前提就是AST(抽象语法树,abstract syntax code)

源码解析

版本:vue-2.5.17

生成AST的位置:
src\platforms\web\entry-runtime-with-compiler.js

  const { render, staticRenderFns } = compileToFunctions(template, {
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
  }, this)
  options.render = render
  options.staticRenderFns = staticRenderFns

调用了方法compileToFunctions,而方法的定义:

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

最终由方法parse生成ast:

const ast = parse(template.trim(), options)

下面来解析这个parse:

parse、parseHTML

路径:src\compiler\parser\index.js

parse里主要是对一些工具方法做了定义,其核心逻辑还是调用parseHTML方法。所以我们先看parseHTML:
parseHTML方法主要作用是对一段template做分解工作。把里面的节点(div等节点,空格,注释)逐一解析。其用的是while:

while (html) {
    last = html
    
    // ...处理完一个html片段后,执行advance截断。再处理下一个片段
}

function advance (n) {
    index += n
    html = html.substring(n)
}

里面解析的流程,
1.先判断是不是最后一段tag,或者是isPlainTextElement: script,style,textarea

先用正则匹配出头部直到下一个'<'符号的内容,然后判断其是一段内容,还是一个tag,通过判断头部是不是有'<'符号

    let textEnd = html.indexOf('<')
    if (textEnd === 0)

如果是,然后依照以下顺序判断:

// Comment:
if (comment.test(html)) {
  const commentEnd = html.indexOf('-->')

  if (commentEnd >= 0) {
    if (options.shouldKeepComment) {
      options.comment(html.substring(4, commentEnd))
    }
    advance(commentEnd + 3)
    continue
  }
}

// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
  const conditionalEnd = html.indexOf(']>')

  if (conditionalEnd >= 0) {
    advance(conditionalEnd + 2)
    continue
  }
}

// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
  advance(doctypeMatch[0].length)
  continue
}

// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
  const curIndex = index
  advance(endTagMatch[0].length)
  parseEndTag(endTagMatch[1], curIndex, index)
  continue
}

// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    advance(1)
  }
  continue
}
  1. 是否是注释
  2. 是否是Doctype
  3. 是否是结束的tag
  4. 是否是开始的tag
  5. 如果不是,则判定其是一段内容,比如一段字符串'hello world',使用options.chars方法处理:
  let text, rest, next
  if (textEnd >= 0) {
    rest = html.slice(textEnd)
    while (
      !endTag.test(rest) &&
      !startTagOpen.test(rest) &&
      !comment.test(rest) &&
      !conditionalComment.test(rest)
    ) {
      // < in plain text, be forgiving and treat it as text
      next = rest.indexOf('<', 1)
      if (next < 0) break
      textEnd += next
      rest = html.slice(textEnd)
    }
    text = html.substring(0, textEnd)
    advance(textEnd)
  }

  if (textEnd < 0) {
    text = html
    html = ''
  }

  if (options.chars && text) {
    options.chars(text)
  }

这五步,我认为比较重要的是345,所以下面着重解析这部分源码。12两点只要看一看源码基本都可以明白。

解析tag start

const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
function parseStartTag () {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      advance(start[0].length)
      let end, attr
      // 没匹配到下一个开头,并且有attr的
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push(attr)
      }
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
}

// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    advance(1)
  }
  continue
}

解析Tag头的时候,关键的一些操作:

  1. tag类型,比如div还是span
  2. attr参数,比如id="app" class="app"
  3. 匹配的开头和结尾
  4. 是否一元标签(unarySlash ),比如input,img这些就是一元标签,他们可以不需要去闭合
  5. 把节点的信息存储在一个数组stack里,用于等所有节点解析后,按顺序的渲染
  6. 执行options.start,该方法在parse定义

options.start

关键逻辑:

为第一个节点设置为根节点

// tree management
if (!root) {
    root = element
    checkRootConstraints(root)
} else if (!stack.length) {

判断当前tag时候是否有父节点,把节点挂载在父节点上:

// 里面做了一些特殊判断,比如判断是否slot节点,这些部分不做详细解析了
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
    }
  }

判断当前tag是否元节点:

如果不是,把当前节点设置为父节点,这是为了解析下一个子节点时,可以让子节点挂载到父节点。并加入到stack数组

解析tag end

// End tag:
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const endTagMatch = html.match(endTag)
if (endTagMatch) {
  const curIndex = index
  advance(endTagMatch[0].length)
  parseEndTag(endTagMatch[1], curIndex, index)
  continue
}

function parseEndTag (tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index

    // Find the closest opened tag of the same type
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break
        }
      }
    } else {
      // If no tag name is provided, clean shop
      pos = 0
    }

    if (pos >= 0) {
      // Close all the open elements, up the stack
      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)
        }
      }

      // Remove the open elements from the stack
      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)
      }
    }
  }

关键操作:
从stack数组尾部开始遍历,寻找能对应tag尾的tag头。找到以后执行options.end方法,下面我们来解析options.end:

  1. 获取stack数组尾部数第一个节点,判断这个节点的最后一个子节点,判断是否需要children.pop()
  2. stack.length减1
  3. 更新currentParent

解析chars

假如我们的template是<div>hello world</div>,那么hello world就是这段template的chars。

遇到chars解析是这样的:
执行options.chars,
对text进行格式化:

text = inPre || text.trim()
? isTextTag(currentParent) ? text : decodeHTMLCached(text)

使用方法parseText对text进行解析,主要是用于解析节点里的内容。把{{ xx }}节点为一个render函数使用的表达式:

// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })

##总结

自此生成AST的过程已经解析完成,里面过程复杂,如有错漏,请大家多多指教。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions