Vue 源碼編譯思想之parse那些事

楔子

筆者公司的前端小組掀起了Vue源碼學習小組,先後幾個月的共同窗習,讓小組成員都已經對Vue對大體框架有了個模糊對輪廓。 如今已經進入第二階段:整理。javascript

咱們將小組分爲四個部分,vue對整理也分爲三個大模塊:數據綁定、從template到vnode、vnode轉化爲dom對patch。html

筆者對小組被分到了template到vnode對部分,拿到手後,感受內容比較多,就先將內容根據源碼對佈局分爲兩小塊:parse 和 render過程。前端

再者,古人云:兵馬未動,糧草先行。筆者打算先介紹思想和思路,而後在具體到一些細枝末節。vue

本文準備到就是parse部分思想。java

正文

1、整個編譯過程的目的

咱們都知道使用vue到時候,咱們使用定製樣式到幾種方式大體爲:node

  • 1.options template。
  • 2.el。(掛載的節點)
  • 3.render 方法。

咱們須要明白到是不管是哪一種方式,咱們最終的目的都是 生成以vnode爲單位vdom樹express

而生成的vdom樹,最終是用於patch過程當中,生成真實dom節點。瀏覽器

因此咱們的編譯的最終目的是得到:緩存

Virtue dom。

形式大體以下圖:性能優化

vnode的樣式

暫時不用管每一個屬性的做用,咱們已經知道咱們的目標是怎樣的了。那麼還須要知道的是 起始點和過程

正常狀況下,若是咱們這樣初始化的話:

new Vue({
      el: '#el',
      template: ` <div> <div v-for="(item,index) in options" :key="item.id"> {{item.id}} <div>{{item.text}}</div> </div> </div> `,
      data: {
        name:"dinglei",
        options: [
          { id: 1, text: 'Hello' },
          { id: 2, text: 'World' }
        ]
      }
    })
複製代碼

那咱們的初始化的狀態則是 template的 一串 String。

固然初始化爲String的寫法仍是滿多的。 好比:

  • 不傳template,直接使用使用$el。
  • template,使用一個模版。

因此咱們從的轉換過程,就是從 一串String的模版virtue dom tree的過程。

目標圖

可是值得一提的是,vue 從模版到vdom的過程並不是是直接一次性到位的過程。多是由於尤大的設計的vnode和原生的dom屬性差距過大,直接編譯成vnode很差完成。其次是考慮到性能優化等方面。

因此vue的編譯過程實際上是分爲三個過程:

  • parse
  • optimize
  • codegen for render And render

分別對應三個過程:

  • 從模版到 astElement
  • 在astElement的基礎上優化添加標記staticRoot,固然這個層面的static標記最後是用於第三步codegen裏面的。
  • codegen 生成 render函數,render 綁定實例後,再進行執行 就能夠獲得vnode的樹。

流程大體以下圖:

編譯流程圖

接下來咱們將進入本文的主題

2、parse、optimize、codegen的核心思想解讀。

1)parse解讀

首先須要明白astElement包括哪些屬性。在vue源碼 flow文件目錄下的compiler.js能夠找到astElement的模型

declare type ASTElement = {
  type: 1;
  tag: string;
  attrsList: Array<ASTAttr>;
  attrsMap: { [key: string]: any };
  rawAttrsMap: { [key: string]: ASTAttr };
  parent: ASTElement | void;
  children: Array<ASTNode>;

  start?: number;
  end?: number;

  processed?: true;

  static?: boolean;
  staticRoot?: boolean;
  staticInFor?: boolean;
  staticProcessed?: boolean;
  hasBindings?: boolean;

  text?: string;
  attrs?: Array<ASTAttr>;
  dynamicAttrs?: Array<ASTAttr>;
  props?: Array<ASTAttr>;
  plain?: boolean;
  pre?: true;
  ns?: string;

  component?: string;
  inlineTemplate?: true;
  transitionMode?: string | null;
  slotName?: ?string;
  slotTarget?: ?string;
  slotTargetDynamic?: boolean;
  slotScope?: ?string;
  scopedSlots?: { [name: string]: ASTElement };

  ref?: string;
  refInFor?: boolean;

  if?: string;
  ifProcessed?: boolean;
  elseif?: string;
  else?: true;
  ifConditions?: ASTIfConditions;

  for?: string;
  forProcessed?: boolean;
  key?: string;
  alias?: string;
  iterator1?: string;
  iterator2?: string;

  staticClass?: string;
  classBinding?: string;
  staticStyle?: string;
  styleBinding?: string;
  events?: ASTElementHandlers;
  nativeEvents?: ASTElementHandlers;

  transition?: string | true;
  transitionOnAppear?: boolean;

  model?: {
    value: string;
    callback: string;
    expression: string;
  };

  directives?: Array<ASTDirective>;

  forbidden?: true;
  once?: true;
  onceProcessed?: boolean;
  wrapData?: (code: string) => string;
  wrapListeners?: (code: string) => string;

  // 2.4 ssr optimization
  ssrOptimizability?: number;

  // weex specific
  appendAsTree?: boolean;
};
複製代碼

在開始講流程以前,先列出整個流程會出現的重要的角色。下面的流程和流程圖都是以此爲基礎的。

