Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue 源码解读(8)—— 编译器 之 解析(上) #17

Open
liyongning opened this issue Jun 27, 2022 · 0 comments
Open

Vue 源码解读(8)—— 编译器 之 解析(上) #17

liyongning opened this issue Jun 27, 2022 · 0 comments
Labels
Vue Vue 技术栈 源码原理 深度解读框架源码原理

Comments

@liyongning
Copy link
Owner

Vue 源码解读(8)—— 编译器 之 解析(上)

当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

封面

特殊说明

由于文章篇幅限制,所以将 Vue 源码解读(8)—— 编译器 之 解析 拆成了上下两篇,所以在阅读本篇文章时请同时打开 Vue 源码解读(8)—— 编译器 之 解析(下)一起阅读。

前言

Vue 源码解读(4)—— 异步更新 最后说到刷新 watcher 队列,执行每个 watcher.run 方法,由 watcher.run 调用 watcher.get,从而执行 watcher.getter 方法,进入实际的更新阶段。这个流程如果不熟悉,建议大家再去读一下这篇文章。

当更新一个渲染 watcher 时,执行的是 updateComponent 方法:

// /src/core/instance/lifecycle.js
const updateComponent = () => {
  // 执行 vm._render() 函数,得到 虚拟 DOM,并将 vnode 传递给 _update 方法,接下来就该到 patch 阶段了
  vm._update(vm._render(), hydrating)
}

可以看到每次更新前都需要先执行一下 vm._render() 方法,vm._render 就是大家经常听到的 render 函数,由两种方式得到:

  • 用户自己提供,在编写组件时,用 render 选项代替模版

  • 由编译器编译组件模版生成 render 选项

今天我们就来深入研究编译器,看看它是怎么将我们平时编写的类 html 模版编译成 render 函数的。

编译器的核心由三部分组成:

  • 解析,将类 html 模版转换为 AST 对象

  • 优化,也叫静态标记,遍历 AST 对象,标记每个节点是否为静态节点,以及标记出静态根节点

  • 生成渲染函数,将 AST 对象生成渲染函数

由于编译器这块儿的代码量太大,所以,将这部分知识拆成三部分来讲,第一部分就是:解析。

目标

深入理解 Vue 编译器的解析过程,理解如何将类 html 模版字符串转换成 AST 对象。

源码解读

接下来我们去源码中找答案。

阅读建议

由于解析过程代码量巨大,所以建议大家抓住主线:“解析类 HTML 字符串模版,生成 AST 对象”,而这个 AST 对象就是我们最终要得到的结果,所以大家在阅读的过程中,要动手记录这个 AST 对象,这样有助于理解,也让你不那么容易迷失。

也可以先阅读 下篇帮助 部分,有个提前的准备和心理预期。

入口 - $mount

编译器的入口位置在 /src/platforms/web/entry-runtime-with-compiler.js,有两种方式找到这个入口

/src/platforms/web/entry-runtime-with-compiler.js

/**
 * 编译器的入口
 * 运行时的 Vue.js 包就没有这部分的代码,通过 打包器 结合 vue-loader + vue-compiler-utils 进行预编译,将模版编译成 render 函数
 * 
 * 就做了一件事情,得到组件的渲染函数,将其设置到 this.$options 上
 */
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 挂载点
  el = el && query(el)

  // 挂载点不能是 body 或者 html
  /* istanbul ignore if */
  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 配置项,则直接跳过编译阶段,否则进入编译阶段
   *   解析 template 和 el,并转换为 render 函数
   *   优先级:render > template > el
   */
  if (!options.render) {
    let template = options.template
    if (template) {
      // 处理 template 选项
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // { template: '#app' },template 是一个 id 选择器,则获取该元素的 innerHtml 作为模版
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        // template 是一个正常的元素,获取其 innerHtml 作为模版
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 设置了 el 选项,获取 el 选择器的 outerHtml 作为模版
      template = getOuterHTML(el)
    }
    if (template) {
      // 模版就绪,进入编译阶段
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      // 编译模版,得到 动态渲染函数和静态渲染函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        // 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        // 界定符,默认 {{}}
        delimiters: options.delimiters,
        // 是否保留注释
        comments: options.comments
      }, this)
      // 将两个渲染函数放到 this.$options 上
      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')
      }
    }
  }
  // 执行挂载
  return mount.call(this, el, hydrating)
}

compileToFunctions

/src/compiler/to-function.js

