(源碼分析)爲何 Vue 中 template 有且只能一個 root ?

引言

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

可能,你們在日常開發中,用的較多就是 templatehtml 的形式。固然,不排除用 JSXrender() 函數的。可是,究其本質,它們最終都會轉化成 render() 函數。而後,再由 render() 函數轉爲 Vritual DOM(如下統稱 VNode)。而 render() 函數轉爲 VNode 的過程,是由 createElement() 函數完成的。html

所以,本次文章將會先講述 Vue 爲何限制 template 有且只能一個 root。而後,再分析 Vue 如何規避出現多 root 的狀況。那麼,接下來咱們就從源碼的角度去深究一下這個過程!vue

1、爲何限制 template 有且只能有一個 root

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

1.1 createElement()

createElement() 函數在源碼中,被設計爲 render() 函數的參數。因此官方文檔也講解了,如何使用 render() 函數的方式建立組件。 java

createElement() 會在 _render 階段執行:node

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

能夠很簡單地看出,源碼中經過 call() 將當前實例做爲 context 上下文以及 $createElement 做爲參數傳入。數組

Vue2x 源碼中用了大量的 call 和 apply,例如經典的 $set() API 實現數組變化的響應式處理就用的非常精妙,你們有興趣能夠看看。

$createElement 的定義又是這樣:app

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
須要注意的是這個是咱們手寫 render() 時調用的,若是是寫 template 則會調用另外一個 vm._c 方法。二者的區別在於 createElement() 最後的參數前者爲 true,後者爲 false。

而到這裏,這個 createElement() 實質是調用了 _createElement() 方法,它的定義:async

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

如今,見到了咱們日常使用的 createElement()廬山真面目。這裏,咱們並不看函數內部的執行邏輯,這裏分析一下這五個參數:ide

  • 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

而且,能夠想一個情景,若是多個 root,那麼當你將 VNode 轉爲真實 DOM 時,掛載到頁面中,是否是要遍歷這個 DOM Collection,而後掛載上去,而這個階段又是操做 DOM 的階段。你們都知道的一個東西就是操做 DOM很是昂貴的。因此,一個 root 的好處在這個時候就體現出它的好處了。

其實這個過程,讓我想起 紅寶書中在講文檔碎片的時候,提倡把要建立的 DOM 先添加到文檔碎片中,而後將文檔碎片添加到頁面中。

2、如何規避出現多 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 過程的流程圖:

而後,咱們經過一個例子來分析一下,其中針對多 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() 等等關鍵點,我的認爲都是很值得深刻了解的。

寫做不易,若是你以爲有收穫的話,能夠帥氣三連擊!!!
相關文章
相關標籤/搜索