1. 程式人生 > 程式設計 >Vue 中 template 有且只能一個 root的原因解析(原始碼分析)

Vue 中 template 有且只能一個 root的原因解析(原始碼分析)

引言

今年, 疫情 並沒有影響到各種面經的正常出現,可謂是絡繹不絕(學不動...)。然後,在前段時間也看到一個這樣的關於 Vue 的問題, 為什麼每個元件 template 中有且只能一個 root?

可能,大家在平常開發中,用的較多就是 templatehtml 的形式。當然,不排除用 JSXrender() 函式的。但是,究其本質,它們最終都會轉化成 render() 函式。然後,再由 render() 函式轉為 Vritual DOM (以下統稱 VNode )。而 render() 函式轉為 VNode 的過程,是由 createElement() 函式完成的。

因此,本次文章將會先講述 Vue

為什麼限制 template 有且只能一個 root 。然後,再分析 Vue 如何規避出現多 root 的情況。那麼,接下來我們就從原始碼的角度去深究一下這個過程!

一、為什麼限制 template 有且只能有一個 root

這裡,我們會分兩個方面講解,一方面是 createElement() 的執行過程和定義,另一方面是 VNode 的定義。

1.1 createElement()

createElement() 函式在原始碼中,被設計為 render() 函式的引數。所以 官方文件 也講解了,如何使用 render() 函式的方式建立元件。

createElement() 會在 _render

階段執行:

...
const { render,_parentVnode } = vm.$options
...
vnode = render.call(vm._renderProxy,vm.$createElement);

可以很簡單地看出,原始碼中通過 call() 將當前例項作為 context 上下文以及 $createElement 作為引數傳入。

Vue2x 原始碼中用了大量的 call 和 apply,例如經典的 $set() API 實現陣列變化的響應式處理就用的很是精妙,大家有興趣可以看看。

$createElement 的定義又是這樣:

vm.$createElement = (a,b,c,d) => createElement(vm,a,d,true)

需要注意的是這個是我們手寫 render() 時呼叫的,如果是寫 template 則會呼叫另一個 vm._c 方法。兩者的區別在於 createElement() 最後的引數前者為 true,後者為 false。

而到這裡,這個 createElement() 實質是呼叫了 _createElement() 方法,它的定義:

export function _createElement (
 context: Component,// vm例項
 tag?: string | Class<Component> | Function | Object,// DOM標籤
 data?: VNodeData,// vnode資料
 children?: any,normalizationType?: number
): VNode | Array<VNode> {
 ...
}

現在,見到了我們平常使用的 createElement()廬山真面目 。這裡,我們並不看函式內部的執行邏輯,這裡分析一下這五個引數:

  • context ,是 Vue_render 階段傳入的當前例項
  • tag ,是我們使用 createElement 時定義的根節點 HTML 標籤名
  • data ,是我們使用 createElement 是傳入的該節點的屬性,例如 classstyleprops 等等
  • children ,是我們使用 createElement 是傳入的該節點包含的子節點,通常是一個數組
  • normalizationType ,是用於判斷拍平子節點陣列時,要用簡單迭代還是遞迴處理,前者是針對簡單二維,後者是針對多維。

可以看出, createElement() 的設計,是針對一個節點,然後帶 children 的元件的 VNode 的建立。並且,它並沒有留給你進行多 root 的建立的機會,只能傳一個根 roottag ,其他都是它的選項。

1.2 VNode

我想大家都知道 Vue2x 用的靜態型別檢測的方式是 flow ,所以它會藉助 flow 實現自定義型別。而 VNode 就是其中一種。那麼,我們看看 VNode 型別定義:

前面,我們分析了 createElement() 的呼叫時機,知道它最終返回的就是 VNode。那麼,現在我們來看看 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 前面三個屬性:

  • tag,即 VNode 對於的標籤名
  • data,即 VNode 具備的一些屬性
  • children,即 VNode 的子節點,它是一個 VNode 陣列

顯而易見的是 VNode 的設計也是一個 root ,然後由 children 不斷延申下去。這樣和前面 createElement() 的設計相呼應, 不可能會 出現多 root 的情況。

1.3 小結

可以看到 VNodecreateElement() 的設計,就只是針對單個 root 的情況進行處理,最終形成 樹的結構 。那麼,我想這個時候 可能有人會問為什麼它們被設計樹的結構?

而針對這個問題,有 兩個方面 ,一方面是樹形結構的 VNode 轉為真實 DOM 後,我們只需要將根 VNode 的真實 DOM 掛載到頁面中。另一方面是 DOM 本身就是樹形結構,所以 VNode 也被設計為樹形結構,而且之後我們分析 template 編譯階段會提到 AST 抽象語法樹,它也是樹形結構。所以,統一的結構可以實現很方便的型別轉化,即從 ASTRender 函式,從 Render 函式到 VNode ,最後從 VNode 到真實 DOM

Vue 中 template 有且只能一個 root的原因解析(原始碼分析)