角色

  • 識別器(parseHTML) (1)
  • 存儲棧(stack) (2)
  • 建立函數(createASTElement) (3)
  • 上下文(currentParent)(4)
  • 開始處理函數start (5)
  • 結束處理函數end (6)

總體工做流程大體以下:

  • 1.就是識別器(1)利用正則從前到後識別全部敏感字段。如(標籤、事件、迭代、數據綁定、插槽等等)

  • 2.開始標籤 例如<div>{{test}}</div>裏的<div>,識別器(1)一旦識別到此類標籤,就會使用開始函數start (5)來對開始標籤作統一處理,而start函數 會建立一個astElement並 存放到stack當中,當時建立的方式是利用建立函數(3)建立的,同時也會處理一些非組件屬性(具體如v-model、v-if、v-for)。這裏須要注意的是:

    • 1.一元標籤如:img等,會直接走3處理。
    • p標籤會作特殊處理,目的是爲了和瀏覽器等識別方式達到目標一致。具體點擊此處
  • 3.識別到閉合標籤,則會轉交給end函數來處理,end函數 會摘取原生屬性如:id 、 class等等、還有ref、slot、component、key等等,並跟上下文(4)創建起父子關係。最終造成了樹的結構。

  • 4.一直重複 1~3直到stack沒有任何標籤。

流程圖大體以下:

其中的細節特別多,若是有興趣請關注咱們的動態,咱們會在下期給出詳細過程講解。 點擊這裏瞭解更多

2) optimize思想解讀。

咱們在上面已經給出了astElement的詳細屬性,其中有兩個屬性叫作staticRoot 和 staticInFor。而optimize 的過程,就是給astElement打上這兩個標記。這裏是爲了讓這類靜態節點,在render過程,可以走緩存的方式,只渲染一次。

好處很明顯,就是靜態節點不須要重複對比和 從新渲染,以此來提升總體性能。

3)code generate解讀

這個過程其實尚未生成vnode,而是生成一個執行函數,且包含了this的執行code,其格式以下:

with(this) { _c('div'...xxx...xxx) }
複製代碼

咱們在強調下,此次又是從astElement直接到另外一個String。 上面astElement是個樹狀結構。

而後在這個過程,基本一個astElement就對應一個短函數。 最基本短函數是createElement 也就是_c。 最終的樹狀結構,會以函數的形式表現處理函數以下

_c(
    'div',
    {
        key:'xxx',
        ref:'xx',
        pre:'xxx',
        domPro:xxx,
        ....
    },
    [ // chidren
        _v(_s('ding')),
        _c('p',{model:'isshow',}, [ ...xxx ])
    ]
)
複製代碼

能夠清晰的看到,最終造成的string,依然是一個樹狀形式,是以function形式展現的樹狀,並且全部屬性都已經抽離成createElement的第二個參數。

這裏對短函數作個簡單的說明:

  • 1.短函數最終執行短this其實就是Vue實例,或者組件實例。

  • 2.短函數比較重要的三個建立函數分別是_c(createElement)、_v(createTextVNode)、_e(createEmptyVNode),分別生成三種類型的vnode。

一句話歸納下code generate作的事情就是:

生成vnode的前置工做,抽離astElement全部的屬性,造成短函數鏈。

短函數對應大體以下:

export function installRenderHelpers (target: any) {
  target._o = markOnce // v-once
  target._n = toNumber
  target._s = toString
  target._l = renderList // v-for
  target._t = renderSlot // slot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic // static 
  target._f = resolveFilter 
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots // scopeSlot
  target._g = bindObjectListeners // linsners
  target._d = bindDynamicKeys
  target._p = prependModifier
}
複製代碼

4) render 函數。

這個過程是直接執行,就會獲得以vnode爲節點對虛擬dom樹,其細節包括了component的處理,會有單獨一節去介紹。 最後這些虛擬樹會丟給patch函數,最終在不斷對比對過程當中生成目標真實對dom樹。 這裏展現下vnode的格式。

declare interface VNodeData {
  key?: string | number;
  slot?: string;
  ref?: string;
  is?: string;
  pre?: boolean;
  tag?: string;
  staticClass?: string;
  class?: any;
  staticStyle?: { [key: string]: any };
  style?: string | Array<Object> | Object;
  normalizedStyle?: Object;
  props?: { [key: string]: any };
  attrs?: { [key: string]: string };
  domProps?: { [key: string]: any };
  hook?: { [key: string]: Function };
  on?: ?{ [key: string]: Function | Array<Function> };
  nativeOn?: { [key: string]: Function | Array<Function> };
  transition?: Object;
  show?: boolean; // marker for v-show
  inlineTemplate?: {
    render: Function;
    staticRenderFns: Array<Function>;
  };
  directives?: Array<VNodeDirective>;
  keepAlive?: boolean;
  scopedSlots?: { [key: string]: Function };
  model?: {
    value: any;
    callback: Function;
  };
};
複製代碼

總結

  • 1.編譯四大過程parse、optimize、codegen、render。
  • 2.用戶自定義的render實際上是忽略了前面三個步驟,直接定製的方式。
  • 3.vdom 最終是以vnode爲節點的樹狀結構,在patch過程,會以新老vdom進行對比,來決定哪些dom是須要更新或添加、刪除的。
相關文章
相關標籤/搜索