Vue 原始碼解讀(8)—— 編譯器 之 解析(上)

李永寧發表於2022-03-02

特殊說明

由於文章篇幅限制,所以將 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。

相關文章