/**
 * 1、执行编译函数,得到编译结果 -> compiled
 * 2、处理编译期间产生的 error 和 tip,分别输出到控制台
 * 3、将编译得到的字符串代码通过 new Function(codeStr) 转换成可执行的函数
 * 4、缓存编译结果
 * @param { string } template 字符串模版
 * @param { CompilerOptions } options 编译选项
 * @param { Component } vm 组件实例
 * @return { render, staticRenderFns }
 */
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') {
    // 检测可能的 CSP 限制
    try {
      new Function('return 1')
    } catch (e) {
      if (e.toString().match(/unsafe-eval|CSP/)) {
        // 看起来你在一个 CSP 不安全的环境中使用完整版的 Vue.js,模版编译器不能工作在这样的环境中。
        // 考虑放宽策略限制或者预编译你的 template 为 render 函数
        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.'
        )
      }
    }
  }

  // 如果有缓存,则跳过编译,直接从缓存中获取上次编译的结果
  const key = options.delimiters
    ? String(options.delimiters) + template
    : template
  if (cache[key]) {
    return cache[key]
  }

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

  // 检查编译期间产生的 error 和 tip,分别输出到控制台
  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))
      }
    }
  }

  // 转换编译得到的字符串代码为函数,通过 new Function(code) 实现
  // turn code into functions
  const res = {}
  const fnGenErrors = []
  res.render = createFunction(compiled.render, fnGenErrors)
  res.staticRenderFns = compiled.staticRenderFns.map(code => {
    return createFunction(code, fnGenErrors)
  })

  // 处理上面代码转换过程中出现的错误,这一步一般不会报错,除非编译器本身出错了
  /* 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

/**
 * 编译函数,做了两件事:
 *   1、选项合并,将 options 配置项 合并到 finalOptions(baseOptions) 中,得到最终的编译配置对象
 *   2、调用核心编译器 baseCompile 得到编译结果
 *   3、将编译期间产生的 error 和 tip 挂载到编译结果上,返回编译结果
 * @param {*} template 模版
 * @param {*} options 配置项
 * @returns 
 */
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)
      }
    }

    /**
     * 将 options 中的配置项合并到 finalOptions
     */

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

  // 日志
  finalOptions.warn = warn

  // 到这里为止终于到重点了,调用核心编译函数,传递模版字符串和最终的编译选项,得到编译结果
  // 前面做的所有事情都是为了构建平台最终的编译选项
  const compiled = baseCompile(template.trim(), finalOptions)
  if (process.env.NODE_ENV !== 'production') {
    detectErrors(compiled.ast, warn)
  }
  // 将编译期间产生的错误和提示挂载到编译结果上
  compiled.errors = errors
  compiled.tips = tips
  return compiled
}

baseOptions

/src/platforms/web/compiler/options.js

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  // 处理 class、style、v-model
  modules,
  // 处理指令
  // 是否是 pre 标签
  isPreTag,
  // 是否是自闭合标签
  isUnaryTag,
  // 规定了一些应该使用 props 进行绑定的属性
  mustUseProp,
  // 可以只写开始标签的标签,结束标签浏览器会自动补全
  canBeLeftOpenTag,
  // 是否是保留标签(html + svg)
  isReservedTag,
  // 获取标签的命名空间
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

baseCompile

/src/compiler/index.js

/**
 * 在这之前做的所有的事情,只有一个目的,就是为了构建平台特有的编译选项(options),比如 web 平台
 * 
 * 1、将 html 模版解析成 ast
 * 2、对 ast 树进行静态标记
 * 3、将 ast 生成渲染函数
 *    静态渲染函数放到  code.staticRenderFns 数组中
 *    code.render 为动态渲染函数
 *    在将来渲染时执行渲染函数得到 vnode
 */
function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 将模版解析为 AST,每个节点的 ast 对象上都设置了元素的所有信息,比如,标签信息、属性信息、插槽信息、父节点、子节点等。
  // 具体有那些属性,查看 start 和 end 这两个处理开始和结束标签的方法
  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

/**
 * 
 * 将 HTML 字符串转换为 AST
 * @param {*} template HTML 模版
 * @param {*} options 平台特有的编译选项
 * @returns root
 */
