淺析Vue源碼(四)—— $mount中template的編譯--parse

$mount

mount是什麼?--mount是手動加載的過程,接下來讓咱們看看具體是怎麼實現的:html

src/platforms/web/entry-runtime-with-compiler.js
複製代碼
/*把本來不帶編譯的$mount方法保存下來,在最後會調用。*/
const mount = Vue.prototype.$mount
/*掛載組件,帶模板編譯*/
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* 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
  /*處理模板templete,編譯成render函數,render不存在的時候纔會編譯template,不然優先使用render*/
  if (!options.render) {
    let template = options.template
     /*template存在的時候取template,不存在的時候取el的outerHTML*/
    if (template) {
    /*當template的類型是字符串時*/
      if (typeof template === 'string') {
        /*檢索到template的首字母是#時判斷爲id*/
        if (template.charAt(0) === '#') {
          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爲DOM節點的時候*/
        template = template.innerHTML
      } else {
      /*報錯*/
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
    /*獲取element的outerHTML*/
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
     /*將template編譯成render函數,這裏會有render以及staticRenderFns兩個返回,這是vue的編譯時優化,static靜態不須要在VNode更新時進行patch,優化性能*/
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      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')
      }
    }
  }
   /*調用const mount = Vue.prototype.$mount保存下來的不帶編譯的mount*/
  return mount.call(this, el, hydrating)
}
複製代碼

經過mount編譯代碼咱們清晰的瞭解到,在mount的過程當中,若是render函數不存在(render函數存在會優先使用render)會將template進行compileToFunctions獲得render以及staticRenderFns。譬如說手寫組件時加入了template的狀況都會在運行時進行編譯。而render function在運行後會返回VNode節點,供頁面的渲染以及在update的時候patch。接下來咱們來看一下template是如何編譯的。vue

compile 函數(src/compiler/index.js)就是將 template 編譯成 render function 的字符串形式。接下來就詳細講解這個函數:node

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 {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
複製代碼

createCompiler 函數主要經過3個步驟:parse、optimize、generate來生成一個包含ast、render、staticRenderFns的對象。git

parse

在說parse函數以前,咱們先來了解一個概念:AST(Abstract Syntax Tree)抽象語法樹: 在計算機科學中,抽象語法樹(abstract syntax tree或者縮寫爲AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。具體能夠查看抽象語法樹github

AST會通過generate獲得render函數,render的返回值是VNode,VNode是Vue的虛擬DOM節點,具體定義以下:web

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { /*當前節點的標籤名*/ this.tag = tag /*當前節點對應的對象,包含了具體的一些數據信息,是一個VNodeData類型,能夠參考VNodeData類型中的數據信息*/ this.data = data /*當前節點的子節點,是一個數組*/ this.children = children /*當前節點的文本*/ this.text = text /*當前虛擬節點對應的真實dom節點*/ this.elm = elm /*當前節點的名字空間*/ this.ns = undefined /*編譯做用域*/ this.context = context /*函數化組件做用域*/ this.functionalContext = undefined /*節點的key屬性,被看成節點的標誌,用以優化*/ this.key = data && data.key /*組件的option選項*/ this.componentOptions = componentOptions /*當前節點對應的組件的實例*/ this.componentInstance = undefined /*當前節點的父節點*/ this.parent = undefined /*簡而言之就是是否爲原生HTML或只是普通文本,innerHTML的時候爲true,textContent的時候爲false*/ this.raw = false /*靜態節點標誌*/ this.isStatic = false /*是否做爲跟節點插入*/ this.isRootInsert = true /*是否爲註釋節點*/ this.isComment = false /*是否爲克隆節點*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false /*異步組件的工廠方法*/ this.asyncFactory = asyncFactory /*異步源*/ this.asyncMeta = undefined /*是否異步的預賦值*/ this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } 複製代碼

接下來咱們來看看parse的源碼:正則表達式

src/compiler/parser/index.js
複製代碼
function parse(template) {
    ...
    const stack = [];
    let currentParent;    //當前父節點
    let root;            //最終返回出去的AST樹根節點
    ...
    parseHTML(template, {
        start: function start(tag, attrs, unary) {
           ......
        },
        end: function end() {
          ......
        },
        chars: function chars(text) {
           ......
        }
    })
    return root
}
複製代碼

這個方法太長啦,就省略了parse的相關內容,只看一下大致的功能,其主要的功能函數應該是parseHTML方法。接受了2個參數,一個使咱們的模板template,另外一個是包含start、end、chars的方法。 在看parseHTML以前,咱們須要先了解一下下面這幾個正則:編程

// 該正則式可匹配到 <div id="index"> 的 id="index" 屬性部分
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 匹配起始標籤
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
// 匹配結束標籤
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 匹配DOCTYPE、註釋等特殊標籤
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
複製代碼

Vue 經過上面幾個正則表達式去匹配開始結束標籤、標籤名、屬性等等。有了上面這些基礎,咱們再來看看parseHtml的內部實行:數組

export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  while (html) {
    // 保留 html 副本
    last = html
    // 若是沒有lastTag,並確保咱們不是在一個純文本內容元素中:script、style、textarea
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        // Comment:
        if (comment.test(html)) {
          ...
        }
        if (conditionalComment.test(html)) {
          ...
        }
        // Doctype:
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          ...
        }
        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          ...
        }
        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          ...
        }
      }
      let text, rest, next
      if (textEnd >= 0) {
        ...
      }
      if (textEnd < 0) {
        text = html
        html = ''
      }
      // 繪製文本內容,使用 options.char 方法。
      if (options.chars && text) {
        options.chars(text)
      }
    } else {
      ...
    }
    ...
  }
