Vue parse之 從template到astElement 源碼詳解

前奏

在緊張的一個星期的整理,筆者的前端小組每一個人都整理了一篇文章,筆者整理了Vue編譯模版到虛擬樹的思想這一篇幅。建議讀者看到這篇以前,先點擊這裏預習一下整個流程的思想和思路。html

本文介紹的是Vue編譯中的parse部分的源碼分析,也就是從template 到 astElemnt的解析到程。前端

正文

從筆者的 Vue編譯思想詳解一文中,咱們已經知道編譯個四個流程分別爲parse、optimize、code generate、render。具體細節這裏不作贅述,附上以前的一張圖。vue

編譯流程圖

本文則旨在從思想落實到源代碼分析,固然只是針對parse這一部分的。node

1、 源碼結構。

筆者先列出咱們在看源碼以前,須要先預習的一些概念和準備。ios

準備

1.正則

parse的最終目標是生成具備衆多屬性的astElement樹,而這些屬性有不少則摘自標籤的一些屬性。 如 div上的v-for、v-if、v-bind等等,最終都會變成astElement的節點屬性。 這裏先給個例子:web

<div v-for="(item,index) in options" :key="item.id"></div>正則表達式

{
    alias: "item"
    attrsList: [],
    attrsMap: {"v-for": "(item,index) in options", :key: "item.id"},
    children: (2) [{…}, {…}],
    end: 139,
    for: "options",
    iterator1: "index",
    key: "item.id",
    parent: {type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …},
    plain: false,
    rawAttrsMap: {v-for: {…}, :key: {…}},
    start: 15,
    tag: "div",
    type: 1,
}
複製代碼

能夠看到v-for的屬性已經被解析和從摘除出來,存在於astElement的多個屬性上面了。而摘除的這個功能就是出自於正則強大的力量。下面先列出一些重要的正則預熱。express

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/  // 重要1
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 pased as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/

export const onRE = /^@|^v-on:/ 
export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\./
  : /^v-|^@|^:/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g  // 在v-for中去除 括號用的。
const dynamicArgRE = /^\[.*\]$/  // 判斷是否爲動態屬性

const argRE = /:(.*)$/ // 配置 :xxx
export const bindRE = /^:|^\.|^v-bind:/  // 匹配bind的數據,若是在組件上會放入prop裏面  不然放在attr裏面。
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g

const slotRE = /^v-slot(:|$)|^#/

const lineBreakRE = /[\r\n]/
const whitespaceRE = /\s+/g

const invalidAttributeRE = /[\s"'<>\/=]/ 複製代碼

正則基礎不太好的同窗能夠先學兩篇正則基礎文章,特別詳細:數組

而且附帶上兩個網站,供你們學習正則。瀏覽器

一次性看到這麼多正則是否是有點頭暈目眩。不要慌,這裏給你們詳細講解下比較複雜的幾條正則。

1)獲取屬性的正則

attribute 和 dynamicArgAttribute 分別獲取普通屬性和動態屬性的正則表達式。 普通屬性你們必定十分熟悉了,這裏對動態屬性作下解釋。

動態屬性,就是key值可能會發生變更的屬性,vue的寫法如 v-bind:[attrName]="attrVal",經過改變attrName來改變傳遞的屬性的key值。(非動態屬性只能修改val值)。

咱們先對attribute這個通用正則作一個詳細的講解:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

很長對不對??