export function parse(
  template: s tring,
  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 || !isReservedTag(el.tag)

  // 分别获取 options.modules 下的 class、model、style 三个模块中的 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
  
  // 解析 html 模版字符串,处理所有标签以及标签上的属性
  // 这里的 parseHTMLOptions 在后面处理过程中用到,再进一步解析
  // 提前解析的话容易让大家岔开思路
  parseHTML(template, parseHtmlOptions)
  
  // 返回生成的 ast 对象
  return root

parseHTML

/src/compiler/parser/html-parser.js

/**
 * 通过循环遍历 html 模版字符串,依次处理其中的各个标签,以及标签上的属性
 * @param {*} html html 模版
 * @param {*} options 配置项
 */
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) {
        // 处理注释标签 <!-- xx -->
        if (comment.test(html)) {
          // 注释标签的结束索引
          const commentEnd = html.indexOf('-->')

          if (commentEnd >= 0) {
            // 是否应该保留 注释
            if (options.shouldKeepComment) {
              // 得到:注释内容、注释的开始索引、结束索引
              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 html>
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        /**
         * 处理开始标签和结束标签是这整个函数中的核型部分,其它的不用管
         * 这两部分就是在构造 element ast
         */

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

        // 处理开始标签,比如 <div id="app">,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 中找到下一个 <,直到 <xx 是上述几种情况的标签,则结束,
        // 在这整个过程中一直在调整 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 {
      // 处理 script、style、textarea 标签的闭合标签
      let endTagLength = 0
      // 开始标签的小写形式
      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()
}

advance

/src/compiler/parser/html-parser.js

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

parseStartTag

/src/compiler/parser/html-parser.js

/**
 * 解析开始标签,比如:<div id="app">
 * @returns { tagName: 'div', attrs: [[xx], ...], start: index }
 */
function parseStartTag() {
  const start = html.match(startTagOpen)
  if (start) {
    // 处理结果
    const match = {
      // 标签名
      tagName: start[1],
      // 属性,占位符
      attrs: [],
      // 标签的开始位置
      start: index
    }
    /**
     * 调整 html 和 index,比如:
     *   html = ' id="app">'
     *   index = 此时的索引
     *   start[0] = '<div'
     */
    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
    }
  }
}

handleStartTag

/src/compiler/parser/html-parser.js

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

  // 一元标签,比如 <hr />
  const unary = isUnaryTag(tagName) || !!unarySlash

  // 处理 match.attrs,得到 attrs = [{ name: attrName, value: attrVal, start: xx, end: xx }, ...]
  // 比如 attrs = [{ name: 'id', value: 'app', start: xx, end: xx }, ...]
  const l = match.attrs.length
  const attrs = new Array(l)
  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
    }
  }

  // 如果不是自闭合标签,则将标签信息放到 stack 数组中,待将来处理到它的闭合标签时再将其弹出 stack
  // 如果是自闭合标签,则标签信息就没必要进入 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 方法,主要做了以下 6 件事情:
   *   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)
  }
}

parseEndTag

/src/compiler/parser/html-parser.js

/**
 * 解析结束标签,比如:</div>
 * 最主要的事就是:
 *   1、处理 stack 数组,从 stack 数组中找到当前结束标签对应的开始标签,然后调用 options.end 方法
 *   2、处理完结束标签之后调整 stack 数组,保证在正常情况下 stack 数组中的最后一个元素就是下一个结束标签对应的开始标签
 *   3、处理一些异常情况,比如 stack 数组最后一个元素不是当前结束标签对应的开始标签,还有就是
 *      br 和 p 标签单独处理
 * @param {*} tagName 标签名,比如 div
 * @param {*} start 结束标签的开始索引
 * @param {*} end 结束标签的结束索引
 */
function parseEndTag(tagName, start, end) {
  let pos, lowerCasedTagName
  if (start == null) start = index
  if (end == null) end = index

  // 倒序遍历 stack 数组,找到第一个和当前结束标签相同的标签,该标签就是结束标签对应的开始标签的描述对象
  // 理论上,不出异常,stack 数组中的最后一个元素就是当前结束标签的开始标签的描述对象
  // 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
  }

  // 如果在 stack 中一直没有找到相同的标签名,则 pos 就会 < 0,进行后面的 else 分支

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

parseHtmlOptions

src/compiler/parser/index.js

定义如何处理开始标签、结束标签、文本节点和注释节点。

start

/**
 * 主要做了以下 6 件事情:
 *   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、如果当前元素为自闭合标签,则表示该标签要处理结束了,让自己和父元素产生关系,以及设置自己的子元素
 * @param {*} tag 标签名
 * @param {*} attrs [{ name: attrName, value: attrVal, start, end }, ...] 形式的属性数组
 * @param {*} unary 自闭合标签
 * @param {*} start 标签在 html 字符串中的开始索引
 * @param {*} end 标签在 html 字符串中的结束索引
 */
