从 vue 源码看问题 —— vue 编译器的解析

前言

Vue 编译器主要处理内容:

  • 将组件的 html 模版解析成 AST 对象
  • 优化
    • 通过遍历 AST 对象,为每个节点做 静态标记,通过标记其是否为静态节点,然后进一步标记出 静态根节点,方便在后续更新过程中跳过这些静态节点
    • 标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
  • AST 生成运行渲染函数
    • render 函数
    • staticRenderFns 数组,里面保存了所有的 静态节点的渲染函数

深入源码

编译器入口 —— Vue.prototype.$mount

文件位置:src\platforms\web\entry-runtime-with-compiler.js

这里重点在于获取动态渲染函数 render 函数和静态渲染函数 staticRenderFnscompileToFunctions 方法.

// 保存原来的 Vue.prototype.$mount 方法
const mount = Vue.prototype.$mount

/*
  重写 Vue.prototype.$mount
  问题:当一个配置项中存在 el、template、render 选项时,它们的优先级是怎样的?
  回答:源码中从上到下的处理顺序,决定了它们的优先级为:render > template > el
*/ 
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {

  /*
    el 有值,则通过 query 方法获取对应的 dom 元素
     1. el 是 string,则通过 document.querySelector(el) 获取 dom 元素
       - 获取到 dom 元素就直接返回 dom
       - 无法获取到 dom 元素就进行警告提示,并返回 document.createElement('div') 
     2. el 不是 string,则直接返回 el 本身
  */ 
  el = el && query(el)

  /* istanbul ignore if */
  // el 不能是 body 元素 和 html 元素
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  //  获取配置选项
  const options = this.$options
  // resolve template/el and convert to render function
  // 当前配置选项中不存在 render 选项
  if (!options.render) {
    // 获取 template 模板
    let template = options.template

    // template 存在
    if (template) {
      // template 为 string
      if (typeof template === 'string') {
        // 字符串以 # 开头,代表是 id 选择器
        if (template.charAt(0) === '#') {
          // 获取 dom 元素对应的 innerHtml 字符内容
          template = idToTemplate(template)
          /* istanbul ignore if */
          // template 选项不能为空字符串
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        // 代表是一个 dom 元素,取出 dom 元素的 innerHTML 内容
        template = template.innerHTML
      } else {
        // 其他类型则不属于有效的 template 选项
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // template 不存在,直接使用 el 对应的 dom 元素作为 template 模板 
      template = getOuterHTML(el)
    }

    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      // 获取对应的动态渲染函数 render 函数和静态渲染函数 staticRenderFns 
      const { render, staticRenderFns } = compileToFunctions(template, {
        // 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        // 界定符,默认 {{}}
        delimiters: options.delimiters,
        // 是否保留注释
        comments: options.comments
      }, this)
      // 将 render 和 staticRenderFns 分别保存到配置选项上
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }

  // 通过调用前面保存 mount 方法
  return mount.call(this, el, hydrating)
}
复制代码

compileToFunctions() 方法

文件位置:src\compiler\to-function.js

这里的重点是 createCompileToFunctionFn 方法的入参 compile 函数.

/* 
    1、如果缓存中有编译结果,直接返回缓存的编译内容
    2、执行编译函数 compile,得到编译结果 compiled 
    3、处理编译期间出现的所有 error 和 tip,分别输出到控制台 
    4、将编译得到的字符串代码通过 new Function(codeStr) 转换成可执行的函数
       即 动态渲染函数 render 和 静态渲染函数 staticRenderFns
    5、缓存编译结果
*/
export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    // 模板字符串
    template: string,
    // 编译选项
    options?: CompilerOptions,
    // 组件实例
    vm?: Component
  ): CompiledFunctionResult {
    // 复制配置选项
    options = extend({}, options)
    // 日志
    const warn = options.warn || baseWarn
    delete options.warn

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }

    // 定义缓存对应的 key
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template

    // 如果缓存中有编译结果,直接获取缓存的内容
    if (cache[key]) {
      return cache[key]
    }

    // 通过执行 compile 编译函数,得到编译结果
    const compiled = compile(template, options)

    // 检查编译结果中所有的 errors 和 tips,并输出到控制台
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        if (options.outputSourceRange) {
          compiled.errors.forEach(e => {
            warn(
              `Error compiling template:\n\n${e.msg}\n\n` +
              generateCodeFrame(template, e.start, e.end),
              vm
            )
          })
        } else {
          warn(
            `Error compiling template:\n\n${template}\n\n` +
            compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
            vm
          )
        }
      }
      if (compiled.tips && compiled.tips.length) {
        if (options.outputSourceRange) {
          compiled.tips.forEach(e => tip(e.msg, vm))
        } else {
          compiled.tips.forEach(msg => tip(msg, vm))
        }
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []
    /* 
     编译结果中 compiled.render 是一个可执行函数的字符串形式
     需要通过 createFunction 方法将 compiled.render 字符串变成一个真正可执行的函数
     本质就是通过 new Function(code) 的形式将字符串转换成函数
    */
    // 动态渲染函数
    res.render = createFunction(compiled.render, fnGenErrors)
    // 静态渲染函数
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }

    // 缓存编译结果
    return (cache[key] = res)
  }
}
复制代码

compile() 方法

文件位置:src\compiler\create-compiler.js

这里的中调就是调用核心编译函数 baseCompile,传递模版字符串和最终的编译选项,得到编译结果.

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    /* 
     编译函数:
      1、选项合并,将 options 配置项合并到 finalOptions(baseOptions) 中,
         得到最终的编译配置对象
      2、调用核心编译器 baseCompile 得到编译结果
      3、将编译期间产生的 error 和 tip 挂载到编译结果上
      4、返回编译结果
     */
    function compile (
      // 模板字符串
      template: string,
      // 编译选项
      options?: CompilerOptions
    ): CompiledResult {
      // 以平台特有的编译配置为原型,创建编译选项对象
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      // 日志,负责记录 error 和 tip
      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

     // 如果存在编译选项,合并 options 和 baseOptions
      if (options) {
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/)[0].length

         // 增强 日志 方法
          warn = (msg, range, tip) => {
            const data: WarningMessage = { msg }
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            (tip ? tips : errors).push(data)
          }
        }

        // 合并自定义 modules 到 finalOptions 中
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }

        // 合并自定义 directives 到 finalOptions 中
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // 除了 modules 和 directives,将其它配置项拷贝到 finalOptions 中
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn

     // 调用核心编译函数 baseCompile,传递模版字符串和最终的编译选项,得到编译结果
      const compiled = baseCompile(template.trim(), finalOptions)

      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }

      // 将编译期间产生的错误和提示挂载到编译结果上
      compiled.errors = errors
      compiled.tips = tips

      // 返回编译结果
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
复制代码