可是細細的拆分的化,一共五個分組。

  • 1.([^\s"'<>/=]+)

這個分組是匹配 非空格、"、'、<、>、/、= 等符號的字符串。 主要會匹配到屬性的key值部分。以下面的屬性:

id="container"
複製代碼

([^\s"'<>/=]+)會匹配到id。

  • 2.\s*(=)\s* 這個是 匹配 = 號,固然了空格頁一併匹配了。好比下面的屬性:
id="container"
id = "container"
複製代碼

都會匹配到 = 號,第二個會把空格一塊兒匹配了。

  • 3."([^"])"正則一 、'([^'])'正則二 、([^\s"'=<>`]+)正則三 . 這三個正則分別匹配三種狀況 "val" 、'val' 、val。仍是繼續拿例子來說。
id="container" // exp1
id='container' // exp2
id=container // exp3
複製代碼

對於exp1正則一會匹配到"container", exp2正則2匹配到'container',exp3的話正則三會匹配到container。

Vue源碼的正則基本將大多數狀況都考慮在內了。

這樣的話應該比較清晰了,咱們來歸納下:

attribute匹配的一共是三種狀況, name="xxx" name='xxx' name=xxx。可以保證屬性的全部狀況都能包含進來。 須要注意的是正則處理後的數組的格式是:

['name','=','val','',''] 
或者
['name','=','','val',''] 
或者
['name','=','','','val'] 
複製代碼

下面講源碼的時候,會知道這種數組格式是attr屬性的原始狀態,parse後期會將這種屬性處理成attrMap的形式,大體以下:

{
    name:'xxx',
    id:'container'
}
複製代碼

關於這個正則,咱們附上一個講解圖:

而關於dynamicArgAttribute, 則是大同小異:

主要是多了\[[^=]+\][^\s"'<>\/=]* 也就是 [name] 或者 [name]key 這類狀況,附上正則詳解圖:

2)標籤處理正則

標籤主要包含開始標籤 (如<div>)和結束標籤(如</div>),正則分別爲如下兩個:

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}[^>]*>`)
複製代碼

可以看到標籤的匹配是以qnameCapture爲基礎的,那麼這玩意又是啥呢? 其實qname就是相似於xml:xxx的這類帶冒號的標籤,因此startTagOpen是匹配<div<xml:xxx的標籤。 endTag匹配的是如</div>或</xml:xxx>的標籤

3)處理vue的標籤
export const onRE = /^@|^v-on:/ 處理綁定事件的正則
export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\./  // v-   | @click | :name | .stop  指令匹配
  : /^v-|^@|^:/
複製代碼

一眼就能看出來,對不對?直接進入複雜的for標籤。

for 標籤比較重要,匹配也稍微複雜點,這裏作個詳解:

export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
複製代碼

首先申明這裏的正則是依賴於attribute正則的,咱們會拿到v-for裏面的內容,舉個例子v-for="item in options",咱們最終會處理成一個map的形式,大體以下:

const element = {
    attrMap: {
        'v-for':'item in options',
        ...
    }
}
複製代碼

也就是說咱們會在item in options的基礎上進行正則匹配。 先看forAliasRE的分組,一共兩個分組分別是([\s\S]*?)([\s\S]*) 會分別匹配 itemoptions。這裏舉的例子比較簡單。 實際上 inof以前的內容可能會比較複雜的,如(value,key) 或者(item,index)等,甚至可能(value,key,index),這個時候就是forIteratorRE開始起做用了。 它一共兩個分組都是([^,\}\]]*),其實就是拿到alias的最後兩個參數,你們都知道Vue對於Object的循環,是能夠這麼作的,例子以下:

<div v-for="(value,key,index)">
複製代碼

forIteratorRE則是爲了獲取keyindex的。最終會放在astElement的iterator1iterator2

{
    iterator1:',key',
    iterator2:',index'
}
複製代碼

好了關於正則就說這麼多了,具體的狀況仍是得本身去看看源碼的。

2.源碼結構

依然是在開始講源碼前,先大體介紹下源碼的結構。先貼個代碼出來

function parse() {
    模塊一:初始化須要的方法
    模塊二: 初始化全部標記
    模塊三: 開始識別並建立 astElement 樹。
}
複製代碼

模塊一大體是一些功能函數,給出代碼:

platformIsPreTag = options.isPreTag || no  //判斷是否爲 pre 標籤
  platformMustUseProp = options.mustUseProp || no // 判斷某個屬性是不是某個標籤的必要屬性,如selected 對於option
  platformGetTagNamespace = options.getTagNamespace || no  // 判斷是否爲 svg or math標籤 對函數
  const isReservedTag = options.isReservedTag || no // 判斷是否爲該平臺對標籤,目前vue源碼只有 web 和weex兩個平臺。
  maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) //是否可能爲組件

  transforms = pluckModuleFunction(options.modules, 'transformNode')  // 數組,成員是方法, 用途是摘取 staticStyle styleBinding staticClass classBinding
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') // ??
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') // ??

  delimiters = options.delimiters // express標誌
  
  function closeElement() {...} // 處理astElement對結尾函數
  function trimEndingWhitespace() {...} // 處理尾部空格
  function checkRootConstraints() {...} // 檢查root標籤對合格性
複製代碼

模塊二則是一些parse函數做用域內的全局標誌和存儲容器,代碼以下:

const stack = [] // 配合使用的棧 主要目的是爲了完成樹狀結構。
  
  let root // 根節點記錄,樹頂
  let currentParent // 當前父節點
  let inVPre = false // 標記是否在v-pre節點 當中
  let inPre = false // 是否在pre標籤當中
  let warned = false
複製代碼

模塊三是核心部分,也就是解析template的部分,這個函數一旦執行完, 模塊2的root會變成一顆以astElement爲節點的dom樹。

,其代碼大體爲:

parseHTML(template,options)
複製代碼

parseHTML函數和 options 是解析的關鍵,options包括不少平臺配置和 傳入的四個處理方法。大體以下:

options = {
    warn,
    expectHTML: options.expectHTML, // 是否指望和瀏覽器器保證一致。
    isUnaryTag: options.isUnaryTag, // 是否爲一元標籤的判斷函數
    canBeLeftOpenTag: options.canBeLeftOpenTag, // 能夠直接進行閉合的標籤
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments, // 是否保留註釋
    outputSourceRange: options.outputSourceRange,
    // 這裏分開,上面是平臺配置、下面是處理函數。
    start, // 解析處理函數(1)
    end, //解析處理函數(2)
    chars, //解析處理函數(3)
    commend //解析處理函數(4)
}
複製代碼

筆者以前的parse思想的文章,已經介紹過兩個處理函數start和end了,一個是建立astElement另外一個是創建父子關係,其中細節會在下文中,詳細介紹,這也是本文的重點。

chars函數處理的是文本節點,commend處理的則是註釋節點。 切記這四個函數相當重要,下面會用代號講解。

2、各模塊重點功能。

Vue的html解析並不是一步到位,先來介紹一些重點的函數功能

1.parseHTML函數內部功能函數詳細講解。

(1)解析開始標籤和處理屬性,生成初始化match。

前面咱們說到了startTagOpen是用來匹配開始標籤的。而parseHTML裏面的parseStartTag函數則是利用該正則,匹配開始標籤,創立一種初始的數據結構match,保存相應的屬性,對於開始標籤裏的全部屬性,如id、class、v-bind,都會保存到match.attr中。

代碼以下:

/**
   * 建立match數據結構
   * 初始化的狀態
   * 只有
   * tagName
   * attrs
   *    attrs本身是個數組 也就是 正則達到的效果。。
   * start
   * end
   */
  function parseStartTag () {
    const start = html.match(startTagOpen) // 匹配開始標籤。c
    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(dynamicArgAttribute) || html.match(attribute))) {
        attr.start = index
        advance(attr[0].length)
        attr.end = index
        match.attrs.push(attr)
      }
      if (end) {
        match.unarySlash = end[1] // 是否爲 一元標記 直接閉合
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }
複製代碼

上面的while中,咱們是用開始標籤的結束符做爲結束條件的。 startTagClose的正則是

const startTagClose = /^\s*(\/?)>/
複製代碼

它自己除了判斷是否已經結束,還有一個\/?是用來判斷是否爲一元標籤的。 一元標籤就是如<img/>能夠只寫一個標籤的元素。這個標記後面會用到。

parseStartTag的目標是比較原始的,得到相似於

const match = { // 匹配startTag的數據結構
        tagName: 'div',
        attrs: [
            { 'id="xxx"','id','=','xxx' },
            ...
        ],
        start: index,
        end: xxx
      }
複製代碼

match大體能夠歸納爲獲取標籤、屬性和位置信息。並將此傳遞給下個函數。

(2)handleStartTag處理parseStartTag傳遞過來的match。

// parseStartTag 拿到的是 match
  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++) { // 將attrs的 數組模式變成  { name:'xx',value:'xxx' }
      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) { // 非一元標籤處理方式
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      lastTag = tagName
    }

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

複製代碼

handleStartTag的自己效果其實很是簡單直接,就是吧match的attrs從新處理,由於以前是數組結構,在這裏他們將全部的數組式attr變成一個對象,流程大體以下:

從這樣:

attrs: [
        { 'id="xxx"','id','=','xxx' },
        ...
],
複製代碼

變成這樣:

attrs: [
        {name='id',value='xxx' },
        ...
],
複製代碼

那麼其實還有些特殊處理expectHTML一元標籤

expectHTML 是爲了處理一些異常狀況。如 p標籤的內部出現div等等、瀏覽器會特殊處理的狀況,而Vue會盡可能和瀏覽器保持一致。具體參考 p標籤標準

最後handleStartTag會調用 從parse傳遞的start(1)函數來作處理,start函數會在下文中有詳細的講解。

(3) parseEndTag

parseEndTag自己的功能特別簡單就是直接調用options傳遞進來的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 }
          )
        }
        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
    } else if (lowerCasedTagName === 'br') {
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') {
      if (options.start) {
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        options.end(tagName, start, end)
      }
    }
  }
}

複製代碼

看起來還蠻長的,其實主要都是去執行options.end, Vue的源碼有不少的代碼量都是在處理特殊狀況,因此看起來很臃腫。這個函數的特殊狀況主要有兩種:

  • 1.編寫者失誤,有標籤沒有閉合。會直接一次性和檢測的閉合標籤一塊兒進入options.end。 如:
<div>
        <span>
        <p>
    </div>
複製代碼

在處理div的標籤時,根據pos的位置,將pos以前的全部標籤和匹配到的標籤都會一塊兒遍歷的去執行end函數。

    1. p標籤和br標籤

可能會遇到</p></br>標籤 這個時候 p標籤會走跟瀏覽器自動補全效果,先start再end。 而br則是一元標籤,直接進入end效果。

2.start、end、comment、chars四大函數。

1)start函數

start函數很是長。這裏截取重點部分

start() {
    ...
    let element: ASTElement = createASTElement(tag, attrs, currentParent) // 1.建立astElement
    ...
    
      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
        processFor(element)
        processIf(element)
        processOnce(element)
      }

      if (!root) {
        root = element
        if (process.env.NODE_ENV !== 'production') {
          checkRootConstraints(root)
        }
      }

      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
}
複製代碼
  • 1).建立astElement節點。

結構以下:

{
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []
  }
複製代碼
  • 2)處理屬性 固然在這裏只是處理部分屬性,且分爲兩種狀況:

    (1)pre模式 直接摘取全部屬性

    (2)普通模式 分別處理processFor(element) 、processIf(element) 、 processOnce(element)。

    這些函數的詳細細節,後文會有講解,這裏只是讓你們有個印象。

2)end函數

end函數很是短

end (tag, start, end) {
      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
      }
      closeElement(element)
    },
複製代碼

end函數第一件事就是取出當前棧的父元素賦值給currentParent,而後執行closeElement,爲的就是可以建立完整的樹節點關係。 因此closeElement纔是end函數的重點。

下面詳細解釋下closeElement

function closeElement (element) {
    trimEndingWhitespace(element) // 去除 未部對空格元素
    if (!inVPre && !element.processed) {
      element = processElement(element, options) // 處理Vue相關的一些屬性關係
    }
    // tree management
    if (!stack.length && element !== root) {
      // allow root elements with v-if, v-else-if and v-else
      if (root.if && (element.elseif || element.else)) {
        if (process.env.NODE_ENV !== 'production') {
          checkRootConstraints(element)
        }
        addIfCondition(root, { // 處理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.`,
          { start: element.start }
        )
      }
    }
    if (currentParent && !element.forbidden) {
      if (element.elseif || element.else) { // 處理 elseif else 塊級
        processIfConditions(element, currentParent)
      } else {
        if (element.slotScope) { // 處理slot, 將生成的各個slot的astElement 用對象展現出來。
          // scoped slot
          // keep it in the children list so that v-else(-if) conditions can
          // find it as the prev node.
          const name = element.slotTarget || '"default"'
          ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
        }
        currentParent.children.push(element)
        element.parent = currentParent
      }
    }

    // final children cleanup
    // filter out scoped slots
    element.children = element.children.filter(c => !(c: any).slotScope)
    // remove trailing whitespace node again
    trimEndingWhitespace(element)

    // check pre state
    if (element.pre) {
      inVPre = false
    }
    if (platformIsPreTag(element.tag)) {
      inPre = false
    }
    // apply post-transforms
    for (let i = 0; i < postTransforms.length; i++) {
      postTransforms[i](element, options)
    }
  }