複製代碼

上面只看一下代碼的大概意思:bash

1.首先經過while (html)去循環判斷html內容是否存在。

2.再判斷文本內容是否在script/style標籤中。

3.上述條件都知足的話,開始解析html字符串。

這裏面有parseStartTag 和 handleStartTag兩個方法值得關注一下:

function parseStartTag () {
  //判斷html中是否存在開始標籤
  const start = html.match(startTagOpen)
  if (start) {
    // 定義 match 結構
    const match = {
      tagName: start[1], // 標籤名
      attrs: [], // 屬性名
      start: index // 起點位置
    }

    /**
     * 經過傳入變量n來截取字符串,這也是Vue解析的重要方法,經過不斷地蠶食掉html字符串,一步步完成對他的解析過程
     */
    advance(start[0].length)
    let end, attr

    // 若是尚未到結束標籤的位置
    // 存入屬性
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
      advance(attr[0].length)
      match.attrs.push(attr)
    }
    // 返回處理後的標籤match結構
    if (end) {
      match.unarySlash = end[1]
      advance(end[0].length)
      match.end = index
      return match
    }
  }
}
複製代碼

假設咱們設置一個html字符串

{{msg}}
,通過上面一步的解析,咱們獲得了一個起始標籤match的數據結構:

function handleStartTag (match) {
  // match 是上面調用方法的時候傳遞過來的數據結構
  const tagName = match.tagName
  const unarySlash = match.unarySlash
  ...
  const unary = isUnaryTag(tagName) || !!unarySlash

  // 備份屬性數組的長度
  const l = match.attrs.length
  // 構建長度爲1的空數組
  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] || ''

    // 改變attr的格式爲 [{name: 'id', value: 'demo'}]
    attrs[i] = {
      name: args[1],
      value: decodeAttr(
        value,
        options.shouldDecodeNewlines
      )
    }
  }

  // stack中記錄當前解析的標籤
  // 若是不是自閉和標籤
  // 這裏的stack這個變量在parseHTML中定義,做用是爲了存放標籤名 爲了和結束標籤進行匹配的做用。
  if (!unary) {
    stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
    lastTag = tagName
  }
  // parse 函數傳入的 start 方法
  options.start(tagName, attrs, unary, match.start, match.end)
}
複製代碼

到這裏彷佛一切明朗了許多,parseHTML主要用來解析html字符串,解析出字符串中的tagName,attrs,match等元素,傳入start方法:

start (tag, attrs, unary) {
  ...
  // 建立基礎的 ASTElement
  let element: ASTElement = createASTElement(tag, attrs, currentParent)
  if (ns) {
    element.ns = ns
  }
  ...

  if (!inVPre) {
    // 判斷有沒有 v-pre 指令的元素。若是有的話 element.pre = true
    // 官網有介紹:<span v-pre>{{ this will not be compiled }}</span>
    // 跳過這個元素和它的子元素的編譯過程。能夠用來顯示原始 Mustache 標籤。跳過大量沒有指令的節點會加快編譯。
    processPre(element)
    if (element.pre) {
      inVPre = true
    }
  }
  if (platformIsPreTag(element.tag)) {
    inPre = true
  }
  if (inVPre) {
    // 處理原始屬性
    processRawAttrs(element)
  } else if (!element.processed) {
    // structural directives
    // v-for v-if v-once
    processFor(element)
    processIf(element)
    processOnce(element)
    // element-scope stuff
    processElement(element, options)
  }

  // 檢查根節點約束
  function checkRootConstraints (el) {
    if (process.env.NODE_ENV !== 'production') {
      if (el.tag === 'slot' || el.tag === 'template') {
        warnOnce(
          `Cannot use <${el.tag}> as component root element because it may ` +
          'contain multiple nodes.'
        )
      }
      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) {
    // 容許有 v-if, v-else-if 和 v-else 的根元素
    ...
  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 {
      // 將元素插入 children 數組中
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }
  if (!unary) {
    currentParent = element
    stack.push(element)
  } else {
    endPre(element)
  }
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}
複製代碼

其實start方法就是處理 element 元素的過程。肯定命名空間;建立AST元素 element;執行預處理;定義root;處理各種 v- 標籤的邏輯;最後更新 root、currentParent、stack 的結果。 最終經過 createASTElement 方法定義了一個新的 AST 對象。

總結

下面咱們來屢一下parse總體的過程:

1.經過parseHtml來一步步解析傳入html字符串的標籤、元素、文本、註釋..。

2.parseHtml解析過程當中,調用傳入的start,end,chars方法來生成AST語法樹

咱們看一下最終生成的AST語法樹對象:

要是喜歡的話給我個star, github

感謝muwoo提供的解析思路。

相關文章
相關標籤/搜索