Vue - The Good Parts: template到render函數

前言

熟悉 Vue 的同窗都知道,從 Vue2 開始,在實際運行的時候,是將用戶所寫的 template 轉換爲 render 函數,獲得 vnode 數據(虛擬 DOM),而後再繼續執行,最終通過 patch 到真實的 DOM,而當有數據更新的時候,也是靠這個進行 vnode 數據的 diff,最終決定更新哪些真實的 DOM。html

這個也是 Vue 的一大核心優點,尤大不止一次的講過,由於用戶本身寫的是靜態的模板,因此 Vue 就能夠根據這個模板信息作不少標記,進而就能夠作針對性的性能優化,這個在 Vue 3 中作了進一步的優化處理,block 相關設計。前端

因此,咱們就來看一看,在 Vue 中,template 到 render 函數,到底經歷了怎麼樣的過程,這裏邊有哪些是值得咱們借鑑和學習的。vue

正文分析

What

template 到 render,在 Vue 中實際上是對應的 compile 編譯的部分,也就是術語編譯器 cn.vuejs.org/v2/guide/in… 本質上來說,這個也是不少框架所採用的的方案 AOT,就是將本來須要在運行時作的事情,放在編譯時作好,以提高在運行時的性能。node

關於 Vue 自己模板的語法這裏就不詳細介紹了,感興趣的同窗能夠看 cn.vuejs.org/v2/guide/sy… ,大概就是形以下面的這些語法(插值和指令):webpack

image2021-6-18_17-6-55.png

render 函數呢,這部分在 Vue 中也有着詳細的介紹,你們能夠參閱 cn.vuejs.org/v2/guide/re… ,簡單來說,大概就是這個樣子:git

image2021-6-18_17-5-1.png

那咱們的核心目標就是這樣:github

image2021-6-18_17-9-3.png

若是你想體驗,能夠這裏 template-explorer.vuejs.orgweb

固然 Vue 3 的其實也是能夠的 https://vue-next-template-explorer ,雖然這裏咱們接下來要分析的是 Vue 2 版本的。正則表達式

How