複製代碼

主要是作了五個操做:

  • 1.processElement。

processElement是closeElement很是重要的一個處理函數。先把代碼貼出來。

export function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  processKey(element)

  // determine whether this is a plain element after
  // removing structural attributes
  element.plain = (
    !element.key &&
    !element.scopedSlots &&
    !element.attrsList.length
  )

  processRef(element)
  processSlotContent(element)
  processSlotOutlet(element)
  processComponent(element)
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }
  processAttrs(element)
  return element
}
複製代碼

能夠看到主要是processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs和最後一個遍歷的執行的transforms。

咱們一個個來探討一下,給你們留個印象,實際上,後面會有案例詳細講解函數們的做用。

  • 1.首先最爲簡單的是processKey和processRef,在這兩個函數處理以前,咱們的key屬性和ref屬性都是保存在astElement上面的attrs和attrsMap,通過這兩個函數以後,attrs裏面的key和ref會被幹掉,變成astElement的直屬屬性。

  • 2.探討一下slot的處理方式,咱們知道的是,slot的具體位置是在組件中定義的,而須要替換的內容又是組件外面嵌套的代碼,Vue對這兩塊的處理是分開的。

先說組件內的屬性摘取,主要是slot標籤的name屬性,這是processSlotOutLet完成的。

// handle <slot/> outlets
function processSlotOutlet (el) {
  if (el.tag === 'slot') {
    el.slotName = getBindingAttr(el, 'name') // 就是這一句了。
    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')
      )
    }
  }
}
複製代碼

