【大型乾貨】來看看vue對template做了什麼(附部分原始碼及註釋)

JserWang發表於2018-04-26

在整理這篇文章時,我感到了困惑,困惑的是不知道該怎麼用更好的方式來將這部分繁瑣的內容讓你很容易的看的明白。我也希望這篇文章能作為你的在閱讀時引導,你可以一起邊看引導,邊看原始碼。

如何找到我們需要關注的最終方法

還記得之前的《手拉手帶你過一遍vue部分原始碼》嗎?在那裡,我們已經知道,在src/platform/web/entry-runtime-with-compiler.js重寫原型的$mount方法時,已經將template轉換為render函式了,接下來,我們從這個地方作為入口。

  const { render, staticRenderFns } = compileToFunctions(template, {
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
  }, this)
複製程式碼

compileToFunctions從哪來的?說真的 這個問題看原始碼的時候 還挺繞的,首先引自於platforms/web/compiler/index.js,然後發現是呼叫了src/compiler/index.jscreateCompiler方法,而createCompiler又是通過呼叫src/compiler/create-compiler.jscreateCompilerCreator方法,而我們使用的compileToFunctions方法呢,又是通過呼叫src/compiler/to-function.js中的createCompileToFunctionFn來建立的,所以,這裡,我們為了好記,暫時忽略中間的所有步驟。 先從最後一步開始看吧。

createCompileToFunctionFn(src/compiler/to-function.js)

程式碼有點多久不在這裡不全貼出來了,我說,你看著。

try {
    new Function('return 1')
} catch (e) {
	if (e.toString().match(/unsafe-eval|CSP/)) {
	  warn(
	    'It seems you are using the standalone build of Vue.js in an ' +
	    'environment with Content Security Policy that prohibits unsafe-eval. ' +
	    'The template compiler cannot work in this environment. Consider ' +
	    'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
	    'templates into render functions.'
	  )
	}
}
複製程式碼

這段程式碼做了什麼?在當前情景下,相當於eval('function fn() {return 1},檢測當前網頁的內容安全政策,具體CSP是什麼,可以看一下阮老師的CSP。這裡為什麼要做檢測?好問題,先記住,繼續往下看,你會自己得到答案。

const key = options.delimiters
  ? String(options.delimiters) + template
  : template
if (cache[key]) {
  return cache[key]
}
複製程式碼

又一段程式碼來為了提高效率,直接從快取中查詢是否有已經編譯好的結果,有則直接返回。

const compiled = compile(template, options)
複製程式碼

這裡的compile方法就需要看src/compiler/create-compiler.js檔案的createCompilerCreator中的function compile了。

compile(src/compiler/create-compiler.js)

在compile方法裡,主要做了做了3件事

  1. 將傳入的CompilerOptions經過處理後掛載至finalOptions

    這裡finalOptions最終會變成:

【大型乾貨】來看看vue對template做了什麼(附部分原始碼及註釋)

  1. templatefinalOptions傳入src/compiler/index.js檔案的baseCompile中。
  2. 收集ast轉換時的錯誤資訊。

baseCompile(src/compiler/index.js)

這裡我們進入到baseCompile中,看看baseCompile做了什麼。

parse
// 將傳入html 轉換為ast語法樹
const ast = parse(template.trim(), options)
複製程式碼

劃重點啦,通過parse方法將我們傳入的template中的內容,轉換為AST語法樹

一起來看下src/compiler/parser/index.js檔案中的parse方法。

function parse (template, options){
  warn = options.warn || baseWarn

  platformIsPreTag = options.isPreTag || no
  platformMustUseProp = options.mustUseProp || no
  platformGetTagNamespace = options.getTagNamespace || no

  // pluckModuleFunction:找出options.mudules中每一項中屬性含有key方法
  transforms = pluckModuleFunction(options.modules, 'transformNode')
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

  delimiters = options.delimiters
  // 存放astNode
  const stack = []
  const preserveWhitespace = options.preserveWhitespace !== false
  // 定義根節點
  let root
  // 當前處理節點的父節點
  let currentParent
  // 標識屬性中是否有v-pre的
  // 什麼是v-pre,見 https://cn.vuejs.org/v2/api/#v-pre
  let inVPre = false
  // 標識是否為pre標籤
  let inPre = false
  // 標識是否已經觸發warn
  let warned = false

  function warnOnce (msg) {}

  function closeElement (element) {}

  // 通過迴圈的方式解析傳入html
  parseHTML(params)
  /**
	* 處理v-pre
	* @param {*} el
	*/
  function processPre() {}
  
  /**
   * 處理html原生屬性
   * @param {*} el
   */
  function processRawAttrs (el) {}
  return root
}
複製程式碼

從上面部分我們可以看出,實際做轉換的是parseHTML方法,我們在上面省略了parseHTML的引數,因為在parseHTML方法內部,會用到引數中的startendcharscomment方法,所以為了防止大家迷惑,在這裡我會在文章的最後,拆出來專門為每個方法提供註釋,方便大家閱讀。

這裡先進入src/compiler/parser/html-parser.js中看看parseHTML方法吧。

function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  // 宣告index,標識當前處理傳入html的索引
  let index = 0
  let last, lastTag
  // 迴圈處理html
  while (html) {...}

  // Clean up any remaining tags
  parseEndTag()
  /**
   * 修改當前處理標記索引,並且將html已處理部分擷取掉
   * @param {*} n 位數
   */
  function advance (n) {}

  /**
   * 處理開始標籤,將屬性放入attrs中
   */
  function parseStartTag () {}

  /**
   * 將parseStartTag處理的結果進行處理並且通過options.start生成ast node
   * @param {*} match 通過parseStartTag處理的結果
   */
  function handleStartTag (match) {}

  /**
   * 處理結束標籤
   * @param {*} tagName 標籤名
   * @param {*} start 在html中起始位置
   * @param {*} end 在html中結束位置
   */
  function parseEndTag (tagName, start, end) {}
}
複製程式碼