要想了解是如何作到的,咱們就要從源碼入手,編譯器相關的都在 github.com/vuejs/vue/t… 目錄下,咱們這裏從入口文件 index.js 開始:express

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
 
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult {
  // 重點!
  // 第1步 parse 模板 獲得 ast
  const ast = parse(template.trim(), options)
  // 優化 能夠先忽略
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 第2步 根據 ast 生成代碼
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
複製代碼

其實,你會發現,這是一個經典的編譯器(Parsing、Transformation、Code Generation)實現的步驟(這裏實際上是簡化):

  1. parse,獲得 ast
  2. generate,獲得目標代碼

接下來咱們就分別來看下對應的實現。

1. parse

parse 的實如今 github.com/vuejs/vue/b… 這裏,因爲代碼比較長,咱們一部分一部分的看,先來看暴露出來的 parse 函數:

export function parse ( template: string, options: CompilerOptions ): ASTElement | void {
  // options 處理 這裏已經忽略了
  // 重要的棧 stack
  const stack = []
  const preserveWhitespace = options.preserveWhitespace !== false
  const whitespaceOption = options.whitespace
  // 根節點,只有一個,由於咱們知道 Vue 2 的 template 中只能有一個根元素
  // ast 是樹狀的結構,root 也就是這個樹的根節點
  let root
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false
  // parseHTML 處理
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    // 注意後邊的這些 options 函數 start end chars comment
    // 約等因而 parseHTML 所暴露出來的鉤子,以便於外界處理
    // 因此純粹的,parseHTML 只是負責 parse,可是並不會生成 ast 相關邏輯
    // 這裏的 ast 生成就是靠這裏的鉤子函數配合
    // 直觀理解也比較容易:
    // start 就是每遇到一個開始標籤的時候 調用
    // end 就是結束標籤的時候 調用
    // 這裏重點關注 start 和 end 中的邏輯就能夠,重點!!
    // chars comment 相對應的純文本和註釋
    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)
      }
      // 建立一個 ASTElement,根據標籤 屬性
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }
 
      if (process.env.NODE_ENV !== 'production') {
        if (options.outputSourceRange) {
          element.start = start
          element.end = end
          element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
            cumulated[attr.name] = attr
            return cumulated
          }, {})
        }
        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
              }
            )
          }
        })
      }
 
      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 }
        )
      }
 
      // 一些前置轉換 能夠忽略
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }
 
      if (!inVPre) {
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        // 處理 vue 指令 等
        processFor(element)
        processIf(element)
        processOnce(element)
      }
 
      if (!root) {
        // 若是尚未 root 即當前元素就是根元素
        root = element
        if (process.env.NODE_ENV !== 'production') {
          checkRootConstraints(root)
        }
      }
 
      if (!unary) {
        // 設置當前 parent 元素,處理 children 的時候須要
        currentParent = element
        // 由於咱們知道 html 的結構是 <div><p></p></div> 這樣的,因此會先 start 處理
        // 而後繼續 start 處理 而後 纔是兩次 end 處理
        // 是一個經典的棧的處理,先進後出的方式
        // 其實任意的編譯器都是離不開棧的,處理方式也是相似
        stack.push(element)
      } else {
        closeElement(element)
      }
    },
 
    end (tag, start, end) {
      // 當前處理的元素
      const element = stack[stack.length - 1]
      // 彈出最後一個
      // pop stack
      stack.length -= 1
      // 最新的尾部 就是接下來要處理的元素的 parent
      currentParent = stack[stack.length - 1]
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        element.end = end
      }
      closeElement(element)
    },
 
    chars (text: string, start: number, end: number) {
      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
      if (inPre || text.trim()) {
        text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
      } else if (!children.length) {
        // 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 ? ' ' : ''
      }
      if (text) {
        if (!inPre && whitespaceOption === 'condense') {
          // condense consecutive whitespaces into single space
          text = text.replace(whitespaceRE, ' ')
        }
        let res
        let child: ?ASTNode
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          }
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          child = {
            type: 3,
            text
          }
        }
        if (child) {
          if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
            child.start = start
            child.end = end
          }
          children.push(child)
        }
      }
    },
    comment (text: string, start, end) {
      // adding anything as a sibling to the root node is forbidden
      // comments should still be allowed, but ignored
      if (currentParent) {
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          child.start = start
          child.end = end
        }
        currentParent.children.push(child)
      }
    }
  })
  // 返回根節點
  return root
}
複製代碼

能夠看出作的最核心的事情就是調用 parseHTML,且傳的鉤子中作的事情最多的仍是在 start 開始標籤這裏最多。針對於在 Vue 的場景,利用鉤子的處理,最終咱們返回的 root 其實就是一個樹的根節點,也就是咱們的 ast,形如:

模板爲:

<div id="app">{{ msg }}</div>
複製代碼
{
    "type": 1,
    "tag": "div",
    "attrsList": [
        {
            "name": "id",
            "value": "app"
        }
    ],
    "attrsMap": {
        "id": "app"
    },
    "rawAttrsMap": {},
    "children": [
        {
            "type": 2,
            "expression": "_s(msg)",
            "tokens": [
                {
                    "@binding": "msg"
                }
            ],
            "text": "{{ msg }}"
        }
    ],
    "plain": false,
    "attrs": [
        {
            "name": "id",
            "value": "app"
        }
    ]
}
複製代碼

因此接下來纔是parse最核心的部分 parseHTML,取核心部分(不全),一部分一部分來分析,源文件 github.com/vuejs/vue/b…

