聊聊Vue.js的template編譯

染陌同學發表於2019-03-04

寫在前面

因為對Vue.js很感興趣,而且平時工作的技術棧也是Vue.js,這幾個月花了些時間研究學習了一下Vue.js原始碼,並做了總結與輸出。

文章的原地址:github.com/answershuto…

在學習過程中,為Vue加上了中文的註釋github.com/answershuto…,希望可以對其他想學習Vue原始碼的小夥伴有所幫助。

可能會有理解存在偏差的地方,歡迎提issue指出,共同學習,共同進步。

$mount

首先看一下mount的程式碼

/*把原本不帶編譯的$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`) {
        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,
        delimiters: options.delimiters
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== `production` && config.performance && mark) {
        mark(`compile end`)
        measure(`${this._name} compile`, `compile`, `compile end`)
      }
    }
  }
  /*Github:https://github.com/answershuto*/
  /*呼叫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是如何編譯的。

一些基礎

首先,template會被編譯成AST語法樹,那麼AST是什麼?

在電腦科學中,抽象語法樹(abstract syntax tree或者縮寫為AST),或者語法樹(syntax tree),是原始碼的抽象語法結構的樹狀表現形式,這裡特指程式語言的原始碼。具體可以檢視抽象語法樹

AST會經過generate得到render函式,render的返回值是VNode,VNode是Vue的虛擬DOM節點,具體定義如下:

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
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  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?
  /*Github:https://github.com/answershuto*/

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*當前節點的標籤名*/
    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
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}複製程式碼

關於VNode的一些細節,請參考VNode節點

createCompiler

createCompiler用以建立編譯器,返回值是compile以及compileToFunctions。compile是一個編譯器,它會將傳入的template轉換成對應的AST樹、render函式以及staticRenderFns函式。而compileToFunctions則是帶快取的編譯器,同時staticRenderFns以及render函式會被轉換成Funtion物件。

因為不同平臺有一些不同的options,所以createCompiler會根據平臺區分傳入一個baseOptions,會與compile本身傳入的options合併得到最終的finalOptions。

compileToFunctions

