您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了Vue中template為什么有且只能一個(gè)root,內(nèi)容簡而易懂,希望大家可以學(xué)習(xí)一下,學(xué)習(xí)完之后肯定會有收獲的,下面讓小編帶大家一起來看看吧。
引言
今年, 疫情 并沒有影響到各種面經(jīng)的正常出現(xiàn),可謂是絡(luò)繹不絕(學(xué)不動...)。然后,在前段時(shí)間也看到一個(gè)這樣的關(guān)于 Vue
的問題, 為什么每個(gè)組件 template 中有且只能一個(gè) root?
可能,大家在平常開發(fā)中,用的較多就是 template
寫 html
的形式。當(dāng)然,不排除用 JSX
和 render()
函數(shù)的。但是,究其本質(zhì),它們最終都會轉(zhuǎn)化成 render()
函數(shù)。然后,再由 render()
函數(shù)轉(zhuǎn)為 Vritual DOM
(以下統(tǒng)稱 VNode
)。而 render()
函數(shù)轉(zhuǎn)為 VNode
的過程,是由 createElement()
函數(shù)完成的。
因此,本次文章將會先講述 Vue
為什么限制 template
有且只能一個(gè) root
。然后,再分析 Vue
如何規(guī)避出現(xiàn)多 root
的情況。那么,接下來我們就從源碼的角度去深究一下這個(gè)過程!
一、為什么限制 template 有且只能有一個(gè) root
這里,我們會分兩個(gè)方面講解,一方面是 createElement()
的執(zhí)行過程和定義,另一方面是 VNode
的定義。
1.1 createElement()
createElement()
函數(shù)在源碼中,被設(shè)計(jì)為 render()
函數(shù)的參數(shù)。所以 官方文檔 也講解了,如何使用 render()
函數(shù)的方式創(chuàng)建組件。
而 createElement()
會在 _render
階段執(zhí)行:
... const { render, _parentVnode } = vm.$options ... vnode = render.call(vm._renderProxy, vm.$createElement);
可以很簡單地看出,源碼中通過 call()
將當(dāng)前實(shí)例作為 context
上下文以及 $createElement
作為參數(shù)傳入。
Vue2x 源碼中用了大量的 call 和 apply,例如經(jīng)典的 $set() API 實(shí)現(xiàn)數(shù)組變化的響應(yīng)式處理就用的很是精妙,大家有興趣可以看看。
$createElement
的定義又是這樣:
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
需要注意的是這個(gè)是我們手寫 render() 時(shí)調(diào)用的,如果是寫 template 則會調(diào)用另一個(gè) vm._c 方法。兩者的區(qū)別在于 createElement() 最后的參數(shù)前者為 true,后者為 false。
而到這里,這個(gè) createElement()
實(shí)質(zhì)是調(diào)用了 _createElement()
方法,它的定義:
export function _createElement ( context: Component, // vm實(shí)例 tag?: string | Class<Component> | Function | Object, // DOM標(biāo)簽 data?: VNodeData, // vnode數(shù)據(jù) children?: any, normalizationType?: number ): VNode | Array<VNode> { ... }
現(xiàn)在,見到了我們平常使用的 createElement()
的 廬山真面目 。這里,我們并不看函數(shù)內(nèi)部的執(zhí)行邏輯,這里分析一下這五個(gè)參數(shù):
context
,是 Vue
在 _render
階段傳入的當(dāng)前實(shí)例tag
,是我們使用 createElement
時(shí)定義的根節(jié)點(diǎn) HTML
標(biāo)簽名data
,是我們使用 createElement
是傳入的該節(jié)點(diǎn)的屬性,例如 class
、 style
、 props
等等children
,是我們使用 createElement
是傳入的該節(jié)點(diǎn)包含的子節(jié)點(diǎn),通常是一個(gè)數(shù)組normalizationType
,是用于判斷拍平子節(jié)點(diǎn)數(shù)組時(shí),要用簡單迭代還是遞歸處理,前者是針對簡單二維,后者是針對多維。可以看出, createElement()
的設(shè)計(jì),是針對一個(gè)節(jié)點(diǎn),然后帶 children
的組件的 VNode
的創(chuàng)建。并且,它并沒有留給你進(jìn)行多 root
的創(chuàng)建的機(jī)會,只能傳一個(gè)根 root
的 tag
,其他都是它的選項(xiàng)。
1.2 VNode
我想大家都知道 Vue2x
用的靜態(tài)類型檢測的方式是 flow
,所以它會借助 flow
實(shí)現(xiàn)自定義類型。而 VNode
就是其中一種。那么,我們看看 VNode
類型定義:
前面,我們分析了 createElement()
的調(diào)用時(shí)機(jī),知道它最終返回的就是 VNode。那么,現(xiàn)在我們來看看 VNode
的定義:
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 key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal 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? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { ... } ... }
可以看到 VNode 所具備的屬性還是蠻多的,本次我們就只看 VNode
前面三個(gè)屬性:
顯而易見的是 VNode
的設(shè)計(jì)也是一個(gè) root
,然后由 children
不斷延申下去。這樣和前面 createElement()
的設(shè)計(jì)相呼應(yīng), 不可能會 出現(xiàn)多 root
的情況。
1.3 小結(jié)
可以看到 VNode
和 createElement()
的設(shè)計(jì),就只是針對單個(gè) root
的情況進(jìn)行處理,最終形成 樹的結(jié)構(gòu) 。那么,我想這個(gè)時(shí)候 可能有人會問為什么它們被設(shè)計(jì)樹的結(jié)構(gòu)? 。
而針對這個(gè)問題,有 兩個(gè)方面 ,一方面是樹形結(jié)構(gòu)的 VNode
轉(zhuǎn)為真實(shí) DOM
后,我們只需要將根 VNode
的真實(shí) DOM
掛載到頁面中。另一方面是 DOM
本身就是樹形結(jié)構(gòu),所以 VNode
也被設(shè)計(jì)為樹形結(jié)構(gòu),而且之后我們分析 template
編譯階段會提到 AST
抽象語法樹,它也是樹形結(jié)構(gòu)。所以,統(tǒng)一的結(jié)構(gòu)可以實(shí)現(xiàn)很方便的類型轉(zhuǎn)化,即從 AST
到 Render
函數(shù),從 Render
函數(shù)到 VNode
,最后從 VNode
到真實(shí) DOM
。
并且,可以想一個(gè)情景,如果多個(gè) root
,那么當(dāng)你將 VNode
轉(zhuǎn)為真實(shí) DOM
時(shí),掛載到頁面中,是不是要遍歷這個(gè) DOM Collection
,然后掛載上去,而這個(gè)階段又是操作 DOM
的階段。大家都知道的一個(gè)東西就是操作 DOM
是 非常昂貴的 。所以,一個(gè) root
的好處在這個(gè)時(shí)候就體現(xiàn)出它的好處了。
其實(shí)這個(gè)過程,讓我想起 紅寶書 中在講文檔碎片的時(shí)候,提倡把要?jiǎng)?chuàng)建的 DOM 先添加到文檔碎片中,然后將文檔碎片添加到頁面中。(PS:想想第一次看紅寶書是去年 4 月份,剛開始學(xué)前端,不經(jīng)意間過了快一年了....)
二、如何規(guī)避出現(xiàn)多 root 的情況
2.1 template 編譯過程
在我們平常的開發(fā)中,通常是在 .vue
文件中寫 <template>
,然后通過在 <template>
中創(chuàng)建一個(gè) div
來作為 root
,再在 root
中編寫描述這個(gè) .vue
文件的 html
標(biāo)簽。當(dāng)然,你也可以直接寫 render()
函數(shù)。
在文章的開始,我們也說了在 Vue
中無論是寫 template
還是 render
,它最終會轉(zhuǎn)成 render()
函數(shù)。而平常開發(fā)中,我們用 template
的方式會較多。所以,這個(gè)過程就需要 Vue
來編譯 template
。
編譯 template
的這個(gè)過程會是這樣:
template
生成 AST
(抽象語法樹)AST
,即對 AST
節(jié)點(diǎn)進(jìn)行靜態(tài)節(jié)點(diǎn)或靜態(tài)根節(jié)點(diǎn)的判斷,便于之后 patch
判斷AST
可執(zhí)行的函數(shù),在 Vue
中針對這一階段定義了很多 _c
、 _l
之類的函數(shù),就其本質(zhì)它們是對 render()
函數(shù)的封裝這三個(gè)步驟在源碼中的定義:
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { // 生成 AST const ast = parse(template.trim(), options) if (options.optimize !== false) { // 優(yōu)化 AST optimize(ast, options) } // 生成可執(zhí)行的函數(shù) const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })
需要注意的是 Vue-CLI
提供了兩個(gè)版本, Runtime-Compiler
和 Runtime
,兩者的區(qū)別,在于前者可以將 template
編譯成 render()
函數(shù),但是后者必須手寫 render()
函數(shù)
而對于開發(fā)中,如果你寫了多個(gè) root
的組件,在 parse
的時(shí)候,即生成 AST
抽象語法樹的時(shí)候, Vue
就會過濾掉多余的 root
,只認(rèn)第一個(gè) root
。
而 parse
的整個(gè)過程,其實(shí)就是正則匹配的過程,并且這個(gè)過程會用棧來存儲起始標(biāo)簽。整個(gè) parse
過程的流程圖:
然后,我們通過一個(gè)例子來分析一下,其中針對多 root
的處理。假設(shè)此時(shí)我們定義了這樣的 template
:
<div><span></span></div><div></div>
顯然,它是多 root
的。而在處理第一個(gè) <div>
時(shí),會創(chuàng)建對應(yīng)的 ASTElement
,它的結(jié)構(gòu)會是這樣:
{ type: 1, tag: "div", attrsList: [], attrsMap: {}, rawAttrsMap: {}, parent: undefined, children: [], start: 0, end: 5 }
而此時(shí),這個(gè) ASTElement
會被添加到 stack
中,然后刪除原字符串中的 <div>
,并且設(shè)置 root
為該 ASTElement
。
然后,繼續(xù)遍歷。對于 <span>
也會創(chuàng)建一個(gè) ASTElement
并入棧,然后刪除繼續(xù)下一次。接下來,會匹配到 </span>
,此時(shí)會處理標(biāo)簽的結(jié)束,例如于棧頂 ASTElement
的 tag
進(jìn)行匹配,然后出棧。接下來,匹配到 </div>
,進(jìn)行和 span
同樣的操作。
最后,對于第二個(gè) root
的 <div>
,會做和上面一樣的操作。但是,在處理 </div>
時(shí),此時(shí)會進(jìn)入判斷 multiple root
的邏輯,即此時(shí)字符串已經(jīng)處理完了,但是這個(gè)結(jié)束標(biāo)簽對應(yīng)的 ASTElement
并不等于我們最初定義的 root
。所以此時(shí)就會報(bào)錯(cuò):
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
而且,該 ASTElement
也不會加入最終的 AST
中,所以之后也不可能會出現(xiàn)多個(gè) root
的情況。
同時(shí),這個(gè)報(bào)錯(cuò)也提示我們?nèi)绻枚鄠€(gè) root
,需要借助 if
條件判斷來實(shí)現(xiàn)。
可以看出, template
編譯的最終的目標(biāo)就是構(gòu)建一個(gè) AST
抽象語法樹。所以,它會在創(chuàng)建第一個(gè) ASTElement
的時(shí)候就確定 AST
的 root
,從而確保 root
唯一性。
2.2 _render 過程
不了解 Vue
初始化過程的同學(xué),可能不太清楚 _render
過程。你可以理解為渲染的過程。在這個(gè)階段會調(diào)用 render
方法生成 VNode
,以及對 VNode
進(jìn)行一些處理,最終返回一個(gè) VNode
。
而相比較 template
編譯的過程, _render
過程的判斷就比較簡潔:
if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ); } vnode = createEmptyVNode(); }
前面在講 createElement
的時(shí)候,也講到了 render()
需要返回 VNode
。所以,這里是防止部分騷操作, return
了包含多個(gè) VNode
的數(shù)組。
結(jié)語
通過閱讀,我想大家也明白了 為什么 Vue 中 template 有且只能一個(gè) root ? 。 Vue
這樣設(shè)計(jì)的出發(fā)點(diǎn)可能很簡單,為了減少掛載時(shí) DOM
的操作。但是,它是如何處理多 root
的情況,以及相關(guān)的 VNode
、 AST
、 createElement()
等等關(guān)鍵點(diǎn),個(gè)人認(rèn)為都是很值得深入了解的。
以上就是關(guān)于Vue中template為什么有且只能一個(gè)root的內(nèi)容,如果你們有學(xué)習(xí)到知識或者技能,可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。