baseOptions 配置

文件位置:src\platforms\web\compiler\options.js

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  // 负责 class、style、v-model
  modules,
  // 指令
  directives,
  // pre 标签
  isPreTag,
  // 是否是一元标签
  isUnaryTag,
  // 必须用于 props 的属性
  mustUseProp,
  // 只有开始标签的标签
  canBeLeftOpenTag,
  // 保留标签
  isReservedTag,
  // 命名空间
  getTagNamespace,
  // 静态 key
  staticKeys: genStaticKeys(modules)
}
复制代码

baseCompile() 方法

文件位置:src\compiler\index.js

这里的重点就是通过 parse 方法将 html 模版字符串解析成 ast.

/*
  在这之前做的所有的事情,只是为了构建平台特有的编译选项(options),比如 web 平台
  1、将 html 模版字符串解析成 ast
  2、对 ast 树进行静态标记
  3、将 ast 生成渲染函数
     - 静态渲染函数放到 code.staticRenderFns 数组中
     - 动态渲染函数 code.render
     - 在将来渲染时执行渲染函数能够得到 vnode
 */
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  /* 
   将模版字符串解析为 AST 语法树
   每个节点的 ast 对象上都设置了元素的所有信息,如,标签信息、属性信息、插槽信息、父节点、子节点等
  */
  const ast = parse(template.trim(), options)

  /*
   优化,遍历 AST,为每个节点做静态标记
     - 标记每个节点是否为静态节点,,保证在后续更新中跳过这些静态节点
     - 标记出静态根节点,用于生成渲染函数阶段,生成静态根节点的渲染函数
 */
  if (options.optimize !== false) {
    optimize(ast, options)
  }

  /*
    从 AST 语法树生成渲染函数
    如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)"
  */
  const code = generate(ast, options)

  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
复制代码

parse() 方法

文件位置:src\compiler\parser\index.js

这里 parse 方法中定义了很多方法,这些方法是在parseHTMLOptions 中有使用到,因此在这里不提前做解读,同时也意味着这里的重点就在于 parseHTML(template, parseHTMLOptions) 方法.

/**
 * 将 HTML 字符串转换为 AST
 */
export function parse (
  // 模板字符串
  template: string,
  // 编译选项
  options: CompilerOptions
): ASTElement | void {
  // 日志 
  warn = options.warn || baseWarn

  // 是否为 pre 标签
  platformIsPreTag = options.isPreTag || no
  // 必须使用 props 进行绑定的属性
  platformMustUseProp = options.mustUseProp || no
  // 是否为命名空间
  platformGetTagNamespace = options.getTagNamespace || no
  // 是否为保留标签(html + svg)
  const isReservedTag = options.isReservedTag || no
  // 判断一个元素是否为一个组件
  maybeComponent = (el: ASTElement) => !!(
    el.component ||
    el.attrsMap[':is'] ||
    el.attrsMap['v-bind:is'] ||
    !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
  )

  /*
     分别获取 options.modules 中的 transformNode、preTransformNode、postTransformNode 方法
     负责处理元素节点上的 class、style、v-model
  */ 
  transforms = pluckModuleFunction(options.modules, 'transformNode')
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

  // 界定符,比如: {{}}
  delimiters = options.delimiters

  // 存放解析的中间结果
  const stack = []
  // 是否保留空白
  const preserveWhitespace = options.preserveWhitespace !== false
  // 获取空白选项
  const whitespaceOption = options.whitespace

  // 根节点 root,之后处理的节点都会按照层级挂载到 root 下,最后 return 得到的就是 root,也就是 ast 语法树
  let root
  // 当前元素的父元素
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false
  
  function warnOnce (msg, range){xxx}
  function closeElement (element){xxx}
  function trimEndingWhitespace (el){xxx}
  function checkRootConstraints (el){xxx}
  
  // 解析 html 模版字符串,处理所有标签以及标签上的属性,
  // 这里 parseHTMLOptions 在后面处理过程中用到,再进一步解析
  parseHTML(template, parseHTMLOptions);
  
  // 返回生成的 ast 对象 
  return root
 }
复制代码

parseHTML() 方法

文件位置:src\compiler\parser\html-parser.js

parseHTML 方法中主要涉及到了以下几个方法:

  • advance
  • parseStartTag
  • handleStartTag
  • parseEndTag

