Vue $mount的掛載入口的奧祕

$mount掛載入口的奧祕

在想了解$mount掛載入口的時候,但願先了解一下關於我公衆號14篇關於Vue合併策略的解析。這樣能夠更好的瞭解$mount掛載入口的區分意義

合併策略已經講解完成。在合併策略以後還有不少始化操做,在執始化執行到最後就是執行Vue原型上$mount方法將組件掛載到開發者給定的元素之上。$mount存在兩種掛載方式,手動掛載 、一樣在api中向外暴露了。第二個則是自動掛載,一旦有el選項,則會在執行_init最後進行內部的自動掛載。html

掛載入口進行分析

渲染掛載vue

首先第一個掛載入口在src/platforms/web/runtime/index.js,在Vue原型上掛載了$mount函數。在src/platfroms/web/runtime/index.js中進行了兩個步驟的處理,第一個在合併策略中已經提到過,對平臺進行區分重寫,添加了一些針對於平臺內置的components,和directives。第二個則是vue runtime-only的版本,在此入口進行rollup打包進以後是一個不通過編譯的版本,可是須要經過打包工具把template轉成render渲染函數。node

手動掛載Demo

var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// 建立並掛載到 #app (會替換 #app)
new MyComponent().$mount('#app')
// 或者
new MyComponent().$mount(document.querySelector('#app'))
複製代碼

$mount文檔中規定傳入的el參數能夠是兩種狀況,{Element | string} [elementOrSelector],要麼字符串,要麼是DOM元素。經過$mount進行掛載,會替換入的el對應的DOM無素。上面的DEMO是經過手動調用$mount進行掛載。一樣建立實列的時候傳入el進行自動掛載web

渲染$mount源碼解析

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
複製代碼

渲染$mount傳入兩個參數,一個是el能夠是一個DOM元素,也能夠是一個字符串api

el = el && inBrowser ? query(el) : undefined
複製代碼

內部執行的第一部先拿到換化後的el參數,由於el多是字符串,可能會是Dom元素,也有多是一個不符合參數要求的值。首先作兩個判斷,是否el有值,而且此時執行的環境是瀏覽器環境瀏覽器

inBrowser 判斷是不是瀏覽器環境

在 src/core/util/env文件中能夠查看inBrowser的實現緩存

export const inBrowser = typeof window !== 'undefined'
複製代碼

描述: 檢查當前執行環境是不是瀏覽器環境bash

實現原理: 只有在瀏覽器環境中才會有window對象,經過typeof去檢測window的類型,若是window對象不存在,確定是undefined閉包


知足了二者條件以後,通地query方法去解析el參數,獲取到真正的DOM元素,不然不知足二者條件,直接返回undefined.app

query方法

/**
 * Query an element selector if it's not an element already. */ export function query (el: string | Element): Element { if (typeof el === 'string') { const selected = document.querySelector(el) if (!selected) { process.env.NODE_ENV !== 'production' && warn( 'Cannot find element: ' + el ) return document.createElement('div') } return selected } else { return el } } 複製代碼

描述: 經過無素選擇器去獲取元素

參數: el 能夠是Dom無素,也能夠是字符串

實現方式: 首選判斷el參數是不是字符串。是字符串使用document.querySelector方法經過元素選擇器去獲取真正的dom元素。若是獲取不到,開發環境的狀況下,發出'Cannot find element: ' + el警告。建立一個空的div元素返回出去.el參數不是字符串的狀況下,不作任何操做直接返回el參數。但在這個狀況下還有兩種可能,一個是不合法的值,比說傳入了一個數字或布而值,或者傳入了真正的DOM元素(只考慮合併的狀況)。

return mountComponent(this, el, hydrating)
複製代碼

最後調用mountComponent進行真正的掛載工做。最後返回的則是掛載後的組件實列。

const vm = new MyComponent().$mount('#app')
console.log(vm)
複製代碼