// parse的過程就是一個遍歷 html 字符串的過程
export function parseHTML (html, options) {
  // html 就是一個 HTML 字符串
  // 再次出現棧,最佳數據結構,用於處理嵌套解析問題
  // HTML 中就是處理 標籤 嵌套
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  // 初始索引位置 index
  let index = 0
  let last, lastTag
  // 暴力循環 目的爲了遍歷
  while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    if (!lastTag || !isPlainTextElement(lastTag)) {
      // 沒有 lastTag 即初始狀態 或者說 lastTag 是 script style
      // 這種須要當作純文本處理的標籤元素
      // 正常狀態下 都應進入這個分支
      // 判斷標籤位置,其實也就是判斷了非標籤的end位置
      let textEnd = html.indexOf('<')
      // 在起始位置
      if (textEnd === 0) {
        // 註釋,先忽略
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')
 
          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            advance(commentEnd + 3)
            continue
          }
        }
 
        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        // 條件註釋,先忽略
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')
 
          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }
 
        // Doctype 先忽略
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }
 
        // 結束標籤,第一次先忽略,其餘case會進入
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          // 處理結束標籤
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }
 
        // 重點,通常場景下,開始標籤
        const startTagMatch = parseStartTag()
        // 若是存在開始標籤
        if (startTagMatch) {
          // 處理相關邏輯
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
      }
 
      let text, rest, next
      if (textEnd >= 0) {
        // 剩餘的 html 去掉文本以後的
        rest = html.slice(textEnd)
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // 純文本內的 <
          // < in plain text, be forgiving and treat it as text
          next = rest.indexOf('<', 1)
          if (next < 0) break
          textEnd += next
          rest = html.slice(textEnd)
        }
        // 獲得真正的文本內容
        text = html.substring(0, textEnd)
      }
      // 已經沒有 < 了 因此內容就是純文本
      if (textEnd < 0) {
        text = html
      }
 
      if (text) {
        // 重點 前進指定長度
        advance(text.length)
      }
 
      if (options.chars && text) {
        // 鉤子函數處理
        options.chars(text, index - text.length, index)
      }
    } else {
      // lastTag 存在 且是 script style 這樣的 將其內容當作純文本處理
      let endTagLength = 0
      // 存在棧中的tag名
      const stackedTag = lastTag.toLowerCase()
      // 指定 tag 的 匹配正則 注意 是到對應結束標籤的 正則,例如 </script>
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
      // 作替換
      // 即把 <div>xxxx</div></script> 這樣的替換掉
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        // 結束標籤自己長度 即 </script>的長度
        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 ''
      })
      // 索引前進 注意沒有用 advance 由於 html 實際上是已經修正過的 即 rest
      index += html.length - rest.length
      html = rest
      // 處理結束標籤
      parseEndTag(stackedTag, index - endTagLength, index)
    }
 
    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
    }
  }
}
複製代碼

這裏邊有幾個重點的函數,他們都是定義在 parseHTML 整個函數上下文中的,因此他們能夠直接訪問上邊定義的 index stack lastTag 等關鍵變量:

// 比較好理解,前進n個位置
function advance (n) {
    index += n
    html = html.substring(n)
}
複製代碼
// 開始標籤
function parseStartTag () {
  // 正則匹配開始 例如 <div
  const start = html.match(startTagOpen)
  if (start) {
    // 匹配到的
    const match = {
      tagName: start[1],
      attrs: [],
      start: index
    }
    // 移到 <div 以後
    advance(start[0].length)
    let end, attr
    // 到結束以前 即 > 以前
    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)
    }
    // 遇到了 > 結束了
    if (end) {
      // 是不是 自閉合標籤,例如 <xxxx />
      match.unarySlash = end[1]
      advance(end[0].length)
      match.end = index
      return match
    }
  }
}
複製代碼
// 當遇到開始標籤的狀況 去處理他們
// 由於開始標籤的狀況比較複雜 因此 單獨了一個函數處理
function handleStartTag (match) {
    const tagName = match.tagName
    const unarySlash = match.unarySlash
 
    if (expectHTML) {
      // HTML 場景
      // p 標籤以內不能存在 isNonPhrasingTag 的tag
      // 詳細的看 https://github.com/vuejs/vue/blob/v2.6.14/src/platforms/web/compiler/util.js#L18
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        // 因此在瀏覽器環境 也是會自動容錯處理的 直接閉合他們
        parseEndTag(lastTag)
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }
    // 自閉和的場景 或者 能夠省略結束標籤的case
    // 即 <xxx /> 或者 <br> <img> 這樣的場景
    const unary = isUnaryTag(tagName) || !!unarySlash
 
    const l = match.attrs.length
    const attrs = new Array(l)
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      const value = args[3] || args[4] || args[5] || ''
      const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines
      attrs[i] = {
        name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines)
      }
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        attrs[i].start = args.start + args[0].match(/^\s*/).length
        attrs[i].end = args.end
      }
    }
 
    if (!unary) {
      // 若是不是自閉和case 也就意味着能夠當作有 children 處理的
      // 棧裏 push 一個當前的
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      // 把 lastTag 設置爲當前的
      // 爲了下次進入 children 作準備
      lastTag = tagName
    }
    // start 鉤子處理
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }
複製代碼
// 結束標籤處理
function parseEndTag (tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index
 
    // Find the closest opened tag of the same type
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
      // 這裏須要找到 最近的 相同類型的 未閉合標籤
      // 相對應的配對的那個元素
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break
        }
      }
    } else {
      // If no tag name is provided, clean shop
      pos = 0
    }
 
    if (pos >= 0) {
      // 回到那個未閉合的標籤,這中間裏邊全部的元素都須要閉合掉
      // Close all the open elements, up the stack
      for (let i = stack.length - 1; i >= pos; i--) {
        if (process.env.NODE_ENV !== 'production' &&
          (i > pos || !tagName) &&
          options.warn
        ) {
          options.warn(
            `tag <${stack[i].tag}> has no matching end tag.`,
            { start: stack[i].start, end: stack[i].end }
          )
        }
        // end 鉤子
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }
      // 裏邊的元素也不須要處理了 直接修改棧的長度便可
      // Remove the open elements from the stack
      stack.length = pos
      // 記得更新 lastTag
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
      // br 的狀況 若是寫的是 </br> 其實效果至關於 <br>
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') {
      // p 的狀況 若是找不到 <p> 直接匹配到了 </p> 那麼認爲是 <p></p> 由於瀏覽器也是這樣兼容
      if (options.start) {
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        options.end(tagName, start, end)
      }
    }
  }
複製代碼

因此大概瞭解了上邊三個函數的做用,再和 parseHTML 的主邏輯結合起來,咱們能夠大概整理下 parseHTML 的整個過程。

這裏爲了方便,以一個具體的示例來進行,例如

<div id="app">
  <p :class="pClass">
    <span>
      This is dynamic msg:
        <span>{{ msg }}</span>
    </span>
  </p>
</div>
複製代碼

那麼首先直接進入 parseHTML,進入 while 循環,很明顯會走入到對於開始標籤的處理 parseStartTag

image2021-6-22_21-1-11.png

image2021-6-22_20-54-41.png

image2021-6-22_20-55-37 (1).png

image2021-6-22_20-56-37 (1).png

image2021-6-22_20-57-34 (1).png

image2021-6-22_20-58-33.png

此時通過上邊的一輪處理,html已是這個樣子了,由於每次都有 advance 前進:

image2021-6-22_20-59-18.png

也就是關於最開始的根標籤 div 的開始部分 <div id="app"> 已經處理完成了。

接着進入到 handleStartTag 的邏輯中

image2021-6-22_21-3-52.png

此時,stack 棧中已經 push 了一個元素,即咱們的開始標籤 div,也保存了相關的位置和屬性信息,lastTag 指向的就是 div。

接着繼續 while 循環處理

image2021-6-22_21-8-5.png

由於有空格和換行的關係,此時 textEnd 的值是 3,因此要進入到文本的處理邏輯(空格和換行原本就屬於文本內容)

image2021-6-22_21-11-19.png

因此這輪循環會處理好文本,而後進入下一次循環操做,此時已經和咱們第一輪循環的效果差很少:

image2021-6-22_21-14-49.png

image2021-6-22_21-15-57.png

再次lastTag變爲了 p,而後進入處處理文本(空格、換行)的邏輯,這裏直接省略,過程是同樣的;

下面直接跳到第一次處理 span

image2021-6-22_21-19-18.png

其實仍是重複和第一次的循環同樣,處理普通元素,處理完成後的結果:

image2021-6-22_21-20-28.png

此時棧頂的元素是外部的這個 span。而後進入新一輪的處理文本:

image2021-6-22_21-22-25.png

接着再一次進入處理裏層的 span 元素,同樣的邏輯,處理完成後

image2021-6-22_21-24-15.png

而後處理最裏層的文本,結束後,到達最裏層的結束標籤 </span>

這個時候咱們重點看下這一輪的循環:

image2021-6-22_21-26-33.png

image2021-6-22_21-27-7.png

image2021-6-22_21-28-29.png

image2021-6-22_21-28-58.png

image2021-6-22_21-30-21.png

能夠看到通過這一圈處理,最裏層的 span 已經通過閉合處理,棧和lastTag已經更新爲了外層的 span 了。

剩下的循環的流程,相信你已經可以大概猜到了,一直是處理文本內容(換行 空格)以及 parseEndTag 相關處理,一次次的出棧,直到 html 字符串處理完成,爲空,即中止了循環處理。

十分相似的原理,咱們的 parse 函數也是同樣的,根據 parseHTML 的鉤子函數,一次次的壓榨,處理,而後出棧 處理,直至完成,這些鉤子作的核心事情就是根據 parse HTML 的過程當中,一步步構建本身的 ast,那麼最終的 ast 結果

image2021-6-22_21-40-30.png

到這裏 parse 的階段已經完全完成。

2. generate

接下來看看如何根據上述的 ast 獲得咱們想要的 render 函數。相關的代碼在 github.com/vuejs/vue/b…

export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult {
  const state = new CodegenState(options)
  // fix #11483, Root level <script> tags should not be rendered.
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
複製代碼

能夠看出,generate 核心,第一步建立了一個 CodegenState 實例,沒有很具體的功能,約等因而配置項的處理,而後進入核心邏輯 genElement,相關代碼 github.com/vuejs/vue/b…

// 生成元素代碼
export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
 
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }
 
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}
複製代碼

基本上就是根據元素類型進行對應的處理,依舊是上邊的示例的話,會進入到

image2021-6-22_21-52-8.png

接下來會是一個重要的 genChildren github.com/vuejs/vue/b…