export function parseHTML(html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  // 是否是自闭合标签
  const isUnaryTag = options.isUnaryTag || no
  // 是否可以只有开始标签
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no

  // 记录当前在原始 html 字符串中的开始位置
  let index = 0
  let last, lastTag

  while (html) {
    last = html
    // 确保不会在 script、style、textarea 这样的纯文本内容元素中
    if (!lastTag || !isPlainTextElement(lastTag)) {
      // 找第一个 < 字符
      let textEnd = html.indexOf('<')

      /* 
         textEnd === 0 说明在开头找到了
         分别处理可能找到的注释标签、条件注释标签、Doctype、开始标签、结束标签
         每处理完一种情况,就会截断(continue)循环,并且重置 html 字符串,将处理过的标签截掉,下一次循环处理剩余的 html 字符串模版
      */

      if (textEnd === 0) {
        // 处理注释标签,如:<!-- xxx -->
        if (comment.test(html)) {
          // 找到注释节点结束标签的索引
          const commentEnd = html.indexOf('-->')

          if (commentEnd >= 0) {
            // 是否保留注释节点
            if (options.shouldKeepComment) {
              // options.comment(注释内容, 注释的开始索引, 结束索引)
              options.comment(
                html.substring(4, commentEnd),
                index,
                index + commentEnd + 3,
              )
            }
            // 调整 html 和 index 变量
            advance(commentEnd + 3)
            continue
          }
        }

        /*
           处理条件注释标签:<!--[if IE]>
           http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        */

        if (conditionalComment.test(html)) {
          // 获取条件注释的结束索引
          const conditionalEnd = html.indexOf(']>')

          if (conditionalEnd >= 0) {
            // 调整 html 和 index 变量
            advance(conditionalEnd + 2)
            continue
          }
        }

        // 处理 Doctype ,规则为 /^<!DOCTYPE [^>]+>/i 如:<!DOCTYPE html>
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        // End tag: 处理结束标签,如 </div>
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          // 处理结束标签
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // Start tag: 处理开始标签,比如处理 <div></div>
        // startTagMatch = { tagName: 'div', attrs: [[xx], ...], start: index }
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          // 进一步处理上一步得到结果,并最后调用 options.start 方法
          // 真正的解析工作都是在这个 start 方法中做的
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
      }

      let text, rest, next
      if (textEnd >= 0) {
        /* 
          能走到这儿,说明虽然在 html 中匹配到到了 <xx,但是又不属于上述的几种情况,
          它就只是一段普通文本,如:<文本内容
          于是从 html 中找到下一个 <,直到 <xxx 是上述几种情况的标签,则结束
          在这整个过程中会一直调整 textEnd 的值,作为 html 中下一个有效标签的开始位置
          截取 html 模版字符串中 textEnd 之后的内容,rest = <xx
        */
        rest = html.slice(textEnd)

        // 当前 while 循环就是处理 <xx 之后的纯文本情况
        // 截取文本内容,并找到有效标签的开始位置(textEnd)
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // 认为 < 后面的内容为纯文本,然后在这些纯文本中再次找 <
          next = rest.indexOf('<', 1)
          // 如果没找到 <,则直接结束循环
          if (next < 0) break
          // 走到这儿说明在后续的字符串中找到了 <,索引位置为 textEnd
          textEnd += next
          // 截取 html 字符串模版 textEnd 之后的内容赋值给 rest,继续判断之后的字符串是否存在标签
          rest = html.slice(textEnd)
        }
        text = html.substring(0, textEnd)
      }

      // 如果 textEnd < 0,说明 html 中就没找到 <,那说明 html 就是一段文本
      if (textEnd < 0) {
        text = html
      }

      // 将文本内容从 html 模版字符串上截取掉
      if (text) {
        advance(text.length)
      }

      /* 
         处理文本
         基于文本生成 ast 对象,然后将该 ast 放到它的父元素中,即 currentParent.children 数组中
      */
      if (options.chars && text) {
        options.chars(text, index - text.length, index)
      }
    } else {
      let endTagLength = 0
      // 处理 script、style、textarea 标签的闭合标签
      const stackedTag = lastTag.toLowerCase()
      // 开始标签的小写形式
      const reStackedTag =
        reCache[stackedTag] ||
        (reCache[stackedTag] = new RegExp(
          '([\\s\\S]*?)(</' + stackedTag + '[^>]*>)',
          'i',
        ))
      // 匹配并处理开始标签和结束标签之间的所有文本,比如 <script>xx</script>
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        endTagLength = endTag.length
        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
          text = text
            .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
            .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
        }
        if (shouldIgnoreFirstNewline(stackedTag, text)) {
          text = text.slice(1)
        }
        if (options.chars) {
          options.chars(text)
        }
        return ''
      })
      index += html.length - rest.length
      html = rest
      parseEndTag(stackedTag, index - endTagLength, index)
    }

    // 正常要处理结束,如果 stack 数组中还有内容,则说明有标签没有被闭合,给出提示信息
    if (html === last) {
      options.chars && options.chars(html)
      if (
        process.env.NODE_ENV !== 'production' &&
        !stack.length &&
        options.warn
      ) {
        options.warn(`Mal-formatted tag at end of template: "${html}"`, {
          start: index + html.length,
        })
      }
      break
    }
  }

  // Clean up any remaining tags
  parseEndTag()

  /*
   重置 html,html = 从索引 n 位置开始的向后的所有字符
   index 为 html 在原始的模版字符串中的结束索引,也是下一次该处理的字符的开始位置
 */
  function advance(n) {
    index += n
    html = html.substring(n)
  }

  // 处理开始标签
  function parseStartTag() {
    // 匹配开始标签,规则为 /^<${qnameCapture}/
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1], // 标签名
        attrs: [], // 属性,占位符
        start: index, // 标签的开始位置
      }

      /*
        调整 html 和 index,比如当前匹配到的开始标签内容为 start[0] = '<div',则:
        1. html = ' id="app">'
        2. index = start[0].length
     */
      advance(start[0].length)

      let end, attr
      // 处理开始标签上的各个属性,并将这些属性放到 match.attrs 数组中
      while (
        !(end = html.match(startTagClose)) &&
        (attr = html.match(dynamicArgAttribute) || html.match(attribute))
      ) {
        attr.start = index
        advance(attr[0].length)
        attr.end = index
        match.attrs.push(attr)
      }
      // 开始标签的结束,end = '>' 或 end = ' />'
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }

  /*
   进一步处理开始标签的解析结果 ——— match 对象
   1. 处理属性 match.attrs,如果不是自闭合标签,则将标签信息放到 stack 数组,
      待将来处理到它的闭合标签时再将其弹出 stack,表示该标签处理完毕,此时标签的所有信息都在 element ast 对象上
   2. 接下来调用 options.start 方法处理标签,并根据标签信息生成 element ast,
      以及处理开始标签上的属性和指令,最后将 element ast 放入 stack 数组
  
    match = { tagName: 'div', attrs: [[xx], ...], start: index }
  */
  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)
      }
    }

    // 根据 标签名 或 一元斜线 unarySlash 判断是否是自闭合标签,比如 <hr />
    const unary = isUnaryTag(tagName) || !!unarySlash

    const l = match.attrs.length
    const attrs = new Array(l)

    // 处理 match.attrs,得到 attrs = [{ name: attrName, value: attrVal, start: xx, end: xx }, ...]
    // 比如处理 <div id="app"></div> ,则 attrs = [{ name: 'id', value: 'app', start: xx, end: xx }, ...]
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      // 比如:args[3] => 'id',args[4] => '=',args[5] => 'app'
      const value = args[3] || args[4] || args[5] || ''
      const shouldDecodeNewlines =
        tagName === 'a' && args[1] === 'href'
          ? options.shouldDecodeNewlinesForHref
          : options.shouldDecodeNewlines
      // attrs[i] = { id: 'app' }
      attrs[i] = {
        name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines),
      }
      // 非生产环境,记录属性的开始和结束索引
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        attrs[i].start = args.start + args[0].match(/^\s*/).length
        attrs[i].end = args.end
      }
    }

    /*
      1. 如果不是自闭合标签,则将标签信息放到 stack 数组中,待将来处理到它的闭合标签时再将其弹出 stack
      2. 如果是自闭合标签,则标签信息就没必要进入 stack 了,直接处理众多属性,
         将它们都设置到 element ast 对象上,就可以跳过处理结束标签的过程,这一步在处理开始标签的过程中就会进行
    */

    if (!unary) {
      // 将标签信息放到 stack 数组中,{ tag, lowerCasedTag, attrs, start, end }
      stack.push({
        tag: tagName,
        lowerCasedTag: tagName.toLowerCase(),
        attrs: attrs,
        start: match.start,
        end: match.end,
      })
      // 标识当前标签的结束标签为 tagName
      lastTag = tagName
    }

    /*
    调用 start 方法,主要内容为:
      1、创建 AST 对象
      2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况
      3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once
      4、如果根节点 root 不存在则设置当前元素为根节点
      5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,并记录 currentParent,
         在接下来处理子元素时用来告诉子元素自己的父节点是谁
      6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素
   */
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  /*
   解析结束标签,比如:</div>
   最主要的事就是:
     1、处理 stack 数组,从 stack 数组中找到当前结束标签对应的开始标签,然后调用 options.end 方法
     2、处理完结束标签之后调整 stack 数组,保证在正常情况下 stack 数组中的最后一个元素就是下一个结束标签对应的开始标签
     3、处理一些异常情况,比如 stack 数组最后一个元素不是当前结束标签对应的开始标签,
        还有就是 br 和 p 标签单独处理
    
    tagName —— 标签名,比如 div
    start —— 结束标签的开始索引
    end —— 结束标签的结束索引
 */
  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
    /* 
      倒序遍历 stack 数组,找到第一个和当前结束标签相同的标签,该标签就是结束标签对应的开始标签的描述对象
      正常情况下,stack 数组中的最后一个元素就是当前结束标签的开始标签的描述对象
    */ 
    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
    }

     // 如果在 stack 中一直没有找到相同的标签名,则 pos 就会 < 0,进行后面的 else 分支
    if (pos >= 0) {
    // Close all the open elements, up the stack
    /* 
       这个 for 循环负责关闭 stack 数组中索引 >= pos 的所有标签
       为什么要用一个循环,上面说到正常情况下 stack 数组的最后一个元素就是需要找到的开始标签,
       但是有些异常情况,就是有些元素没有给提供结束标签,比如:
       stack = ['span', 'div', 'span', 'h1'],当前处理的结束标签 tagName = div
       匹配到 div,pos = 1,那索引为 2 和 3 的两个标签(span、h1)说明就没提供结束标签
       这个 for 循环就负责关闭 div、span 和 h1 这三个标签,
       并在开发环境为 span 和 h1 这两个标签给出 ”未匹配到结束标签的提示
   */ 
      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.`, {
            start: stack[i].start,
            end: stack[i].end,
          })
        }
        // 走到这里,说明上面的异常情况都处理完了,调用 options.end 处理正常的结束标签
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }

      // Remove the open elements from the stack
      // 将刚才处理的那些标签从数组中移除,保证数组的最后一个元素就是下一个结束标签对应的开始标签
      stack.length = pos
      // lastTag 记录 stack 数组中未处理的最后一个开始标签
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
       // 当前处理的标签为 <br /> 标签
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') {
      // 当前处理的标签为 <p></p> 标签
      if (options.start) {
        // 处理 <p> 标签
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        // 处理 </p> 标签
        options.end(tagName, start, end)
      }
    }
  }
}
复制代码

parseHtmlOptions — parseHTML(template, options)

文件位置:src\compiler\parser\index.js

这里主要解读 start、end、chars、comment 这 4 个方法.

 /*
    start 方法主要内容:
      1、创建 AST 对象
      2、处理存在 v-model 指令的 input 标签,分别处理 input 为 checkbox、radio、其它的情况
      3、处理标签上的众多指令,比如 v-pre、v-for、v-if、v-once
      4、如果根节点 root 不存在则设置当前元素为根节点
      5、如果当前元素为非自闭合标签则将自己 push 到 stack 数组,
         并记录 currentParent,在接下来处理子元素时用来告诉子元素自己的父节点是谁
      6、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,
         以及设置自己的子元素

      tag —— 标签名
      attrs —— [{ name: attrName, value: attrVal, start, end }, ...] 形式的属性数组
      unary —— 自闭合标签
      start —— 标签在 html 字符串中的开始索引
      end —— 标签在 html 字符串中的结束索引
   */
    start (tag, attrs, unary, start, end) {
      // 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)
      }
      
      // 创建当前标签的 AST 对象
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      // 设置元素的命名空间
      if (ns) {
        element.ns = ns
      }

      // 非生产环境下,在 ast 对象上添加一些属性,比如 start、end
      if (process.env.NODE_ENV !== 'production') {
        if (options.outputSourceRange) {
          element.start = start
          element.end = end
          /* 
           将属性数组解析成下面形式的对象:
           { attrName: { name: attrName, value: attrVal, start, end }, ... }
          */
          element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
            cumulated[attr.name] = attr
            return cumulated
          }, {})
        }
        // 验证属性是否有效,比如属性名不能包含: spaces, quotes, <, >, / or =.
        attrs.forEach(attr => {
          if (invalidAttributeRE.test(attr.name)) {
            warn(
              `Invalid dynamic argument expression: attribute names cannot contain ` +
              `spaces, quotes, <, >, / or =.`,
              {
                start: attr.start + attr.name.indexOf(`[`),
                end: attr.start + attr.name.length
              }
            )
          }
        })
      }

      /* 
       非服务端渲染,模版中不应该出现 style、script 标签: 
        可以通过 .vue 文件理解为:template、style、script 已经分离,
        因此正常情况下,template 模板中不应该包含 <style>、<script> 标签
       */
      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
      /*
       为 element 对象分别执行 class、style、model 模块中的 preTransforms 方法
       但是 web 平台只有 model 模块有 preTransforms 方法
       用来处理存在 v-model 的 input 标签,但没处理 v-model 属性
       分别处理了 input 为 checkbox、radio 和 其它的情况
       input 具体是哪种情况由 el.ifConditions 中的条件来判断
       <input v-mode="test" :type="checkbox || radio || other(如 text)" />
      */
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }

      if (!inVPre) {
        processPre(element)
        // 判断 element 是否存在 v-pre 指令,存在则设置 element.pre = true
        if (element.pre) {
          inVPre = true
        }
      }
      // 如果是 pre 标签,则设置 inPre 为 true
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) {
        /* 
          说明标签上存在 v-pre 指令,这个节点只会渲染一次,将节点上的属性都设置到
           el.attrs 数组对象中,作为静态属性,数据更新时不会渲染这部分内容

          设置 el.attrs 数组对象,每个元素都是一个属性对象如下: 
           { name: attrName, value: attrVal, start, end }
        */ 
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        // 处理 v-for 属性,得到 element.for = 可迭代对象 element.alias = 别名
        processFor(element)
         /*
          处理 v-if、v-else-if、v-else
          得到 element.if = "exp",element.elseif = exp, element.else = true
          v-if 属性会额外在 element.ifConditions 数组中添加 { exp, block } 对象
        */
        processIf(element)
        // 处理 v-once 指令,得到 element.once = true
        processOnce(element)
      }

      // 如果 root 不存在,则表示当前处理的元素为第一个元素,即组件的根元素
      if (!root) {
        root = element
        if (process.env.NODE_ENV !== 'production') {
          /* 
           检查根元素,对根元素有一些限制,比如:不能使用 slot 和 template 作为根元素,
           也不能在有状态组件的根元素上使用 v-for 指令
          */
          checkRootConstraints(root)
        }
      }

      // 非自闭合标签,通过 currentParent 记录当前元素,
      // 下一个元素在处理的时候,就知道自己的父元素是谁
      if (!unary) {
        currentParent = element
         /*
          然后将 element push 到 stack 数组,将来处理到当前元素的闭合标签时再拿出来
          将当前标签的 ast 对象 push 到 stack 数组中
          注意:在调用 options.start 方法之前也进行过 push 操作,
                那个 push 进来的是当前标签的一个基本配置信息
        */
        stack.push(element)
      } else {
      /*
       说明当前元素为自闭合标签,主要内容:
         1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性
         2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent
         3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中
     */
        closeElement(element)
      }
    },

    /* 处理结束标签 */
    end (tag, start, end) {
      // 结束标签对应的开始标签的 ast 对象
      const element = stack[stack.length - 1]
      // pop stack
      stack.length -= 1
      currentParent = stack[stack.length - 1]
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        element.end = end
      }
      /*
      主要内容:
        1、如果元素没有被处理过,即 el.processed 为 false,
           则调用 processElement 方法处理节点上的众多属性
        2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,
           并设置自己的 parent 属性为 currentParent
        3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中
     */
      closeElement(element)
    },

    /* 
     处理文本
     基于文本生成 ast 对象,然后将该 ast 放到它的父元素的肚子里,
     即 currentParent.children 数组中  
    */
    chars (text: string, start: number, end: number) {
       // 异常处理,currentParent 不存在说明这段文本没有父元素
      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

      // 对 text 进行一系列的处理,比如删除空白字符,
      // 或者存在 whitespaceOptions 选项,则 text 直接置为空或者空格
      if (inPre || text.trim()) {
        // 文本在 pre 标签内 或者 text.trim() 不为空
        text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
      } else if (!children.length) {
        // remove the whitespace-only node right after an opening tag
        /* 
         children 长度为 0 ,则说明文本不在 pre 标签内而且 text.trim() 为空,
         而且当前父元素也没有孩子节点,则将 text 置为空
        */
        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 ? ' ' : ''
      }

      // 如果经过处理后 text 还存在
      if (text) {
        // 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个连续空格压缩为单个
        if (!inPre && whitespaceOption === 'condense') {
          // condense consecutive whitespaces into single space
          text = text.replace(whitespaceRE, ' ')
        }

        // 基于 text 生成 AST 对象
        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
          }
        }

        // child 存在,则将 child 放到父元素的 children 中,即 currentParent.children 数组中
        if (child) {
          if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
            child.start = start
            child.end = end
          }
          children.push(child)
        }
      }
    },

    /* 注释内容-text, 注释开始索引-start, 结束索引-end */
    comment (text: string, start, end) {
      // adding anything as a sibling to the root node is forbidden
      // comments should still be allowed, but ignored
      // 禁止将任何内容作为 root 节点的同级进行添加,注释应该被允许,但是会被忽略

      // 如果 currentParent 不存在,说明注释和 root 为同级,则进行忽略
      if (currentParent) {
        // 注释节点的 ast
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // 分别将注释节点的开始索引和结束索引保存到注释节点 child 中
          child.start = start
          child.end = end
        }
        // 向父元素的 children 属性中添加当前注释节点
        currentParent.children.push(child)
      }
    }
复制代码

preTransformNode() 方法

文件位置:src\platforms\web\compiler\modules\model.js

这里涉及到了下面几个方法:

  • getBindingAttr
  • getAndRemoveAttr
  • processFor
  • addRawAttr
  • processElement
/*
* 处理存在 v-model 的 input 标签,但没处理 v-model 属性
* 分别处理了 input 为 checkbox、radio 和 其它的情况
* input 具体是哪种情况由 el.ifConditions 中的条件来判断
* <input v-mode="test" :type="checkbox || radio || other(比如 text)" />
* @param {*} el 
* @param {*} options 
* @returns branch0
*/
function preTransformNode (el: ASTElement, options: CompilerOptions) {
  // 属于 input 标签
  if (el.tag === 'input') {
    const map = el.attrsMap
    // 不存在 v-model 属性,直接结束
    if (!map['v-model']) {
      return
    }

    // 获取 :type 的值
    let typeBinding
    if (map[':type'] || map['v-bind:type']) {
      typeBinding = getBindingAttr(el, 'type')
    }
    if (!map.type && !typeBinding && map['v-bind']) {
      typeBinding = `(${map['v-bind']}).type`
    }

    // type 类型存在
    if (typeBinding) {
      // 获取 v-if 的值,比如: <input v-model="test" :type="checkbox" v-if="isShow" />
      const ifCondition = getAndRemoveAttr(el, 'v-if', true)
      // 得到 &&isShow
      const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``
      // 是否存在 v-else 属性,<input v-else />
      const hasElse = getAndRemoveAttr(el, 'v-else', true) != null
      // 获取 v-else-if 属性的值 <inpu v-else-if="isShow" />
      const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)

      /*
        克隆一个新的 el 对象,分别处理 input 为 chekbox、radio 或 其它的情况
        具体是哪种情况,通过 el.ifConditins 条件来判断
      */ 
      // 1. checkbox
      const branch0 = cloneASTElement(el)

      // process for on the main node
      /*
       <input v-for="item in arr" :key="item" />
       处理 v-for 表达式,得到:
        branch0.for = arr;
        branch0.alias = item;
      */
      processFor(branch0)
      // 在 branch0.attrsMap 和 branch0.attrsList 对象中添加 type 属性
      addRawAttr(branch0, 'type', 'checkbox')
      /* 
         分别处理元素节点:
          key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、
          其它指令和一些原生属性
      */   
      processElement(branch0, options)
      // 标记当前对象已经被处理过了
      branch0.processed = true // prevent it from double-processed
      // 得到 true&&isShow || false&&isShow,标记当前 input 是否为 checkbox
      branch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra
      // 在 branch0.ifConfitions 数组中放入 { exp, block } 对象
      addIfCondition(branch0, {
        exp: branch0.if,
        block: branch0
      })

      // 克隆一个新的 ast 对象
      // 2. add radio else-if condition
      const branch1 = cloneASTElement(el)
      // 获取 v-for 属性值
      getAndRemoveAttr(branch1, 'v-for', true)
      // 在 branch1.attrsMap 和 branch1.attrsList 对象中添加 type 属性
      addRawAttr(branch1, 'type', 'radio')
       /* 
         分别处理元素节点:
          key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、
          其它指令和一些原生属性
      */   
      processElement(branch1, options)
      addIfCondition(branch0, {
        exp: `(${typeBinding})==='radio'` + ifConditionExtra,
        block: branch1
      })

      // 3. other input 为其它的情况
      const branch2 = cloneASTElement(el)
      // 获取 v-for 属性
      getAndRemoveAttr(branch2, 'v-for', true)
      addRawAttr(branch2, ':type', typeBinding)  
      processElement(branch2, options)
      addIfCondition(branch0, {
        exp: ifCondition,
        block: branch2
      })

      // 给 branch0 设置 else 或 elseif 条件
      if (hasElse) {
        branch0.else = true
      } else if (elseIfCondition) {
        branch0.elseif = elseIfCondition
      }
      // 返回
      return branch0
    }
  }
}
复制代码

getBindingAttr、getAndRemoveAttr、addRawAttr 方法

文件位置:src\compiler\helpers.js

getBindingAttr

// 获取 el 对象上执行属性 name 的值 
export function getBindingAttr (
  el: ASTElement,
  name: string,
  getStatic?: boolean
): ?string {
   // 获取指定属性的值
  const dynamicValue =
    getAndRemoveAttr(el, ':' + name) ||
    getAndRemoveAttr(el, 'v-bind:' + name)
  if (dynamicValue != null) {
    return parseFilters(dynamicValue)
  } else if (getStatic !== false) {
    const staticValue = getAndRemoveAttr(el, name)
    if (staticValue != null) {
      return JSON.stringify(staticValue)
    }
  }
}
复制代码

getAndRemoveAttr

/**
  从 el.attrsList 中删除指定的属性 name
  如果 removeFromMap 为 true,则同样删除 el.attrsMap 对象中的该属性,
  比如 v-if、v-else-if、v-else 等属性就会被移除,
  不过一般不会删除该对象上的属性,因为从 ast 生成 代码期间还需要使用该对象,返回指定属性的值
 */
export function getAndRemoveAttr (
  el: ASTElement,
  name: string,
  removeFromMap?: boolean
): ?string {
  let val
  // 将执行属性 name 从 el.attrsList 中移除
  if ((val = el.attrsMap[name]) != null) {
    const list = el.attrsList
    for (let i = 0, l = list.length; i < l; i++) {
      if (list[i].name === name) {
        list.splice(i, 1)
        break
      }
    }
  }
  // 如果 removeFromMap 为 true,则从 el.attrsMap 中移除指定的属性 name
  // 不过一般不会移除 el.attsMap 中的数据,因为从 ast 生成 代码期间还需要使用该对象
  if (removeFromMap) {
    delete el.attrsMap[name]
  }

  // 返回执行属性的值
  return val
}
复制代码

addRawAttr

// 获取 el 对象上执行属性 name 的值 
export function getBindingAttr (
  el: ASTElement,
  name: string,
  getStatic?: boolean
): ?string {
   // 获取指定属性的值
  const dynamicValue =
    getAndRemoveAttr(el, ':' + name) ||
    getAndRemoveAttr(el, 'v-bind:' + name)
  if (dynamicValue != null) {
    return parseFilters(dynamicValue)
  } else if (getStatic !== false) {
    const staticValue = getAndRemoveAttr(el, name)
    if (staticValue != null) {
      return JSON.stringify(staticValue)
    }
  }
}
复制代码

processFor、processRef、processKey、processElement

文件位置:/src/compiler/parser/index.js

processFor

/**
 * 处理 v-for,将结果设置到 el 对象上,得到:
 *   el.for = 可迭代对象,比如 arr
 *   el.alias = 别名,比如 item
 * @param {*} el 元素的 ast 对象
 */
export function processFor (el: ASTElement) {
  let exp
  // 获取 el 上的 v-for 属性的值
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    // 解析 v-for 的表达式,得到 { for: 可迭代对象, alias: 别名 }
    // 比如 { for: arr, alias: item }
    const res = parseFor(exp)
    if (res) {
      // 将 res 对象上的属性拷贝到 el 对象上
      extend(el, res)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `Invalid v-for expression: ${exp}`,
        el.rawAttrsMap['v-for']
      )
    }
  }
}
复制代码

processRef

/**
 * 处理元素上的 ref 属性
 *  el.ref = refVal
 *  el.refInFor = boolean
 */
function processRef (el) {
  const ref = getBindingAttr(el, 'ref')
  if (ref) {
    el.ref = ref
    // 判断包含 ref 属性的元素是否包含在具有 v-for 指令的元素内或后代元素中
    // 如果是,则 ref 指向的则是包含 DOM 节点或组件实例的数组
    el.refInFor = checkInFor(el)
  }
}
复制代码

processKey

// 处理元素上的 key 属性,设置 el.key = val
function processKey (el) {
  // 拿到 key 的属性值
  const exp = getBindingAttr(el, 'key')
  // 关于 key 使用上的异常处理
  if (exp) {
    // template 标签不允许设置 key
    if (process.env.NODE_ENV !== 'production') {
      if (el.tag === 'template') {
        warn(
          `<template> cannot be keyed. Place the key on real elements instead.`,
          getRawBindingAttr(el, 'key')
        )
      }
      // 不要在 <transition=group> 的子元素上使用 v-for 的 index 作为 key
      // 否则等价与没有使用 key
      if (el.for) {
        const iterator = el.iterator2 || el.iterator1
        const parent = el.parent
        if (iterator && iterator === exp && parent && parent.tag === 'transition-group') {
          warn(
            `Do not use v-for index as key on <transition-group> children, ` +
            `this is the same as not using keys.`,
            getRawBindingAttr(el, 'key'),
            true /* tip */
          )
        }
      }
    }
    // 设置 el.key = exp
    el.key = exp
  }
}
复制代码

processElement

/**
 * 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其它指令和一些原生属性 
 * 然后在 el 对象上添加如下属性:
 * el.key、ref、refInFor、scopedSlot、slotName、component、inlineTemplate、staticClass
 * el.bindingClass、staticStyle、bindingStyle、attrs
 * @param {*} element 被处理元素的 ast 对象
 * @param {*} options 配置项
 * @returns 
 */
export function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  processKey(element)

  // determine whether this is a plain element after
  // removing structural attributes
  // 确定 element 是否为一个普通元素
  element.plain = (
    !element.key &&
    !element.scopedSlots &&
    !element.attrsList.length
  )

  // el.ref = val, el.refInFor = boolean
  processRef(element)
  // 处理作为插槽传递给组件的内容,得到 插槽名称、是否为动态插槽、作用域插槽的值,
  // 以及插槽中的所有子元素,子元素放到插槽对象的 children 属性中
  processSlotContent(element)
  // 处理自闭合的 slot 标签,得到插槽名称 => el.slotName = xx
  processSlotOutlet(element)
  // 处理动态组件,<component :is="compoName"></component>得到 el.component = compName,
  // 以及标记是否存在内联模版,el.inlineTemplate = true of false
  processComponent(element)


  /*
   为 element 对象分别执行 class、style、model 模块中的 transformNode 方法
   不过 web 平台只有 class、style 模块有 transformNode 方法,分别用来处理 class 属性和 style 属性
   得到 el.staticStyle、 el.styleBinding、el.staticClass、el.classBinding
   分别存放静态 style 属性的值、动态 style 属性的值,以及静态 class 属性的值和动态 class 属性的值
  */
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }

  /**
   处理元素上的所有属性:
    1. v-bind 指令变成:el.attrs 或 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...],
    或者是必须使用 props 的属性,变成了 el.props = [{ name, value, start, end, dynamic }, ...]
    2. v-on 指令变成:el.events 或 el.nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] }
    3. 其它指令:el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
    4. 原生属性:el.attrs = [{ name, value, start, end }],或者一些必须使用 props 的属性,
    变成了:el.props = [{ name, value: true, start, end, dynamic }]
   */
  processAttrs(element)
  return element
}
复制代码

processSlotContent() 方法

文件位置:/src/compiler/parser/index.js

/*
 处理作为插槽传递给组件的内容,得到:
  slotTarget => 插槽名
  slotTargetDynamic => 是否为动态插槽
  slotScope => 作用域插槽的值
 直接在 <comp> 标签上使用 v-slot 语法时,将上述属性放到 el.scopedSlots 对象上,其它情况直接放到 el 对象上

 handle content being passed to a component as slot,
 e.g. <template slot="xxx">, <div slot-scope="xxx">
*/
function processSlotContent (el) {
  let slotScope
  if (el.tag === 'template') {
    /* 
     template 标签上使用 scope 属性的提示
     scope 已经弃用,并在 2.5 之后使用 slot-scope 代替
     slot-scope 即可以用在 template 标签也可以用在普通标签上
    */
    slotScope = getAndRemoveAttr(el, 'scope')
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && slotScope) {
      warn(
        `the "scope" attribute for scoped slots have been deprecated and ` +
        `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
        `can also be used on plain elements in addition to <template> to ` +
        `denote scoped slots.`,
        el.rawAttrsMap['scope'],
        true
      )
    }
    // el.slotScope = val
    el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
  } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
      warn(
        `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
        `(v-for takes higher priority). Use a wrapper <template> for the ` +
        `scoped slot to make it clearer.`,
        el.rawAttrsMap['slot-scope'],
        true
      )
    }
    el.slotScope = slotScope
  }

  // slot="xxx"
  // 获取 slot 属性的值
  // slot="xxx",旧的具名插槽的写法
  const slotTarget = getBindingAttr(el, 'slot')
  if (slotTarget) {
    // el.slotTarget = 插槽名(具名插槽)
    el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
    // 动态插槽名
    el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
    // preserve slot as an attribute for native shadow DOM compat
    // only for non-scoped slots.
    if (el.tag !== 'template' && !el.slotScope) {
      addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
    }
  }

  // 2.6 v-slot syntax
  if (process.env.NEW_SLOT_SYNTAX) {
    if (el.tag === 'template') {
      // v-slot on <template>
       // v-slot 在 tempalte 标签上,得到 v-slot 的值
      // v-slot on <template>
      const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
      if (slotBinding) {
        if (process.env.NODE_ENV !== 'production') {
          if (el.slotTarget || el.slotScope) {
             // 不同插槽语法禁止混合使用
            warn(
              `Unexpected mixed usage of different slot syntaxes.`,
              el
            )
          }
          if (el.parent && !maybeComponent(el.parent)) {
             /* 
             <template v-slot> 只能出现在组件的根位置,比如:
              <comp>
                <template v-slot>xx</template>
              </comp>

              而不能是
              
              <comp>
                <div>
                  <template v-slot>xxx</template>
                </div>
              </comp>
             */
            warn(
              `<template v-slot> can only appear at the root level inside ` +
              `the receiving component`,
              el
            )
          }
        }
        // 得到插槽名称
        const { name, dynamic } = getSlotName(slotBinding)
        // 将插槽名称保存到 el.slotTarget 上
        el.slotTarget = name
        // 是否为动态插槽
        el.slotTargetDynamic = dynamic
        // 作用域插槽的值
        el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
      }
    } else {
       /* 
        处理组件上的 v-slot,<comp v-slot:header />
        slotBinding = { name: "v-slot:header", value: "", start, end}
        v-slot on component, denotes default slot
       */
      const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
      if (slotBinding) {
        if (process.env.NODE_ENV !== 'production') {
          // el 不是组件的话,提示,v-slot 只能出现在组件上或 template 标签上
          if (!maybeComponent(el)) {
            warn(
              `v-slot can only be used on components or <template>.`,
              slotBinding
            )
          }
          // 语法混用
          if (el.slotScope || el.slotTarget) {
            warn(
              `Unexpected mixed usage of different slot syntaxes.`,
              el
            )
          }
          // 为了避免作用域歧义,当存在其他命名槽时,默认槽也应该使用<template>语法
          if (el.scopedSlots) {
            warn(
              `To avoid scope ambiguity, the default slot should also use ` +
              `<template> syntax when there are other named slots.`,
              slotBinding
            )
          }
        }
        // 将组件的孩子添加到它的默认插槽内
        // add the component's children to its default slot
        const slots = el.scopedSlots || (el.scopedSlots = {})
        // 获取插槽名称以及是否为动态插槽
        const { name, dynamic } = getSlotName(slotBinding)
        // 创建一个 template 标签的 ast 对象,用于容纳插槽内容,父级是 el
        const slotContainer = slots[name] = createASTElement('template', [], el)
        // 插槽名
        slotContainer.slotTarget = name
        // 是否为动态插槽
        slotContainer.slotTargetDynamic = dynamic
        // 所有的孩子,将每一个孩子的 parent 属性都设置为 slotContainer
        slotContainer.children = el.children.filter((c: any) => {
          if (!c.slotScope) {
            // 给插槽内元素设置 parent 属性为 slotContainer,也就是 template 元素
            c.parent = slotContainer
            return true
          }
        })
        slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
        // remove children as they are returned from scopedSlots now
        el.children = []
        // mark el non-plain so data gets generated
        el.plain = false
      }
    }
  }
}
复制代码

getSlotName、processSlotOutlet、processComponent

文件位置:/src/compiler/parser/index.js

getSlotName

/**
 * 解析 binding,得到插槽名称以及是否为动态插槽
 * @returns { name: 插槽名称, dynamic: 是否为动态插槽 }
 */
function getSlotName (binding) {
  let name = binding.name.replace(slotRE, '')
  if (!name) {
    if (binding.name[0] !== '#') {
      name = 'default'
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `v-slot shorthand syntax requires a slot name.`,
        binding
      )
    }
  }
  return dynamicArgRE.test(name)
    // dynamic [name]
    ? { name: name.slice(1, -1), dynamic: true }
    // static name
    : { name: `"${name}"`, dynamic: false }
}
复制代码

processSlotOutlet

// handle <slot/> outlets,处理自闭合 slot 标签
// 得到插槽名称,el.slotName
function processSlotOutlet (el) {
  if (el.tag === 'slot') {
     // 得到插槽名称
    el.slotName = getBindingAttr(el, 'name')
     // 不允许在 slot 标签上使用 key 属性
    if (process.env.NODE_ENV !== 'production' && el.key) {
      warn(
        `\`key\` does not work on <slot> because slots are abstract outlets ` +
        `and can possibly expand into multiple elements. ` +
        `Use the key on a wrapping element instead.`,
        getRawBindingAttr(el, 'key')
      )
    }
  }
}
复制代码