其次是摘取須要替換的內容,也就是 processSlotContent,這是是處理展現在組件內部的slot,可是在這個地方只是簡單的將給el添加兩個屬性做用域插槽的slotScope和 slotTarget,也就是目標slot。

processComponent 並非處理component,而是摘取動態組件的is屬性。 processAttrs是獲取全部的屬性和動態屬性。

transforms是處理class和style的函數數組。這裏不作贅述了。

  • 2.添加elseif 或else的block。

最終生成的的ifConditions塊級的格式大體爲:

[
    {
        exp:'showToast',
        block: castElement1
    },
    {
        exp:'showOther',
        block: castElement2
    },
    {
        exp: undefined,
        block: castElement3
    }
]
複製代碼

這裏會將條件展現處理成一個數組,exp存放全部的展現條件,若是是else 則爲undefined。

  • 3.處理slot,將各個slot對號入座到一個對象scopedSlots。

processElement完成的slotTarget的賦值,這裏則是將全部的slot建立的astElement以對象的形式賦值給currentParent的scopedSlots。以便後期組件內部實例話的時候能夠方便去使用vm.$$slot。有興趣的童鞋能夠去看看vm.$slot的初始化。

  • 4.處理樹到父子關係,element.parent = currentParent。

  • 5.postTransforms。

