vue 為什麼只能有一個根元素
我們在初學Vue時,第一個上手的例子基本都是 new Vue({el:’#app’}),但是為什麼Vue例項只能掛載在一個div上呢?同樣的當我們開始寫第一個Vue頁面的時候,我們試圖在template標籤下寫兩個div,Vue提醒我們只能寫一個元素,但是為什麼只能有一個元素呢?很多時候我們都已經習以為常,但是卻說不上來為什麼。
筆者入坑Vue也有一段時間了,對Vue也算了解,Vuex、Vue-Router也用了不少;但是前幾天一看到這個面試問題卻感覺一下子回答不上了,想來每次寫程式碼也都是拿來就用,也沒有仔細的思考過裡面的原因;每每報錯了就換一種寫法,能用就行,僅此而已。
這個問題要從兩個方面來說:
- new Vue({el:’#app’})
- 單檔案元件中,template下的元素div
Vue例項
當我們例項化Vue的時候,填寫一個el選項,來指定我們的SPA入口:
let vm = new Vue({
el:'#app',
data:{ msg: 'Hi boy' }
})
<body>
<div id='app'>{{msg}}</div>
</body>
如果我們把程式碼改造一下,變成兩個入口。
let vm = new Vue({ el:'.app', data:{ msg: 'Hi boy' } }) <body> <div class='app'>{{msg}}</div> <div class='app'>{{msg}}</div> </body>
這時候會發現只有第一個div被渲染出來,而第二個div還是原封不動。我們簡單來看一下Vue的原始碼是如何實現的
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 } //以下省略無關程式碼 //... }
可以看到掛載函式傳了一個el引數,這個引數可以是string型別,也可以是一個element元素,也就是dom節點。最重要的是el = el && query(el)
這一行程式碼,那就繼續看一下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
}
}
首先query函式判斷是否是string型別,如果是string型別,就通過querySelector
函式獲取頁面中的元素,但是querySelector僅僅返回匹配指定選擇器的第一個元素,所以這就解釋了為什麼第二個div會原封不動。
Vue其實並不知道哪一個才是我們的入口,因為對於一個入口來講,這個入口就是一個Vue類
,Vue需要把這個入口裡面的所有東西拿來渲染、處理,最後再重新插入到dom中。如果同時設定了多個入口,那麼vue就不知道哪一個才是這個類
。
單檔案元件
當我們在vue-cli腳手架搭建的vue開發環境下使用單檔案元件時,一般會這麼寫:
<template>
<div class="box">
這裡是頁面內容
</div>
</template>
如果我們嘗試在template標籤下寫兩個div,那麼編輯器會提示我們The template root requires exactly one element
。那這裡為什麼template下也必須有且只能有一個div呢?
這裡我們要先看一看template這個標籤,這個標籤是HTML5出來的新標籤,它有三個特性:
- 隱藏性:該標籤不會顯示在頁面的任何地方,即便裡面有多少內容,它永遠都是隱藏的狀態;
- 任意性:該標籤可以寫在頁面的任何地方,甚至是head、body、sciprt標籤內;
- 無效性:該標籤裡的任何HTML內容都是無效的,不會起任何作用;
但是我們可以通過innerHTML來獲取到裡面的內容。
知道了這個,我們再來看.vue的單檔案元件。其實本質上,一個單檔案元件會被各種各樣的loader處理成為.js檔案(因為當你import一個單檔案元件並打印出來的時候,是一個vue例項),通過template的任意性我們知道,template包裹的HTML可以寫在任何地方,那麼對於一個.vue來講,這個template裡面的內容就是會被vue處理為虛擬dom並渲染的內容,導致結果又回到了開始 :既然一個.vue單檔案元件是一個vue例項,那麼這個例項的入口在哪裡?
如果在template下有多個div,那麼該如何指定這個vue例項的根入口?
為了讓元件能夠正常的生成一個vue例項,那麼這個div會被自然的處理成程式的入口。
通過這個‘根節點’,來遞迴遍歷整個vue‘樹’下的所有節點,並處理為vdom,最後再渲染成真正的HTML,插入在正確的位置。