processComponent

/**
 * 处理动态组件,<component :is="compName"></component>
 * 得到 el.component = compName
 */
function processComponent (el) {
  let binding
  // 解析 is 属性,得到属性值,即组件名称,el.component = compName
  if ((binding = getBindingAttr(el, 'is'))) {
    el.component = binding
  }
   /* 
    <component :is="compName" inline-template>xx</component>
    组件上存在 inline-template 属性,进行标记:el.inlineTemplate = true
    表示组件开始和结束标签内的内容作为组件模版出现,而不是作为插槽别分发,方便定义组件模版
   */
  if (getAndRemoveAttr(el, 'inline-template') != null) {
    el.inlineTemplate = true
  }
}
复制代码

transformNode() —— class 模块

文件位置:/src/platforms/web/compiler/modules/class.js

/**
 * 处理元素上的 class 属性
 * 静态的 class 属性值赋值给 el.staticClass 属性
 * 动态的 class 属性值赋值给 el.classBinding 属性
 */
function transformNode (el: ASTElement, options: CompilerOptions) {
  const warn = options.warn || baseWarn
  // 获取元素上静态 class 属性的值 xx,<div class="xx"></div>
  const staticClass = getAndRemoveAttr(el, 'class')
  if (process.env.NODE_ENV !== 'production' && staticClass) {
    const res = parseText(staticClass, options.delimiters)
    // 警告提示,同 style 的提示一样,不能使用 <div class="{{ val}}"></div>,请用
    // <div :class="val"></div> 代替
    if (res) {
      warn(
        `class="${staticClass}": ` +
        'Interpolation inside attributes has been removed. ' +
        'Use v-bind or the colon shorthand instead. For example, ' +
        'instead of <div class="{{ val }}">, use <div :class="val">.',
        el.rawAttrsMap['class']
      )
    }
  }
  // 静态 class 属性值赋值给 el.staticClass
  if (staticClass) {
    el.staticClass = JSON.stringify(staticClass.replace(/\s+/g, ' ').trim())
  }
  // 获取动态绑定的 class 属性值,并赋值给 el.classBinding
  const classBinding = getBindingAttr(el, 'class', false /* getStatic */)
  if (classBinding) {
    el.classBinding = classBinding
  }
}
复制代码