不作具體介紹了,感興趣的同窗本身去研究下吧。

3)chars函數

chars(){
    ...
    const children = currentParent.children
    ...
     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
          }
        }
}
複製代碼

chars主要處理兩中文本狀況,靜態文本和表達式,舉個例子:

<div>name</div>
複製代碼

name就是靜態文本,建立的type爲3.

<div>{{name}}</div>
複製代碼

而在這個裏面name則是表達式,建立的節點type爲2。

作個總結就是:普通tag的type爲1,純文本type爲2,表達式type爲3。

4)comment函數比較簡單

comment (text: string, start, end) {
      // adding anyting 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)
      }
    }
複製代碼

也是純文本,只是節點加上了一個isComment:true的標誌。

3.核心代碼parseHTML內部探索

上面完成了一些重要函數的講解,下面開始識別器的探索。

咱們的主要目的是瞭解parse的主要目的和過程。不會在一些細枝末節做太多贅述。

1)概覽

parseHTML函數的結構以下:

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) {
        last = html;
        ...
    }
    
    function advance (n) {
        index += n
        html = html.substring(n)
    }

}
複製代碼

parseHTML原理是用各個正則,不斷的識別並前進的的過程。舉個列子:

<div id="xxx">text<div>
複製代碼

startTagOpen會先匹配到<div,而後index會前進四個位置到4,並將html去掉前面到部分,而後匹配id="xxx",index前進了8個位置到了13,空格也會算一個位置,html去掉這一部分。而後匹配text,最後經過endTag正則匹配<div>。這樣就結束了。

固然了,匹配到到結果都是經過各個功能函數去處理。

2)標記

先介紹下各個參數的做用,在詳細瞭解while裏面的邏輯。

這裏的核心參數一共有stack、index、last、lastTag。

他們貫穿了整個匹配線路,index相信你們已經明白是起什麼做用的了。咱們這裏分析下其餘屬性的做用域。

  • 1)現看下stack的功能吧:

先看一個示例

<div>
    <span>
</div>
複製代碼

這種誤寫的狀況,若是按順序識別的話,那麼span標籤永遠不會獲得end函數的處理,由於沒有識別到閉合標籤。因此stack有着檢查錯誤的功能。

stack的處理方式是,識別到開始標籤就會推入stack。識別到閉合標籤就會把對應的閉合標籤推出來。

像上面那種狀況,當識別到到時候,咱們會發現,stack裏面上面到span,下面纔是div,咱們會把這兩個一塊兒處理掉。這樣能保證生成的astElement樹的結構包括span。

  • 2)last的做用

請你們思考一個問題,何時咱們纔會結束?

其實就是parseHTML函數不起做用了,換句話說就是while繞了一圈發現,index沒有變,html也沒有變。 剩下的部分,咱們會看成文本處理掉。

而這塊的邏輯就是:

while(html){
    last = html;
    ....
    ....
    if(last===html){
        optios.chars(html);
    }
}
複製代碼

有沒有恍然大悟的感受? 原來最後一步都是判斷中間的處理部分有沒有動html。last就是記錄處理前的樣式,而後在後面對比。沒有變更了就只剩下文本了。咱們直接當文本處理了。

    1. lastTag。

這個標記使用的地方特別多,記錄的是上個標籤。由於有些特殊的狀況,須要判斷上個標籤。 如p標籤,記錄了上個標籤是lastTag,若是裏面出現了div等標籤,咱們會從:

<p>
    <div></div>
</p>
複製代碼

變成:

<p></p>
<div></div>
<p></p>
複製代碼

緣由請參考這裏

3)while循環解析器之剖析

while的輪廓:

while(html) {
    last = html;
    if (!lastTag || !isPlainTextElement(lastTag)) {
        let textEnd = html.indexOf('<');
        
        if (textEnd === 0) {
            ... 模塊一
        }
        let text, rest, next
        if (textEnd >= 0) { 
            ... 模塊二
        }
        
        if (textEnd < 0) {
            text = html 
        }
        
        if (text) {
            advance(text.length)
        }

       if (options.chars && text) { // 空格等 通過這個函數處理爲文本節點
            options.chars(text, index - text.length, index) // 模塊三
       }
    } else {
        // 模塊四
    }
}
複製代碼

筆者將上面的代碼大體分爲四個模塊,咱們逐一來分析講解。

模塊一的代碼:
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
        }

        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
複製代碼

模塊一是在let textEnd = html.indexOf('<');的textEnd爲0的時候,才進入的。

模塊一的主要功能是匹配comment、conditionalComment、doctypeMatch、endTagMatch、startTagMatch五種狀況。他們的共同特性是匹配而且處理完後,會調用advance函數進行前進。

不一樣的是comment、endTagMatch、startTagMatch會分別進入options.comment、options.end和options.start函數。 comment函數比較簡單,這裏不作贅述來,讓咱們具體看endTagMatch和startTagMatch。

  • 1) 先看startTag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    advance(1)
  }
  continue
}
複製代碼

parseStartTag函數以前咱們有說過,除了匹配還會經過attribute正則摘取全部的屬性,並生成一個match對象。 格式以下:

match = {
    tagName:'xxx',
    attrs:[
        ['id','=','container','',''],
        ['v-if','=','show','','']
    ],
    start:xx,
    end: xx
}
複製代碼

而後把結果交給handleStartTag進行處理。 handleStartTag的功能前面也有說明,主要是將原始的正則匹配到到內容,格式一下:

attrs:[
        ['id','=','container','',''],
        ['v-if','=','show','','']
],
複製代碼

會變成:

attrs:[
    {name:'id',value:'container'},
    {name:'v-if',value:'show'}
]
複製代碼

並把類match結構推入到stack當中,最後執行了options.start函數。

  • 2)再看endTag
const endTagMatch = html.match(endTag)
if (endTagMatch) {
  const curIndex = index
  advance(endTagMatch[0].length) // 前進
  parseEndTag(endTagMatch[1], curIndex, index) // 進入
  continue
}
複製代碼

能夠看到匹配到endTag,主要是進入了parseEndTag函數。 前面已經說過,parseEndTag函數主要是判斷結束標籤,再stack到位置,並把stack尾部到這個位置之間到全部到標籤都經過options.end函數處理掉。options.end則使用closeElement去處理各個astElement到父子關係。

模塊二
let text, rest, next
  if (textEnd >= 0) { // 有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)
  }
複製代碼

模塊二主要是檢查下<符號以後的代碼,其中的全部非特殊代碼都賦值到text上,換言之,就是不斷檢查有咩有endTag、startTagOpen、comment等特殊狀況,一旦檢測到就中止,將前面到多是文本到部分賦值給text。而text會看成文本信息讓模塊三去處理。

模塊三

if (options.chars && text) { // 空格等 通過這個函數處理爲文本節點
    options.chars(text, index - text.length, index) // 模塊三
}
複製代碼

模塊三爲類文本信息,咱們會經過options.chars函數去處理,這個函數則會進一步,判斷是否存在表達式文本,就是咱們常常綁定到值如:

{{name}}
複製代碼

模塊四

這個模塊處理到是script或style標籤,這裏暫且不作贅述了,請你們自行去研究。

3、具體示例探索。

說了太多概念,難免會有些抽象,那麼直接給出一個具體的示例吧。

<div class="container" id="root">

    <div v-if="show">
          show attr bind
    </div>

    <div v-for="(item,index) in options" :key="item.id">
      <span>{{item.id}}</span> 
      <div>{{item.text}}</div>
    </div>
        
 </div>
複製代碼

剛進來到達while流程的是html就是完整的代碼:

html = "<div class="container" id="root"> <div v-if="show"> show attr bind </div> <div v-for="(item,index) in options" :key="item.id"> <span>{{item.id}}</span> <div>{{item.text}}</div> </div> </div>"
複製代碼

先經過parseStartTag解析<div class="container" id="root">,獲得的結果爲:

match = {

    attrs:[
         {
            0:class="container",
            1: "class",
            2: "=",
            3: "container",
            4: undefined,
            5: undefined,
            end: 22,
            groups: undefined,
            index: 0,
            input: " class="container" id="root">↵↵ <div v-if="show">↵ show attr bind↵ </div>↵↵ <div v-for="(item,index) in options" :key="item.id">↵ <span>{{item.id}}</span> ↵ <div>{{item.text}}</div>↵ </div>↵ ↵ </div>",
            start: 4
        },
        {
            0: " id="root"",
            1: "id"
            2: "="
            3: "root"
            4: undefined
            5: undefined
            end: 32
            groups: undefined
            index: 0
            input: " id="root">↵↵ <div v-if="show">↵ show attr bind↵ </div>↵↵ <div v-for="(item,index) in options" :key="item.id">↵ <span>{{item.id}}</span> ↵ <div>{{item.text}}</div>↵ </div>↵ ↵ </div>"
            start: 22
        },
    ],
    end: 33
    start: 0
    tagName: "div"
    unarySlash: ""
}
複製代碼

咱們能看到解析到每一個屬性,也就是attrs的對象的時候,都會用input去記錄還剩下的html。 而後將這個結果交給handleStartTag,去處理。

handleStartTag會將上面的attrs從新加工下,從數組變成:

[
    { //以前是數組的形式
        "name":"class",
        "value":"container",
        "start":5,
        "end":22
    },
    {"name":"id","value":"root","start":23,"end":32}
]
複製代碼

將相應的參數傳遞給options.start去處理。這個函數的入參大體以下:

options.start(
    tagName, // div
    attrs, // 上面處理過的attrs
    unary,  // 一元標籤
    match.start, // 開始
    match.end // 結束
)
複製代碼

那麼start函數自己呢,就去建立astElement,並處理掉v-for、v-if、v-once幾種標籤,這幾種標籤的處理方式,大體相同,從attrs去掉對應的屬性,而後直接給astElement自己建立新的屬性,下面給出處理後的格式以下:

  • 1.v-if
{   
    if:'show',
    ifConditions:[
        {
            exp:'show',
            block: astElement
        },
        {
            exp: 'show2',
            block: astElement
        },
        {
            exp: undefined,
            block: astElement
        }
    ],
}
複製代碼

猜猜上述對ifConditions的第三個exp的undefined會是什麼狀況?

其實就是v-else的處理方式。

神祕面紗能夠揭開了,關於v-if 、v-else-if 不會同時做爲父節點的chidren而存在,而是隻有一個children,那就是v-if,而後其餘的會存放在ifConditions裏面。

那麼它們在源碼的具體流程是怎麼樣子的?