經過$mount渲染掛載以後執行mountComponent以後返回了vm實列,因此能夠經過掛載後經過賦值給自定義一個變量,拿到最後掛載後的實列。

自動掛載Demo

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
複製代碼

在執行_init()函數的最後,當初始化工做完成以後,el通過全並已經合併到vm.$options對象上,對$options進行檢測,若是存在el則進行自動掛載,傳入el參數,調用是Vue原型上$mount函數。

ast語法樹轉render函數與渲染掛載$mount

若是此時運行的版本是runtime with compiler版本,這個版本的$mount會被進行重寫。而且增長了把template模板轉成render渲染函數。運行的入口在 src/platforms/web/entry-runtime-with-compiler文件中。

緩存掛載的$mount

const mount = Vue.prototype.$mount
複製代碼

前面分析的渲染掛載的$mount在自執行的過程當中,比src/platforms/web/entry-runtime-with-compiler文件中的$mount先掛在Vue的原型上,負責在頁面渲染真正的DOM結構,經過mount變量緩存了運行時版本的渲染掛載的函數。

Vue.prototype.$mount = function () {
  ...省略
}
複製代碼

緊接着把Vue原型上本來掛載的運行時版本的渲染掛載函數進行重寫,這裏重寫的緣由主要由於這不可是一個運行時的版本,同時也擔做着編譯模版轉化爲render函數的做用。此時針對了版本需求的不一樣進行了重寫。

el = el && query(el)
複製代碼

el參數經過query函數進行獲取指入的掛載點,獲取的Dom元素賦值給el參數,關於query函數的運用已經解釋過,若是不是元素選擇器,則原封不動返回。

body與html元素不能被替換的緣由

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
}
複製代碼

這裏的判斷掛載點是不是<body>元素或者是<html>元素,在生產環境下會報出警。不要掛載到htmlbody元素上,對其它元素進行替換。從Demo理解真正緣由:

<body>
  <p id="app">
  </p>
</body>
var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

const vm = new MyComponent().$mount('#app')
複製代碼

合併結果, 對掛載元素進行審覈:

<html>
  <body>
    <div>Hello</div>
  </body>
</html>
複製代碼

能夠發現idappp元素已經被MyComponent組件模版被替換掉了,此時的掛載點只是一個被將要被替換的佔位符。若是此時掛載點爲body元素或者html元素的狀況,bodyhtml元素一樣會被替換掉,此時html頁面則不是一個標準規定的html標準體了。瀏覽器一樣不會對此進行解析。

const options = this.$options
複製代碼

聲明options變量,把初始化合併到實列對象上的$options對象賦值給options變量。

if (!options.render) {
}
return mount.call(this, el, hydrating)
複製代碼

判斷options選項中是否有render函數,既渲染函數。有則直接調用運行版本的$mount函數,在以前運行時的$mount函數已經緩存給了mount變量。則直接經過mountComponent方法進行渲染掛載,由此可知,渲染整個DOM結構須要render渲染函數作支撐。render函數究竟是從那裏來?爲何有render函數能夠直接開始調用mountComponent方法進行渲染。

  1. 根據官方文檔進行提示進行手動編寫在render選項中
  2. 經過打包過具經過vue-loadertemplate模版進行轉化成render函數。
  3. 經過runtime-with-complier版本,通過compileToFunctions函數把template模版編譯成render函數。

ast語法轉化入口解析。

let template = options.template
if (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 = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    }
複製代碼

在沒有render選項的狀況。經過template或者el二者任意一個選項讓模版進行轉化成render渲染函數,聲明template變量,經過options.template選項賦值給template變量。

  1. 先對template選項獲取模版,當既有template選項時,也有el選項時,template則優先做爲轉化render函數的模版, el則做爲實例的掛載點。

當template是字符串的時候

若是有template,再判斷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
            )
          }
        }
複製代碼

經過charAt方法匹配是否字符串首字符是#號,調用idToToTemplate函數,把元素選擇器傳入傳爲參數。

idToTemplate(template)
複製代碼

描述:

經過元素選擇符獲取到元素,經過獲取到的元素拿到內部的innerHTML

參數

template: 元素選擇符

實現原理:

idToTemplate內部經過閉包進行緩存轉化後的模版。當執行idToTemplate的時候引用了cached執行的返回函數

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})
複製代碼

傳入了一個參數爲元素選擇器的字符,內部則是利用query函數獲取對應轉化後的元素。若是轉化成功後返回元素內的innerHTML 關於cached函數在合併策略中已經講解過了。原理就是利用閉包的原理,傳入一個純函數,若是緩存對象上有已經緩存過的屬性。由於id選擇器是惟一的,根據id選擇器轉化後的屬性和值會記錄在緩存對象上,一旦再次獲取一樣的選擇器的元素,能夠經過緩存對象進行比對,一旦比對成功,則直接從緩存中獲取。

if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
複製代碼

在開發環境中,若是經過id選擇器並無獲取到對應的元素時。則會報錯一個警告

當template是node節點時

tempalte還能夠直接傳入node節點,請看DEMO

<div id="app">
  <div>
     <p>{{a}}</p>
  </div>
 </div>
</body>

<script>
new Vue({
  el: '#app',
  template: document.querySelector('#app'),
  data: {
    a: 10
  }
})
</script>
複製代碼

若是是元素節點的分支的源碼

else if (template.nodeType) {
        template = template.innerHTML
}
複製代碼

此時經過demo能夠看出此時tempalte傳入的是一個元素節點,代碼運行時會跑入上面的分支代碼,直接獲取元素的innerHTML做爲模版

template既不是字符串也不是無素節點處理警告

if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
複製代碼

若是template既不是字符串也不是元素節點並在開發環境下,會報一個警告,請檢查template選項

是字符串模版

template: `<div>
                <p>{{a}}</p>
            </div>`,
複製代碼

在以上的可能性都已經分析過了。若是之前的狀況都經過,則用轉化爲的template模版,可是還有一種最經常使用的狀況,當處理爲字符串的時候,字符串開頭並非以#開頭,直接默認認爲是開發者用模版字符串寫入。以上這樣子的寫法一樣生效。

ast解析轉render渲染函數

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

在各類狀況下template成功獲取以後。經過compileToFunctions進行ast語法樹轉換,獲得render瀉染函數,賦值到實例的$options選項上。

最後調用mount緩存函數進行掛載,前面提到過若是同時有templateel選項,此時el只會是一個掛載點。會優先根據template選項生成真正的模版。

若是隻存在el選項時,並無template選項。el既做爲掛載點,也做爲模版

若是沒有template選項時,模版只會經過如下代碼進行轉換

else if (el) {
      template = getOuterHTML(el)
}
複製代碼

經過getOuterHTML方法傳入el參數獲取template模版。

經過el掛載並生成模版的DEMO:

<div id="app">
  <div>
     <p>{{a}}</p>
  </div>
 </div>
</body>

<script>
new Vue({
  el: document.querySelector('#app'),
  data: {
    a: 10
  }
})
</script>
複製代碼

getOuterHTML源碼解析

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}
複製代碼

首先判斷el元素是否有outerHTML,正常的元素的outerHTML則是傳入el元素自身。說明有些狀況下元素會沒有outerHTML,從注示上能夠看於對於ie瀏覽器中SVG無素是獲取不到outerHTML,此時就須要經過一個hack處理,建立一個containerdiv的空元素,深度克隆el元素,經過appendChild方法把克隆後的el元素添加到cantainer容器中,成爲子節點。最後返回的container中的innerHTML,這樣的操做等同於獲取了元素的outerHTML.

在只有el的狀況下,又做爲template轉化的模版,也要做爲mountComponent函數的替換元素的狀況下,el必須是一個Dom元素。經過el獲取到了template模版以後,調用compileToFunctions轉化成render函數。最後調用緩存的mount函數進行渲染Dom結構體。

相關文章
相關標籤/搜索