transformNode() —— style 模块

文件位置:/src/platforms/web/compiler/modules/style.js

/**
 * 从 el 上解析出静态的 style 属性和动态绑定的 style 属性,分别赋值给:
 * el.staticStyle 和 el.styleBinding
 */
function transformNode (el: ASTElement, options: CompilerOptions) {
  const warn = options.warn || baseWarn
   // <div style="xx"></div>
  // 获取 style 属性
  const staticStyle = getAndRemoveAttr(el, 'style')
  if (staticStyle) {
    /*
     istanbul ignore if 
     提示,如果从 xx 中解析到了界定符,说明是一个动态的 style,
     比如 <div style="{{ val }}"></div>则给出提示:
     动态的 style 请使用 <div :style="val"></div>
    */
    if (process.env.NODE_ENV !== 'production') {
      const res = parseText(staticStyle, options.delimiters)
      if (res) {
        warn(
          `style="${staticStyle}": ` +
          'Interpolation inside attributes has been removed. ' +
          'Use v-bind or the colon shorthand instead. For example, ' +
          'instead of <div style="{{ val }}">, use <div :style="val">.',
          el.rawAttrsMap['style']
        )
      }
    }
    // 将静态的 style 样式赋值给 el.staticStyle
    el.staticStyle = JSON.stringify(parseStyleText(staticStyle))
  }

  // 获取动态绑定的 style 属性,比如 <div :style="styleVariable"></div>
  const styleBinding = getBindingAttr(el, 'style', false /* getStatic */)
  if (styleBinding) {
    // 赋值给 el.styleBinding
    el.styleBinding = styleBinding
  }
}
复制代码

猜你喜欢

转载自juejin.im/post/7048528049456807949