首先還是貼一下compileToFunctions的程式碼。

  /*帶快取的編譯器,同時staticRenderFns以及render函式會被轉換成Funtion物件*/
  function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = options || {}

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== `production`) {
      // detect possible CSP restriction
      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.`
          )
        }
      }
    }
    /*Github:https://github.com/answershuto*/
    // check cache
    /*有快取的時候直接取出快取中的結果即可*/
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (functionCompileCache[key]) {
      return functionCompileCache[key]
    }

    // compile
    /*編譯*/
    const compiled = compile(template, options)

    // check compilation errors/tips
    if (process.env.NODE_ENV !== `production`) {
      if (compiled.errors && compiled.errors.length) {
        warn(
          `Error compiling template:

${template}

` +
          compiled.errors.map(e => `- ${e}`).join(`
`) + `
`,
          vm
        )
      }
      if (compiled.tips && compiled.tips.length) {
        compiled.tips.forEach(msg => tip(msg, vm))
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []
    /*將render轉換成Funtion物件*/
    res.render = makeFunction(compiled.render, fnGenErrors)
    /*將staticRenderFns全部轉化成Funtion物件 */
    const l = compiled.staticRenderFns.length
    res.staticRenderFns = new Array(l)
    for (let i = 0; i < l; i++) {
      res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors)
    }

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== `production`) {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:

` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in

${code}
`).join(`
`),
          vm
        )
      }
    }

    /*存放在快取中,以免每次都重新編譯*/
    return (functionCompileCache[key] = res) 
  }複製程式碼

我們可以發現,在閉包中,會有一個functionCompileCache物件作為快取器。

  /*作為快取,防止每次都重新編譯*/
  const functionCompileCache: {
    [key: string]: CompiledFunctionResult;
  } = Object.create(null)複製程式碼

在進入compileToFunctions以後,會先檢查快取中是否有已經編譯好的結果,如果有結果則直接從快取中讀取。這樣做防止每次同樣的模板都要進行重複的編譯工作。

    // check cache
    /*有快取的時候直接取出快取中的結果即可*/
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (functionCompileCache[key]) {
      return functionCompileCache[key]
    }複製程式碼

在compileToFunctions的末尾會將編譯結果進行快取

  /*存放在快取中,以免每次都重新編譯*/
  return (functionCompileCache[key] = res)複製程式碼

compile

  /*編譯,將模板template編譯成AST樹、render函式以及staticRenderFns函式*/
  function compile (
    template: string,
    options?: CompilerOptions
  ): CompiledResult {
    const finalOptions = Object.create(baseOptions)
    const errors = []
    const tips = []
    finalOptions.warn = (msg, tip) => {
      (tip ? tips : errors).push(msg)
    }

    /*做下面這些merge的目的因為不同平臺可以提供自己本身平臺的一個baseOptions,內部封裝了平臺自己的實現,然後把共同的部分抽離開來放在這層compiler中,所以在這裡需要merge一下*/
    if (options) {
      // merge custom modules
      /*合併modules*/
      if (options.modules) {
        finalOptions.modules = (baseOptions.modules || []).concat(options.modules)
      }
      // merge custom directives
      if (options.directives) {
        /*合併directives*/
        finalOptions.directives = extend(
          Object.create(baseOptions.directives),
          options.directives
        )
      }
      // copy other options
      for (const key in options) {
        /*合併其餘的options,modules與directives已經在上面做了特殊處理了*/
        if (key !== `modules` && key !== `directives`) {
          finalOptions[key] = options[key]
        }
      }
    }

    /*基礎模板編譯,得到編譯結果*/
    const compiled = baseCompile(template, finalOptions)
    if (process.env.NODE_ENV !== `production`) {
      errors.push.apply(errors, detectErrors(compiled.ast))
    }
    compiled.errors = errors
    compiled.tips = tips
    return compiled
  }複製程式碼

compile主要做了兩件事,一件是合併option(前面說的將平臺自有的option與傳入的option進行合併),另一件是baseCompile,進行模板template的編譯。

來看一下baseCompile

baseCompile

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  /*parse解析得到ast樹*/
  const ast = parse(template.trim(), options)
  /*
    將AST樹進行優化
    優化的目標:生成模板AST樹,檢測不需要進行DOM改變的靜態子樹。
    一旦檢測到這些靜態樹,我們就能做以下這些事情:
    1.把它們變成常數,這樣我們就再也不需要每次重新渲染時建立新的節點了。
    2.在patch的過程中直接跳過。
 */
  optimize(ast, options)
  /*根據ast樹生成所需的code(內部包含render與staticRenderFns)*/
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}複製程式碼

baseCompile首先會將模板template進行parse得到一個AST語法樹,再通過optimize做一些優化,最後通過generate得到render以及staticRenderFns。

parse

parse的原始碼可以參見github.com/answershuto…

parse會用正則等方式解析template模板中的指令、class、style等資料,形成AST語法樹。

optimize

optimize的主要作用是標記static靜態節點,這是Vue在編譯過程中的一處優化,後面當update更新介面時,會有一個patch的過程,diff演算法會直接跳過靜態節點,從而減少了比較的過程,優化了patch的效能。

generate

generate是將AST語法樹轉化成render funtion字串的過程,得到結果是render的字串以及staticRenderFns字串。


至此,我們的template模板已經被轉化成了我們所需的AST語法樹、render function字串以及staticRenderFns字串。

舉個例子

來看一下這段程式碼的編譯結果

<div class="main" :class="bindClass">
    <div>{{text}}</div>
    <div>hello world</div>
    <div v-for="(item, index) in arr">
        <p>{{item.name}}</p>
        <p>{{item.value}}</p>
        <p>{{index}}</p>
        <p>---</p>
    </div>
    <div v-if="text">
        {{text}}
    </div>
    <div v-else></div>
</div>複製程式碼

轉化後得到AST樹,如下圖:

img
img

我們可以看到最外層的div是這顆AST樹的根節點,節點上有許多資料代表這個節點的形態,比如static表示是否是靜態節點,staticClass表示靜態class屬性(非bind:class)。children代表該節點的子節點,可以看到children是一個長度為4的陣列,裡面包含的是該節點下的四個div子節點。children裡面的節點與父節點的結構類似,層層往下形成一棵AST語法樹。

再來看看由AST得到的render函式

with(this){
    return _c(  `div`,
                {
                    /*static class*/
                    staticClass:"main",
                    /*bind class*/
                    class:bindClass
                },
                [
                    _c( `div`, [_v(_s(text))]),
                    _c(`div`,[_v("hello world")]),
                    /*這是一個v-for迴圈*/
                    _l(
                        (arr),
                        function(item,index){
                            return _c(  `div`,
                                        [_c(`p`,[_v(_s(item.name))]),
                                        _c(`p`,[_v(_s(item.value))]),
                                        _c(`p`,[_v(_s(index))]),
                                        _c(`p`,[_v("---")])]
                                    )
                        }
                    ),
                    /*這是v-if*/
                    (text)?_c(`div`,[_v(_s(text))]):_c(`div`,[_v("no text")])],
                    2
            )
}複製程式碼

_c,_v,_s,_q

看了render function字串,發現有大量的_c,_v,_s,_q,這些函式究竟是什麼?

帶著問題,我們來看一下core/instance/render

/*處理v-once的渲染函式*/
  Vue.prototype._o = markOnce
  /*將字串轉化為數字,如果轉換失敗會返回原字串*/
  Vue.prototype._n = toNumber
  /*將val轉化成字串*/
  Vue.prototype._s = toString
  /*處理v-for列表渲染*/
  Vue.prototype._l = renderList
  /*處理slot的渲染*/
  Vue.prototype._t = renderSlot
  /*檢測兩個變數是否相等*/
  Vue.prototype._q = looseEqual
  /*檢測arr陣列中是否包含與val變數相等的項*/
  Vue.prototype._i = looseIndexOf
  /*處理static樹的渲染*/
  Vue.prototype._m = renderStatic
  /*處理filters*/
  Vue.prototype._f = resolveFilter
  /*從config配置中檢查eventKeyCode是否存在*/
  Vue.prototype._k = checkKeyCodes
  /*合併v-bind指令到VNode中*/
  Vue.prototype._b = bindObjectProps
  /*建立一個文字節點*/
  Vue.prototype._v = createTextVNode
  /*建立一個空VNode節點*/
  Vue.prototype._e = createEmptyVNode
  /*處理ScopedSlots*/
  Vue.prototype._u = resolveScopedSlots

  /*建立VNode節點*/
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)複製程式碼

通過這些函式,render函式最後會返回一個VNode節點,在_update的時候,經過patch與之前的VNode節點進行比較,得出差異後將這些差異渲染到真實的DOM上。

關於

作者:染陌

Email:answershuto@gmail.com or answershuto@126.com

Github: github.com/answershuto

Blog:answershuto.github.io/

知乎主頁:www.zhihu.com/people/cao-…

知乎專欄:zhuanlan.zhihu.com/ranmo

掘金: juejin.im/user/58f87a…

osChina:my.oschina.net/u/3161824/b…

轉載請註明出處,謝謝。

歡迎關注我的公眾號

相關文章