這裡我們保留了部分片段,完整的註釋部分,我會放在文章的最後。

通過parse方法,我們將整個抽象語法樹拿到了。

optimize

對當前抽象語法樹進行優化,標識出靜態節點,這部分我們下一篇關於vNode的文章會再提到。

generate(scr/compiler/codegen/index.js)

這部分會將我們的抽象語法樹,轉換為對應的render方法的字串。有興趣的可以自行翻閱,看這部分時,你會更清晰一點 在vue instance時,為原型掛載了各種_字母的方法的用意

沒錯 你沒看做,這裡轉換的是with(this){...}的字串,所以上面為什麼在編譯template會檢測是否允許使用eval是不是有眉目了?。

小結

通過compile中的parseoptimizegeneratetemplate轉換為了render

最後

如果你喜歡,我會繼續為你帶來render時,虛擬DOM相關的文章。

附錄—原始碼+註釋

options.start

start (tag, attrs, unary) {
  // check namespace.
  // inherit parent ns if there is one
  const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

  // handle IE svg bug
  /* istanbul ignore if */
  // 處理IE的svg的BUG
  if (isIE && ns === 'svg') {
    attrs = guardIESVGBug(attrs)
  }

  // 建立AST NODE
  let element: ASTElement = createASTElement(tag, attrs, currentParent)
  if (ns) {
    element.ns = ns
  }

  // 判斷當前節點如果是<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.'
    )
  }

  // apply pre-transforms
  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
  }
  // 如果含有v-pre,則跳過編譯過程
  if (inVPre) {
    processRawAttrs(element)
  } else if (!element.processed) {
    // structural directives
    // 處理v-for
    processFor(element)
    // 處理v-if v-else-if v-else
    processIf(element)
    // 處理v-once
    processOnce(element)
    // element-scope stuff
    // 處理ast node節點,key、ref、slot、component、attrs
    processElement(element, options)
  }

  // root節點約束檢測
  function checkRootConstraints (el) {
    if (process.env.NODE_ENV !== 'production') {
      // slot、template不能作為root
      if (el.tag === 'slot' || el.tag === 'template') {
        warnOnce(
          `Cannot use <${el.tag}> as component root element because it may ` +
          'contain multiple nodes.'
        )
      }
      // root節點中不能存在v-for
      if (el.attrsMap.hasOwnProperty('v-for')) {
        warnOnce(
          'Cannot use v-for on stateful component root element because ' +
          'it renders multiple elements.'
        )
      }
    }
  }

  // tree management
  if (!root) {
    root = element
    checkRootConstraints(root)
  } else if (!stack.length) {
    // allow root elements with v-if, v-else-if and v-else
    if (root.if && (element.elseif || element.else)) {
      checkRootConstraints(element)
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      warnOnce(
        `Component template should contain exactly one root element. ` +
        `If you are using v-if on multiple elements, ` +
        `use v-else-if to chain them instead.`
      )
    }
  }
  if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) {
      processIfConditions(element, currentParent)
    } else if (element.slotScope) { // scoped slot
      currentParent.plain = false
      const name = element.slotTarget || '"default"'
      ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
    } else {
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }
  if (!unary) {
    currentParent = element
    stack.push(element)
  } else {
    closeElement(element)
  }
}
複製程式碼

options.end

end () {
  // remove trailing whitespace
  // 拿到stack中最後一個ast node
  const element = stack[stack.length - 1]
  // 找到最近處理的一個節點
  const lastNode = element.children[element.children.length - 1]

  if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
    element.children.pop()
  }
  // pop stack
  // 將element從stack移除
  stack.length -= 1
  currentParent = stack[stack.length - 1]
  closeElement(element)
}
複製程式碼

options.chars