function 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 标签
  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 }
    )
  }

  /**
   * 为 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 or radio or other(比如 text)" />
   */
  // apply pre-transforms
  for (let i = 0; i < preTransforms.length; i++) {
    element = preTransforms[i](element, options) || element
  }

  if (!inVPre) {
    // 表示 element 是否存在 v-pre 指令,存在则设置 element.pre = true
    processPre(element)
    if (element.pre) {
      // 存在 v-pre 指令,则设置 inVPre 为 true
      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)
    }
  }

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

end

/**
 * 处理结束标签
 * @param {*} tag 结束标签的名称
 * @param {*} start 结束标签的开始索引
 * @param {*} end 结束标签的结束索引
 */
function 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
  }
  /**
   * 主要做了 3 件事:
   *   1、如果元素没有被处理过,即 el.processed 为 false,则调用 processElement 方法处理节点上的众多属性
   *   2、让自己和父元素产生关系,将自己放到父元素的 children 数组中,并设置自己的 parent 属性为 currentParent
   *   3、设置自己的子元素,将自己所有非插槽的子元素放到自己的 children 数组中
   */
  closeElement(element)
}

chars

/**
 * 处理文本,基于文本生成 ast 对象,然后将该 ast 放到它的父元素的肚子里,即 currentParent.children 数组中 
 */
function 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) {
    // 说明文本不在 pre 标签内而且 text.trim() 为空,而且当前父元素也没有孩子节点,
    // 则将 text 置为空
    // remove the whitespace-only node right after an opening tag
    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) {
    if (!inPre && whitespaceOption === 'condense') {
      // 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个连续空格压缩为单个
      // condense consecutive whitespaces into single space
      text = text.replace(whitespaceRE, ' ')
    }
    let res
    // 基于 text 生成 AST 对象
    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 放到父元素的肚子里,即 currentParent.children 数组中
    if (child) {
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        child.start = start
        child.end = end
      }
      children.push(child)
    }
  }
},

comment

/**
 * 处理注释节点
 */
function 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.start = start
      child.end = end
    }
    // 将当前注释节点放到父元素的 children 属性中
    currentParent.children.push(child)
  }
}

createASTElement

/src/compiler/parser/index.js

/**
 * 为指定元素创建 AST 对象
 * @param {*} tag 标签名
 * @param {*} attrs 属性数组,[{ name: attrName, value: attrVal, start, end }, ...]
 * @param {*} parent 父元素
 * @returns { type: 1, tag, attrsList, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent, children: []}
 */
export function createASTElement(
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
): ASTElement {
  return {
    // 节点类型
    type: 1,
    // 标签名
    tag,
    // 标签的属性数组
    attrsList: attrs,
    // 标签的属性对象 { attrName: attrVal, ... }
    attrsMap: makeAttrsMap(attrs),
    // 原始属性对象
    rawAttrsMap: {},
    // 父节点
    parent,
    // 孩子节点
    children: []
  }
}

preTransformNode

/src/platforms/web/compiler/modules/model.js

/**
 * 处理存在 v-model 的 input 标签,但没处理 v-model 属性
 * 分别处理了 input 为 checkbox、radio 和 其它的情况
 * input 具体是哪种情况由 el.ifConditions 中的条件来判断
 * <input v-mode="test" :type="checkbox or radio or other(比如 text)" />
 * @param {*} el 
 * @param {*} options 
 * @returns branch0
 */
function preTransformNode (el: ASTElement, options: CompilerOptions) {
  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="test" />
      const ifCondition = getAndRemoveAttr(el, 'v-if', true)
      // &&test
      const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``
      // 是否存在 v-else 属性,<input v-else />
      const hasElse = getAndRemoveAttr(el, 'v-else', true) != null
      // 获取 v-else-if 属性的值 <inpu v-else-if="test" />
      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&&test or false&&test,标记当前 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)
      // 在 branch0.ifConfitions 数组中放入 { exp, block } 对象
      addIfCondition(branch0, {
        // 标记当前 input 是否为 radio
        exp: `(${typeBinding})==='radio'` + ifConditionExtra,
        block: branch1
      })
      // 3. other,input 为其它的情况
      const branch2 = cloneASTElement(el)
      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

/src/compiler/helpers.js