// 1.遇到v-if節點,則在start函數中,使用processIf函數,添加ifConditions.
function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if') // 添加v-if屬性
  if (exp) { // 是if ,
    el.if = exp
    addIfCondition(el, { // 讓咱們直接爲astElement添加一個ifConditions屬性
      exp: exp,
      block: el
    })
  } else { // 不是v-if 只是 給節點加上  el.else 或  el.elseif
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
// 2. 那麼何時加上其餘條件的節點,別急別急,還記得前面的流程嗎,end函數裏面咱們會執行closeElement。
// 而這個函數有一個processIfConditions,若是不記得了,請翻上去看一看。
function processIfConditions (el, parent) {
  const prev = findPrevElement(parent.children) // 找到上一個節點,其實就是 倒數最後一個
  if (prev && prev.if) { // 若是上一個節點是if 那麼ok,咱們就是要把當前節點推到這個節點裏面。
    addIfCondition(prev, {
      exp: el.elseif,
      block: el
    })
  } else if (process.env.NODE_ENV !== 'production') { // 天吶,你寫錯了,v-else或v-else-if以前沒有v-if,直接給錯誤。
    warn(
      `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
      `used on element <${el.tag}> without corresponding v-if.`,
      el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
    )
  }
}
複製代碼
  • 2.v-for

那麼v-for咱們最終會處理成什麼樣子呢?以及又是這麼處理成這種樣子的。

若是咱們的案例是這樣的:

v-for="(item,index) in list"
複製代碼

咱們獲得的結果會是:

{
    for:'list',
    alias:'item',
    iterator1:'index'
}
複製代碼

這裏沒有牽扯到closeElement了,直接在processFor一步到味,咱們詳細的看看吧。

// 1.processFor函數,主要是經過parseFor摘取屬性,而後經過extend拷貝給el。因此重點仍是parseFor函數。
export function processFor (el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) { // exp摘取的是v-for裏面的內容,這裏是(item,index) in list
    const res = parseFor(exp) // 摘取屬性
    if (res) {
      extend(el, res) // 拷貝給el
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `Invalid v-for expression: ${exp}`,
        el.rawAttrsMap['v-for']
      )
    }
  }
}
2.詳細結果在註釋裏面了。
export function parseFor (exp: string): ?ForParseResult {
  // 傳入了 exp =  (item,index) in list
  const inMatch = exp.match(forAliasRE) // 獲取了一個數組,這個正則咱們前面說了,這裏是
  // ['(item,index) in list','(item,index)','list']
  if (!inMatch) return
  const res = {}
  res.for = inMatch[2].trim() // list 不是嗎?
  const alias = inMatch[1].trim().replace(stripParensRE, '') // item,index 對嗎
  const iteratorMatch = alias.match(forIteratorRE) // 這個正則咱們也說過了,也是數組
  // [',index','index']
  if (iteratorMatch) {
    res.alias = alias.replace(forIteratorRE, '').trim()
    res.iterator1 = iteratorMatch[1].trim() // index對嗎
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else {
    res.alias = alias
  }
  return res
}
複製代碼

好的,結果出來了。

接着咱們對解析案例,咱們已經處理了開始標籤<div class="container" id="root">,那麼剩下對還有

<div v-if="show">
          show attr bind
    </div>

    <div v-for="(item,index) in options" :key="item.id">
      <span>{{item.id}}</span> 
      <div>{{item.text}}</div>
    </div>
        
 </div>
複製代碼

那麼接下來呢? parseStartTag會匹配到什麼呢?

<div v-if="show">嗎?

很差意思,並非。現實的template各個標籤之間都有空格,因此在while循環中,對於<符號的匹配根本不會爲0,因此進不了前面所說到模塊一,而是經過模塊二匹配到下一個<符號,並判斷是否爲註釋開始標籤結束標籤的一種。 若是是,那麼從位置0 到 下一個<符號之間的字符串,咱們有理由相信這是一個文本節點,交給模塊三到options.chars去處理。

很顯然,從位置0到,下一個開始標籤<div v-if="show">之間是有不少空格的,咱們會生成一個文本空節點。

而後中間的過程咱們省略的說吧。

  • 處理<div v-if="show">

  • 處理文本節點show attr bind

  • 處理結束標籤

好了,這是咱們處理的第一個結束標籤 ,咱們詳細的看看吧。

// 咱們知道對於結束標籤咱們匹配到後,是直接交給parseEndTag函數處理的。這個函數容錯能力咱們不說了,前面已經
// 有了詳細的講解,咱們須要明白它會調用options.end函數。end會交給closeElement。
// closeElement會創建父子關係並處理好多好多屬性
1.processKey
2.processRef
3.processSlotContent
4.processSlotContent
5.processComponent
6.processIfConditions
....
複製代碼

到了這裏咱們還剩下:

<div v-for="(item,index) in options" :key="item.id">
      <span>{{item.id}}</span> 
      <div>{{item.text}}</div>
    </div>
 </div>
複製代碼

而後繼續省略的講解:

  • 處理<div v-for="(item,index) in options" :key="item.id">,能夠參照上面筆者描述的v-for處理方式看。
  • 處理空節點
  • 處理<span>開始標籤
  • 處理文本標籤{{item.id}},須要注意的是,expression創建的astElement的type爲2。
  • 處理</span>結束標籤
  • 處理空節點
  • 處理<div>開始標籤
  • 處理文本標籤{{item.text}},type也是2
  • 處理</div>結束標籤,結束處理方式相同。
  • 處理空節點
  • 處理</div>結束標籤,結束處理方式相同。
  • 處理空節點
  • 處理</div>結束標籤,結束處理方式相同。
  • 處理空節點

4、總體流程總結。

普通標籤處理流程描述

  • 1.識別開始標籤,生成匹配結構match。
const match = { // 匹配startTag的數據結構
    tagName: 'div',
    attrs: [
        { 'id="xxx"','id','=','xxx' },
        ...
    ],
    start: index,
    end: xxx
}
複製代碼
  • 2.處理attrs,將數組處理成 {name:'xxx',value:'xxx'}
  • 3.生成astElement,處理for,if和once的標籤。
  • 4.識別結束標籤,將沒有閉合標籤的元素一塊兒處理。
  • 5.創建父子關係,最後再對astElement作全部跟Vue 屬性相關對處理。slot、component等等。

文本或表達式的處理流程描述。

  • 一、截取符號<以前的字符串,這裏必定是全部的匹配規則都沒有匹配上,只多是文本了。
  • 二、使用chars函數處理該字符串。
  • 三、判斷字符串是否含有delimiters,默認也就是${},有的話建立type爲2的節點,不然type爲3.

註釋流程描述

  • 一、匹配註釋符號。
  • 二、 使用comment函數處理。
  • 三、直接建立type爲3的節點。

完結感言

時間倉促,但願多多支持。

相關文章
相關標籤/搜索