從源碼理解 Vue 模板編譯

Vue 的 template 是如何編譯成真正的 HTML 並作到雙向綁定等等特殊功能的呢?以往這個問題對我來講一直是個黑洞。最近看了 Vue 的源碼,對模板編譯的整個過程的脈絡有了更爲清晰的瞭解。javascript

先甩一張圖

Vue 渲染過程
在這張圖中,咱們能夠看到 Vue 的模板編譯是在 $mount 的過程當中進行的,在 $mount 的時候執行了 compile 這個方法來將 template 裏的內容轉換成真正的 HTML 代碼。complie 以後執行的事情也蠻重要的,這個咱們留到最後再說。complie 最終生成 render 函數,等待調用。這個方法分爲三步:vue

  • parse 函數解析 template
  • optimize 函數優化靜態內容
  • generate 函數建立 render 函數字符串

parse 解析

在瞭解 parse 的過程以前,咱們須要瞭解 AST,AST 的全稱是 Abstract Syntax Tree,也就是所謂抽象語法樹,用來表示代碼的數據結構。在 Vue 中我把它理解爲嵌套的、攜帶標籤名、屬性和父子關係的 JS 對象,以樹來表現 DOM 結構。
下面是 Vue 裏的 AST 的定義:java

AST

咱們能夠看到 AST 有三種類型,而且經過 children 這個字段層層嵌套造成了樹狀的結構。而每個 AST 節點存放的就是咱們的 HTML 元素、插值表達式或文本內容。AST 正是 parse 函數生成和返回的。
parse 函數裏定義了許多的正則表達式,經過對標籤名開頭、標籤名結尾、屬性字段、文本內容等等的遞歸匹配。把字符串類型的 template 轉化成了樹狀結構的 AST。node

// parse 裏定義的一些正則
export const onRE = /^@|^v-on:/ //匹配 v-on
export const dirRE = /^v-|^@|^:/ //匹配 v-on 和 v-bind
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ //匹配 v-for 屬性
export const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ //匹配 v-for 的多種形式

咱們能夠把這個過程理解爲一個截取的過程,它把 template 字符串裏的元素、屬性和文本一個個地截取出來,其中的細節十分瑣碎,涉及到各類不一樣狀況(好比不一樣類型的 v-for,各類 vue 指令、空白節點以及父子關係等等),咱們再也不贅述。正則表達式

Parse 過程

假設咱們有一個元素<div id="test">texttext</div>,在 parse 完以後會變成以下的結構並返回:數據結構

ele1 = {
    type: 1,
    tag: "div",
    attrsList: [{name: "id", value: "test"}],
    attrsMap: {id: "test"},
    parent: undefined,
    children: [{
        type: 3,
        text: 'texttext'
      }
    ],
    plain: true,
    attrs: [{name: "id", value: "'test'"}]
  }

optimize 優化

在第二步中,會對 parse 生成的 AST 進行靜態內容的優化。靜態內容指的是和數據沒有關係,不須要每次都刷新的內容。標記靜態節點的做用是爲了在後面作 Vnode 的 diff 時起做用,用來確認一個節點是否應該作 patch 仍是直接跳過。optimize 的過程分爲兩步:函數

  • 標記全部的靜態和非靜態結點
  • 標記靜態根節點

標記全部的靜態和非靜態結點

關於這一段咱們能夠直接看源碼:優化

function markStatic (node: ASTNode) {
  // 標記 static 屬性
  node.static = isStatic(node)
  if (node.type === 1) {
    // 注意這個判斷邏輯
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
  }
}

上面的代碼中有幾個須要注意的地方:this

  • isStatic 函數

isStatic 函數顧名思義是判斷該節點是否 static 的函數,符合以下內容的節點就會被認爲是 static 的節點:spa

1. 若是是表達式AST節點,直接返回 false
2. 若是是文本AST節點,直接返回 true
3. 若是元素是元素節點,階段有 v-pre 指令 ||
  1. 沒有任何指令、數據綁定、事件綁定等 &&
  2. 沒有 v-if 和 v-for &&
  3. 不是 slot 和 component &&
  4. 是 HTML 保留標籤 &&
  5. 不是 template 標籤的直接子元素而且沒有包含在 for 循環中
  則返回 true
  • if 判斷條件
  1. !isPlatformReservedTag(node.tag):node.tag 不是 HTML 保留標籤時返回true。
  2. node.tag !== 'slot':標籤不是slot。
  3. node.attrsMap['inline-template'] == null:node不是一個內聯模板容器。

若是知足上面的全部條件,那麼這個節點的 static 就會被置爲 false 而且不遞歸子元素,當不知足上面某一個條件時,遞歸子元素判斷子元素是否 static,只有全部元素都是 static 的時候,該元素纔是 static。

標記靜態根節點

這部分理解起來很簡單,只有當一個節點是 static 而且其不能只擁有一個靜態文本節點時才能被稱爲 static root。由於做者認爲這種狀況去作優化,其消耗會超過得到的收益。

if (node.static && node.children.length && !(
  node.children.length === 1 &&
  node.children[0].type === 3
)) {
  node.staticRoot = true
  return
} else {
  node.staticRoot = false
}

generate 生成 render

生成 render 的 generate 函數的輸入也是 AST,它遞歸了 AST 樹,爲不一樣的 AST 節點建立了不一樣的內部調用方法,等待後面的調用。生成 render 函數的過程以下:
generate 函數

幾種內部方法
_c:對應的是 createElement 方法,顧名思義,它的含義是建立一個元素(Vnode)
_v:建立一個文本結點。
_s:把一個值轉換爲字符串。(eg: {{data}})
_m:渲染靜態內容

假設咱們有這麼一段 template

<template>
  <div id="test">
    {{val}}
    <img src="http://xx.jpg">
  </div>
</template>

最終會被轉換成這樣子的函數字符串

{render: "with(this){return _c('div',{attrs:{"id":"test"}},[[_v(_s(val))]),_v(" "),_m(0)])}"}

後話

整個 Vue 渲染過程,前面咱們說了 complie 的過程,在作完 parse、optimize 和 generate 以後,咱們獲得了一個 render 函數字符串。那麼接下來 Vue 作的事情就是 new watcher,這個時候會對綁定的數據執行監聽,render 函數就是數據監聽的回調所調用的,其結果即是從新生成 vnode。當這個 render 函數字符串在第一次 mount、或者綁定的數據更新的時候,都會被調用,生成 Vnode。若是是數據的更新,那麼 Vnode 會與數據改變以前的 Vnode 作 diff,對內容作改動以後,就會更新到咱們真正的 DOM 上啦~

相關文章
相關標籤/搜索