export function genChildren ( el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function ): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }`
  }
}
複製代碼

能夠看出,基本上是循環 children,而後 調用 genNode 生成 children 的代碼,genNode github.com/vuejs/vue/b…

function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}
複製代碼

這裏就是判斷每個節點類型,而後基本遞歸調用 genElement 或者 genComment、genText 來生成對應的代碼。

最終生成的代碼 code 以下:

image2021-6-22_22-2-39.png

能夠理解爲,遍歷上述的 ast,分別生成他們的對應的代碼,藉助於遞歸,很容易的就處理了各類狀況。固然,有不少細節這裏其實被咱們忽略掉了,主要仍是看的正常狀況下的核心的大概簡要流程,便於理解。

到此,這就是在 Vue 中是如何處理編譯模板到 render 函數的完整過程。

Why

要找到背後的緣由,咱們能夠拆分爲兩個點:

  • 爲何要引入 Virtual DOM
  • 爲何推薦模板(將模板轉換爲render函數,獲得 vnode 數據)

爲何要引入 Virtual DOM

這個問題其實尤大本人本身講過,爲何在 Vue 2 中引入 Virtual DOM,是否是有必要的等等。

來自方應杭的聚合回答:

v2-916e42ce8034a05f783b84318ef08beb_r.jpeg

這裏有一些文章和回答供參考(也包含了別人的總結部分):

爲何推薦模板

這個在官網框架對比中有講到,原文 cn.vuejs.org/v2/guide/co…

image2021-6-22_23-40-37.png

固然,除了上述緣由以外,就是咱們在前言中提到的,模板是靜態的,Vue 能夠作針對性的優化,進而利用 AOT 技術,將運行時性能進一步提高。

這個也是爲何 Vue 中有構建出來了不一樣的版本,詳細參見 cn.vuejs.org/v2/guide/in…

總結

經過上邊的分析,咱們知道在 Vue 中,template到render函數的大概過程,最核心的仍是:

  • 解析 HTML 字符串,獲得本身定義的 AST
  • 根據 AST,生成最終的 render 函數代碼

這個也是編譯器作的最核心的事情。

那麼咱們能夠從中學到什麼呢?

編譯器

編譯器,聽起來就很高大上了。經過咱們上邊的分析,也知道了在 Vue 中是如何處理的。

編譯器的核心原理和相比較的標準化的過程基本上仍是比較成熟的,無論說這裏分析和研究的對於 HTML 的解析,而後生成最終的 render 函數代碼,仍是其餘任何的語言,或者是你本身定義的」語言「都是能夠的。

想要深刻學習的話,最好的就是看編譯原理。在社區中,也有一個很出名的項目 github.com/jamiebuilds… 裏邊有包含了一個」五臟俱全「的編譯器,核心只有 200 行代碼,裏邊除了代碼以外,註釋也是精華,甚至於註釋比代碼更有用,很值得咱們去深刻學習和研究,且易於理解。

樹的這種數據結構,上述咱們經過parse獲得的 ast 其實就是一種樹狀結構,樹的應用,基本上隨處可見,只要你善於發現。利用他,能夠很好的幫助咱們進行邏輯抽象,統一處理。

在上述的分析中,咱們是屢次看到了對於棧的運用,以前在響應式原理中也有提到過,可是在這裏是一個十分典型的場景,也能夠說是棧這個數據結構的最佳實踐之一。

基本上你在社區中不少的框架或者優秀庫中,都能看到棧的相關應用的影子,能夠說是一個至關有用的一種數據結構。

鉤子

咱們在 parseHTML 的 options 中看到了鉤子的應用,其實不止是這裏有用到這種思想。經過 parseHTML 對外暴露的鉤子函數 start、end、chars、comment 能夠很方便的讓使用者鉤入到 parseHTML 的執行邏輯當中,相信你也感覺到了,這是一種頗有簡單,可是確實很實用的思想。固然,這種思想自己,也經常和插件化設計方案或者叫微內核的架構設計一塊兒出現;針對於不一樣的場景,能夠有更復雜一些的實現,進而提供更增強大的功能,例如在 webpack 中,底層的 tapable 庫,本質也是這種思想的應用。

正則表達式

在整個的parser過程當中,咱們遇到了不少種使用正則的場景,尤爲是在 github.com/vuejs/vue/b… 這裏:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
複製代碼

這裏邊仍是包含了不少種正則的使用,也有正則的動態生成。正則自己有簡單的,有複雜的,若是你不能很好的理解這裏的正則,推薦你去看精通正則表達式這本書,相信看過以後,你會收穫不少。

其餘小Tips

  • 目錄 模塊拆分,依舊值得咱們好好學習
  • ast 優化操做,雖然上邊沒有詳細分析,可是在源碼中仍是專門去作了 ast 優化相關的事情的
  • 簡單工廠模式的使用 Creator
  • staticRenderFns,做用是啥,爲啥會有它
  • 緩存技術的再次利用,提高性能
  • 避免重複處理,各類標記的運用
  • 由於涉及到HTML解析,因此仍是有必要了解下 HTML 規範的,以及常規的瀏覽器解析 HTML 的容錯處理,源碼中的一些工具也有體現 github.com/vuejs/vue/b…
  • makeMap 的做用,在 Vue 中大量使用

滴滴前端技術團隊的團隊號已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。

相關文章
相關標籤/搜索