$mount
掛載入口的奧祕
在想了解$mount
掛載入口的時候,希望先了解一下關於我公眾號14篇關於Vue合併策略的解析。這樣可以更好的瞭解$mount掛載入口的區分意義
合併策略已經講解完成。在合併策略之後還有很多始化操作,在執始化執行到最後就是執行Vue原型上$mount
方法將元件掛載到開發者給定的元素之上。$mount
存在兩種掛載方式,手動掛載
、同樣在api
中向外暴露了。第二個則是自動掛載
,一旦有el
選項,則會在執行_init
最後進行內部的自動掛載。
掛載入口進行分析
渲染掛載
首先第一個掛載入口在src/platforms/web/runtime/index.js
,在Vue
原型上掛載了$mount
函式。在src/platfroms/web/runtime/index.js
中進行了兩個步驟的處理,第一個在合併策略中已經提到過,對平臺進行區分重寫,新增了一些針對於平臺內建的components
,和directives
。第二個則是vue runtime-only
的版本,在此入口進行rollup
打包進之後是一個不經過編譯的版本,但是需要通過打包工具把template
轉成render
渲染函式。
手動掛載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進行自動掛載
渲染$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元素
,也可以是一個字串
。
el = el && inBrowser ? query(el) : undefined
複製程式碼
內部執行的第一部先拿到換化後的el
引數,因為el
可能是字串,可能會是Dom
元素,也有可能是一個不符合引數要求的值。首先做兩個判斷,是否el
有值,並且此時執行的環境是瀏覽器環境
inBrowser 判斷是否是瀏覽器環境
在 src/core/util/env檔案中可以檢視inBrowser的實現
export const inBrowser = typeof window !== 'undefined'
複製程式碼
描述: 檢查當前執行環境是否是瀏覽器環境
實現原理: 只有在瀏覽器環境中才會有
window
物件,通過typeof
去檢測window
的型別,如果window
物件不存在,肯定是undefined
滿足了兩者條件之後,通地query
方法去解析el
引數,獲取到真正的DOM
元素,否則不滿足兩者條件,直接返回undefined
.
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>
元素,在生產環境下會報出警。不要掛載到html
和body
元素上,對其它元素進行替換。從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>
複製程式碼
可以發現id
為app
的p
元素已經被MyComponent
元件模版被替換掉了,此時的掛載點只是一個被將要被替換的佔位符。如果此時掛載點為body
元素或者html
元素的情況,body
和html
元素同樣會被替換掉,此時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
方法進行渲染。
- 根據官方文件進行提示進行手動編寫在render選項中
- 通過打包過具通過
vue-loader
把template
模版進行轉化成render
函式。 - 通過
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
變數。
- 先對
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
快取函式進行掛載,前面提到過如果同時有template
和el
選項,此時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
處理,建立一個container
為div
的空元素,深度克隆el
元素,通過appendChild
方法把克隆後的el
元素新增到cantainer
容器中,成為子節點。最後返回的container
中的innerHTML
,這樣的操作等同於獲取了元素的outerHTML
.
在只有el
的情況下,又作為template
轉化的模版,也要作為mountComponent
函式的替換元素的情況下,el
必須是一個Dom
元素。通過el
獲取到了template
模版之後,呼叫compileToFunctions
轉化成render
函式。最後呼叫快取的mount
函式進行渲染Dom
結構體。