/**
 * 获取 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

/src/compiler/helpers.js

/**
 * 从 el.attrsList 中删除指定的属性 name
 * 如果 removeFromMap 为 true,则同样删除 el.attrsMap 对象中的该属性,
 *   比如 v-if、v-else-if、v-else 等属性就会被移除,
 *   不过一般不会删除该对象上的属性,因为从 ast 生成 代码 期间还需要使用该对象
 * 返回指定属性的值
 */
// note: this only removes the attr from the Array (attrsList) so that it
// doesn't get processed by processAttrs.
// By default it does NOT remove it from the map (attrsMap) because the map is
// needed during codegen.
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
}

processFor

/src/compiler/parser/index.js

/**
 * 处理 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']
      )
    }
  }
}

addRawAttr

/src/compiler/helpers.js

// 在 el.attrsMap 和 el.attrsList 中添加指定属性 name
// add a raw attr (use this in preTransforms)
export function addRawAttr (el: ASTElement, name: string, value: any, range?: Range) {
  el.attrsMap[name] = value
  el.attrsList.push(rangeSetItem({ name, value }, range))
}

processElement

/src/compiler/parser/index.js

/**
 * 分别处理元素节点的 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
) {
  // el.key = val
  processKey(element)

  // 确定 element 是否为一个普通元素
  // determine whether this is a plain element after
  // removing structural attributes
  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
  }
  /**
   * 处理元素上的所有属性:
   * v-bind 指令变成:el.attrs 或 el.dynamicAttrs = [{ name, value, start, end, dynamic }, ...],
   *                或者是必须使用 props 的属性,变成了 el.props = [{ name, value, start, end, dynamic }, ...]
   * v-on 指令变成:el.events 或 el.nativeEvents = { name: [{ value, start, end, modifiers, dynamic }, ...] }
   * 其它指令:el.directives = [{name, rawName, value, arg, isDynamicArg, modifier, start, end }, ...]
   * 原生属性:el.attrs = [{ name, value, start, end }],或者一些必须使用 props 的属性,变成了:
   *         el.props = [{ name, value: true, start, end, dynamic }]
   */
  processAttrs(element)
  return element
}

processKey

/src/compiler/parser/index.js

/**
 * 处理元素上的 key 属性,设置 el.key = val
 * @param {*} el 
 */
function processKey(el) {
  // 拿到 key 的属性值
  const exp = getBindingAttr(el, 'key')
  if (exp) {
    // 关于 key 使用上的异常处理
    if (process.env.NODE_ENV !== 'production') {
      // template 标签不允许设置 key
      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
  }
}

processRef

/src/compiler/parser/index.js

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

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']) {
      // 元素不能同时使用 slot-scope 和 v-for,v-for 具有更高的优先级
      // 应该用 template 标签作为容器,将 slot-scope 放到 template 标签上 
      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 = val
    el.slotScope = slotScope
  }

  // 获取 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 在 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 = 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

/src/compiler/parser/index.js

/**
 * 解析 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

/src/compiler/parser/index.js

// 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

/src/compiler/parser/index.js

/**
 * 处理动态组件,<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

/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)
  }
  // 获取动态绑定的 class 属性值,并赋值给 el.classBinding
  const classBinding = getBindingAttr(el, 'class', false /* getStatic */)
  if (classBinding) {
    el.classBinding = classBinding
  }
}

transformNode

/src/platforms/web/compiler/modules/style.js

/**
 * 从 el 上解析出静态的 style 属性和动态绑定的 style 属性,分别赋值给:
 * el.staticStyle 和 el.styleBinding
 * @param {*} el 
 * @param {*} options 
 */
function transformNode(el: ASTElement, options: CompilerOptions) {
  // 日志
  const warn = options.warn || baseWarn
  // <div style="xx"></div>
  // 获取 style 属性
  const staticStyle = getAndRemoveAttr(el, 'style')
  if (staticStyle) {
    // 提示,如果从 xx 中解析到了界定符,说明是一个动态的 style,
    // 比如 <div style="{{ val }}"></div>则给出提示:
    // 动态的 style 请使用 <div :style="val"></div>
    /* istanbul ignore if */
    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="{{ val }}"></div>
  const styleBinding = getBindingAttr(el, 'style', false /* getStatic */)
  if (styleBinding) {
    // 赋值给 el.styleBinding
    el.styleBinding = styleBinding
  }
}

链接

感谢各位的:关注点赞收藏评论,我们下期见。


当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

@liyongning liyongning added 源码原理 深度解读框架源码原理 Vue Vue 技术栈 labels Jun 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Vue Vue 技术栈 源码原理 深度解读框架源码原理
Projects
None yet
Development

No branches or pull requests

1 participant