chars (text: string) {
  // 文字沒有父節點處理
  if (!currentParent) {
    if (process.env.NODE_ENV !== 'production') {
      if (text === template) {
        warnOnce(
          'Component template requires a root element, rather than just text.'
        )
      } else if ((text = text.trim())) {
        warnOnce(
          `text "${text}" outside root element will be ignored.`
        )
      }
    }
    return
  }
  // IE textarea placeholder bug
  /* istanbul ignore if */
  if (isIE &&
    currentParent.tag === 'textarea' &&
    currentParent.attrsMap.placeholder === text
  ) {
    return
  }
  const children = currentParent.children
  // 格式化text
  text = inPre || text.trim()
    ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
    // only preserve whitespace if its not right after a starting tag
    : preserveWhitespace && children.length ? ' ' : ''
  if (text) {
    // 處理{{text}}部分,將{{text}}轉為
    // {expression: '_s(text)', token: [{'@binding': 'text'}]}
    let res
    if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
      children.push({
        type: 2,
        expression: res.expression,
        tokens: res.tokens,
        text
      })
    } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
      children.push({
        type: 3,
        text
      })
    }
  }
}
複製程式碼

parseHTML

function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  // 宣告index,標識當前處理傳入html的索引
  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)) {
      let textEnd = html.indexOf('<')
      // 是否以<開頭
      if (textEnd === 0) {
        // Comment:
        // 判斷是否是<!-- 開頭的註釋
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')

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

        // 判斷是否為相容性註釋以 <![ 開頭
        // <!--[if IE 6]>
        // Special instructions for IE 6 here
        // <![endif]-->
        // 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 開頭
        // Doctype:
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        // 判斷是否為結束標籤
        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // 通過parseStartTag後,會將html = '<div id="demo">...</div>'
        // 的標籤處理為 html = '...<div>'
        // 返回的match = {
        //  start: 0, // 開始索引
        //  end: 15, // 結束索引
        //  tagName: 'div'
        //  attrs: [] // 這裡的陣列為正則匹配出來標籤的attributes
        // }
        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(lastTag, html)) {
            advance(1)
          }
          continue
        }
      }

      let text, rest, next
      // 這裡由於我們的html程式碼可能會有製表位 換行等不需要解析的操作
      // 這裡將無用的東西移除,然後繼續迴圈html
      if (textEnd >= 0) {
        rest = html.slice(textEnd)
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // < in plain text, be forgiving and treat it as text
          next = rest.indexOf('<', 1)
          if (next < 0) break
          textEnd += next
          rest = html.slice(textEnd)
        }
        text = html.substring(0, textEnd)
        advance(textEnd)
      }

      if (textEnd < 0) {
        text = html
        html = ''
      }
      // 處理字元
      if (options.chars && text) {
        options.chars(text)
      }
    } else {
      let endTagLength = 0
      const stackedTag = lastTag.toLowerCase()
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
      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)
    }

    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}"`)
      }
      break
    }
  }

  // Clean up any remaining tags
  parseEndTag()
  /**
   * 修改當前處理標記索引,並且將html已處理部分擷取掉
   * @param {*} n 位數
   */
  function advance (n) {
    index += n
    html = html.substring(n)
  }

  /**
   * 處理開始標籤,將屬性放入attrs中
   */
  function parseStartTag () {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      // 處理完頭部資訊,將頭部移除掉
      advance(start[0].length)
      let end, attr
      // 迴圈找尾部【>】,如果沒有到尾部時,就向attrs中新增當前正則匹配出的屬性。
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push(attr)
      }
      // 將尾部【>】從html中移除,記錄當前處理完的索引
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }

  /**
   * 將parseStartTag處理的結果進行處理並且通過options.start生成ast node
   * @param {*} match 通過parseStartTag處理的結果
   */
  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)
      }
    }

    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]
      // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
      if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
        if (args[3] === '') { delete args[3] }
        if (args[4] === '') { delete args[4] }
        if (args[5] === '') { delete args[5] }
      }
      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 (!unary) {
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
      lastTag = tagName
    }

    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  /**
   * 處理結束標籤
   * @param {*} tagName 標籤名
   * @param {*} start 在html中起始位置
   * @param {*} end 在html中結束位置
   */
  function parseEndTag (tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index

    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
    }

    // Find the closest opened tag of the same type
    // 從stack中找到與當前tag匹配的節點,這裡利用倒序,匹配最近的
    if (tagName) {
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break
        }
      }
    } else {
      // If no tag name is provided, clean shop
      pos = 0
    }

    if (pos >= 0) {
      // Close all the open elements, up the stack
      for (let i = stack.length - 1; i >= pos; i--) {
        if (process.env.NODE_ENV !== 'production' &&
          (i > pos || !tagName) &&
          options.warn
        ) {
          options.warn(
            `tag <${stack[i].tag}> has no matching end tag.`
          )
        }
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }

      // Remove the open elements from the stack
      stack.length = pos
      lastTag = pos && stack[pos - 1].tag
      // 處理br
    } else if (lowerCasedTagName === 'br') {
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
      // 處理p標籤
    } else if (lowerCasedTagName === 'p') {
      if (options.start) {
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        options.end(tagName, start, end)
      }
    }
  }
}
複製程式碼

相關文章