並且,可以想一個情景,如果多個 root ,那麼當你將 VNode 轉為真實 DOM 時,掛載到頁面中,是不是要遍歷這個 DOM Collection ,然後掛載上去,而這個階段又是操作 DOM 的階段。大家都知道的一個東西就是操作 DOM非常昂貴的 。所以,一個 root 的好處在這個時候就體現出它的好處了。

其實這個過程,讓我想起 紅寶書 中在講文件碎片的時候,提倡把要建立的 DOM 先新增到文件碎片中,然後將文件碎片新增到頁面中。(PS:想想第一次看紅寶書是去年 4 月份,剛開始學前端,不經意間過了快一年了....)

二、如何規避出現多 root 的情況

2.1 template 編譯過程

在我們平常的開發中,通常是在 .vue 檔案中寫 <template> ,然後通過在 <template> 中建立一個 div 來作為 root ,再在 root 中編寫描述這個 .vue 檔案的 html 標籤。當然,你也可以直接寫 render() 函式。

在文章的開始,我們也說了在 Vue 中無論是寫 template 還是 render ,它最終會轉成 render() 函式。而平常開發中,我們用 template 的方式會較多。所以,這個過程就需要 Vue 來編譯 template

編譯 template 的這個過程會是這樣:

  • 根據 template 生成 AST (抽象語法樹)
  • 優化 AST ,即對 AST 節點進行靜態節點或靜態根節點的判斷,便於之後 patch 判斷
  • 根據 AST 可執行的函式,在 Vue 中針對這一階段定義了很多 _c_l 之類的函式,就其本質它們是對 render() 函式的封裝

這三個步驟在原始碼中的定義:

export const createCompiler = createCompilerCreator(function baseCompile (
 template: string,options: CompilerOptions
): CompiledResult {
 // 生成 AST
 const ast = parse(template.trim(),options)
 if (options.optimize !== false) {
 // 優化 AST
 optimize(ast,options)
 }
 // 生成可執行的函式
 const code = generate(ast,options)
 return {
 ast,render: code.render,staticRenderFns: code.staticRenderFns
 }
})

需要注意的是 Vue-CLI 提供了兩個版本, Runtime-CompilerRuntime ,兩者的區別,在於前者可以將 template 編譯成 render() 函式,但是後者必須手寫 render() 函式

而對於開發中,如果你寫了多個 root 的元件,在 parse 的時候,即生成 AST 抽象語法樹的時候, Vue 就會過濾掉多餘的 root ,只認第一個 root

parse 的整個過程,其實就是正則匹配的過程,並且這個過程會用棧來儲存起始標籤。整個 parse 過程的流程圖:

Vue 中 template 有且只能一個 root的原因解析(原始碼分析)

然後,我們通過一個例子來分析一下,其中針對多 root 的處理。假設此時我們定義了這樣的 template

<div><span></span></div><div></div>

顯然,它是多 root 的。而在處理第一個 <div> 時,會建立對應的 ASTElement ,它的結構會是這樣:

{
 type: 1,tag: "div",attrsList: [],attrsMap: {},rawAttrsMap: {},parent: undefined,children: [],start: 0,end: 5
}

而此時,這個 ASTElement 會被新增到 stack 中,然後刪除原字串中的 <div> ,並且設定 root 為該 ASTElement

然後,繼續遍歷。對於 <span> 也會建立一個 ASTElement 併入棧,然後刪除繼續下一次。接下來,會匹配到 </span> ,此時會處理標籤的結束,例如於棧頂 ASTElementtag 進行匹配,然後出棧。接下來,匹配到 </div> ,進行和 span 同樣的操作。

最後,對於第二個 root<div> ,會做和上面一樣的操作。但是,在處理 </div> 時,此時會進入判斷 multiple root 的邏輯,即此時字串已經處理完了,但是這個結束標籤對應的 ASTElement 並不等於我們最初定義的 root 。所以此時就會報錯:

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 中,所以之後也不可能會出現多個 root 的情況。

同時,這個報錯也提示我們如果要用多個 root ,需要藉助 if 條件判斷來實現。

可以看出, template 編譯的最終的目標就是構建一個 AST 抽象語法樹。所以,它會在建立第一個 ASTElement 的時候就確定 ASTroot ,從而確保 root 唯一性。

2.2 _render 過程

不瞭解 Vue 初始化過程的同學,可能不太清楚 _render 過程。你可以理解為渲染的過程。在這個階段會呼叫 render 方法生成 VNode ,以及對 VNode 進行一些處理,最終返回一個 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 的時候,也講到了 render() 需要返回 VNode 。所以,這裡是防止部分騷操作, return 了包含多個 VNode 的陣列。

結語

通過閱讀,我想大家也明白了 為什麼 Vue 中 template 有且只能一個 root ?Vue 這樣設計的出發點可能很簡單,為了減少掛載時 DOM 的操作。但是,它是如何處理多 root 的情況,以及相關的 VNodeASTcreateElement() 等等關鍵點,個人認為都是很值得深入瞭解的。

到此這篇關於Vue 中 template 有且只能一個 root的原因解析(原始碼分析)的文章就介紹到這了,更多相關vue template 有且只能一個 root內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!