深刻剖析Vue源碼 - 組件基礎

組件是Vue的一個重要核心,咱們在進行項目工程化時,會將頁面的結構組件化。組件化意味着獨立和共享,而兩個結論並不矛盾,獨立的組件開發可讓開發者專一於某個功能項的開發和擴展,而組件的設計理念又使得功能項更加具備複用性,不一樣的頁面能夠進行組件功能的共享。對於開發者而言,編寫Vue組件是掌握Vue開發的核心基礎,Vue官網也花了大量的篇幅介紹了組件的體系和各類使用方法。這一節內容,咱們會深刻Vue組件內部的源碼,瞭解組件註冊的實現思路,並結合上一節介紹的實例掛載分析組件渲染掛載的基本流程,最後咱們將分析組件和組件之間是如何創建聯繫的。我相信,掌握這些底層的實現思路對於咱們從此在解決vue組件相關問題上會有明顯的幫助。html

5.1 組件兩種註冊方式

熟悉Vue開發流程的都知道,Vue組件在使用以前須要進行註冊,而註冊的方式有兩種,全局註冊和局部註冊。在進入源碼分析以前,咱們先回憶一下二者的用法,以便後續掌握二者的差別。vue

5.1.1 全局註冊
Vue.component('my-test', {
    template: '<div>{{test}}</div>',
    data () {
        return {
            test: 1212
        }
    }
})
var vm = new Vue({
    el: '#app',
    template: '<div id="app"><my-test><my-test/></div>'
})
複製代碼

其中組件的全局註冊須要在全局實例化Vue前調用,註冊以後能夠用在任何新建立的Vue實例中調用。html5

5.1.2 局部註冊
var myTest = {
    template: '<div>{{test}}</div>',
    data () {
        return {
            test: 1212
        }
    }
}
var vm = new Vue({
    el: '#app',
    component: {
        myTest
    }
})
複製代碼

當只須要在某個局部用到某個組件時,可使用局部註冊的方式進行組件註冊,此時局部註冊的組件只能在註冊該組件內部使用。node

5.1.3 註冊過程

在簡單回顧組件的兩種註冊方式後,咱們來看註冊過程到底發生了什麼,咱們以全局組件註冊爲例。它經過Vue.component(name, {...})進行組件註冊,Vue.component是在Vue源碼引入階段定義的靜態方法。算法

// 初始化全局api
initAssetRegisters(Vue);
var ASSET_TYPES = [
    'component',
    'directive',
    'filter'
];
function initAssetRegisters(Vue){
    // 定義ASSET_TYPES中每一個屬性的方法,其中包括component
    ASSET_TYPES.forEach(function (type) {
    // type: component,directive,filter
      Vue[type] = function (id,definition) {
          if (!definition) {
            // 直接返回註冊組件的構造函數
            return this.options[type + 's'][id]
          }
          ...
          if (type === 'component') {
            // 驗證component組件名字是否合法
            validateComponentName(id);
          }
          if (type === 'component' && isPlainObject(definition)) {
            // 組件名稱設置
            definition.name = definition.name || id;
            // Vue.extend() 建立子組件,返回子類構造器
            definition = this.options._base.extend(definition);
          }
          // 爲Vue.options 上的component屬性添加將子類構造器
          this.options[type + 's'][id] = definition;
          return definition
        }
    });
}
複製代碼

Vue.components有兩個參數,一個是須要註冊組件的組件名,另外一個是組件選項,若是第二個參數沒有傳遞,則會直接返回註冊過的組件選項。不然意味着須要對該組件進行註冊,註冊過程先會對組件名的合法性進行檢測,要求組件名不容許出現非法的標籤,包括Vue內置的組件名,如slot, component等。api

function validateComponentName(name) {
    if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
      // 正則判斷檢測是否爲非法的標籤
      warn(
        'Invalid component name: "' + name + '". Component names ' +
        'should conform to valid custom element name in html5 specification.'
      );
    }
    // 不能使用Vue自身自定義的組件名,如slot, component,不能使用html的保留標籤,如 h1, svg等
    if (isBuiltInTag(name) || config.isReservedTag(name)) {
      warn(
        'Do not use built-in or reserved HTML elements as component ' +
        'id: ' + name
      );
    }
  }
複製代碼

在通過組件名的合法性檢測後,會調用extend方法爲組件建立一個子類構造器,此時的this.options._base表明的就是Vue構造器。extend方法的定義在介紹選項合併章節有重點介紹過,它會基於父類去建立一個子類,此時的父類是Vue,而且建立過程子類會繼承父類的方法,並會和父類的選項進行合併,最終返回一個子類構造器。bash

代碼處還有一個邏輯,Vue.component()默認會把第一個參數做爲組件名稱,可是若是組件選項有name屬性時,name屬性值會將組件名覆蓋。app

總結起來,全局註冊組件就是Vue實例化前建立一個基於Vue的子類構造器,並將組件的信息加載到實例options.components對象中。dom

**接下來天然而然會想到一個問題,局部註冊和全局註冊在實現上的區別體如今哪裏?**咱們不急着分析局部組件的註冊流程,先以全局註冊的組件爲基礎,看看做爲組件,它的掛載流程有什麼不一樣。async

5.2 組件Vnode建立

上一節內容咱們介紹了Vue如何將一個模板,經過render函數的轉換,最終生成一個Vnode tree的,在不包含組件的狀況下,_render函數的最後一步是直接調用new Vnode去建立一個完整的Vnode tree。然而有一大部分的分支咱們並無分析,那就是遇到組件佔位符的場景。執行階段若是遇到組件,處理過程要比想像中複雜得多,咱們經過一張流程圖展開分析。

5.2.1 Vnode建立流程圖

5.2.2 具體流程分析

咱們結合實際的例子對照着流程圖分析一下這個過程:

  • 場景
Vue.component('test', {
  template: '<span></span>'
})
var vm = new Vue({
  el: '#app',
  template: '<div><test></test></div>'
})
複製代碼
  • render函數
function() {
  with(this){return _c('div',[_c('test')],1)}
}
複製代碼
  • Vue根實例初始化會執行 vm.$mount(vm.$options.el)實例掛載的過程,按照以前的邏輯,完整流程會經歷render函數生成Vnode,以及Vnode生成真實DOM的過程。
  • render函數生成Vnode過程當中,子會優先父執行生成Vnode過程,也就是_c('test')函數會先被執行。'test'會先判斷是普通的html標籤仍是組件的佔位符。
  • 若是爲通常標籤,會執行new Vnode過程,這也是上一章節咱們分析的過程;若是是組件的佔位符,則會在判斷組件已經被註冊過的前提下進入createComponent建立子組件Vnode的過程。
  • createComponent是建立組件Vnode的過程,建立過程會再次合併選項配置,並安裝組件相關的內部鉤子(後面文章會再次提到內部鉤子的做用),最後經過new Vnode()生成以vue-component開頭的Virtual DOM
  • render函數執行過程也是一個循環遞歸調用建立Vnode的過程,執行3,4步以後,完整的生成了一個包含各個子組件的Vnode tree

_createElement函數的實現以前章節分析過一部分,咱們重點看看組件相關的操做。

// 內部執行將render函數轉化爲Vnode的函數
function _createElement(context,tag,data,children,normalizationType) {
  ···
  if (typeof tag === 'string') {
    // 子節點的標籤爲普通的html標籤,直接建立Vnode
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      );
    // 子節點標籤爲註冊過的組件標籤名,則子組件Vnode的建立過程
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 建立子組件Vnode
      vnode = createComponent(Ctor, data, context, children, tag);
    }
  }
}
複製代碼

config.isReservedTag(tag)用來判斷標籤是否爲普通的html標籤,若是是普通節點會直接建立Vnode節點,若是不是,則須要判斷這個佔位符組件是否已經註冊到,咱們能夠經過context.$options.components[組件名]拿到註冊後的組件選項。如何判斷組件是否已經全局註冊,看看resolveAsset的實現。

// 須要明確組件是否已經被註冊
  function resolveAsset (options,type,id,warnMissing) {
    // 標籤爲字符串
    if (typeof id !== 'string') {
      return
    }
    // 這裏是 options.component
    var assets = options[type];
    // 這裏的分支分別支持大小寫,駝峯的命名規範
    if (hasOwn(assets, id)) { return assets[id] }
    var camelizedId = camelize(id);
    if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
    var PascalCaseId = capitalize(camelizedId);
    if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
    // fallback to prototype chain
    var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
    if (warnMissing && !res) {
      warn(
        'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
        options
      );
    }
    // 最終返回子類的構造器
    return res
  }
複製代碼

拿到註冊過的子類構造器後,調用createComponent方法建立子組件Vnode

// 建立子組件過程
  function createComponent (
    Ctor, // 子類構造器
    data,
    context, // vm實例
    children, // 子節點
    tag // 子組件佔位符
  ) {
    ···
    // Vue.options裏的_base屬性存儲Vue構造器
    var baseCtor = context.$options._base;

    // 針對局部組件註冊場景
    if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
    }
    data = data || {};
    // 構造器配置合併
    resolveConstructorOptions(Ctor);
    // 掛載組件鉤子
    installComponentHooks(data);

    // return a placeholder vnode
    var name = Ctor.options.name || tag;
    // 建立子組件vnode,名稱以 vue-component- 開頭
    var vnode = new VNode(("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),data, undefined, undefined, undefined, context,{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },asyncFactory);

    return vnode
  }
複製代碼

這裏將大部分的代碼都拿掉了,只留下建立Vnode相關的代碼,最終會經過new Vue實例化一個名稱以vue-component-開頭的Vnode節點。其中兩個關鍵的步驟是配置合併和安裝組件鉤子函數,選項合併的內容能夠查看這個系列的前兩節,這裏看看installComponentHooks安裝組件鉤子函數時作了哪些操做。

// 組件內部自帶鉤子
 var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
    },
    prepatch: function prepatch (oldVnode, vnode) {
    },
    insert: function insert (vnode) {
    },
    destroy: function destroy (vnode) {
    }
  };
var hooksToMerge = Object.keys(componentVNodeHooks);
// 將componentVNodeHooks 鉤子函數合併到組件data.hook中 
function installComponentHooks (data) {
    var hooks = data.hook || (data.hook = {});
    for (var i = 0; i < hooksToMerge.length; i++) {
      var key = hooksToMerge[i];
      var existing = hooks[key];
      var toMerge = componentVNodeHooks[key];
      // 若是鉤子函數存在,則執行mergeHook$1方法合併
      if (existing !== toMerge && !(existing && existing._merged)) {
        hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
      }
    }
  }
function mergeHook$1 (f1, f2) {
  // 返回一個依次執行f1,f2的函數
    var merged = function (a, b) {
      f1(a, b);
      f2(a, b);
    };
    merged._merged = true;
    return merged
  }
複製代碼

組件默認自帶的這幾個鉤子函數會在後續patch過程的不一樣階段執行,這部份內容不在本節的討論範圍。

5.2.3 局部註冊和全局註冊的區別

在說到全局註冊和局部註冊的用法時留下了一個問題,局部註冊和全局註冊二者的區別在哪裏。其實局部註冊的原理一樣簡單,咱們使用局部註冊組件時會經過在父組件選項配置中的components添加子組件的對象配置,這和全局註冊後在Vueoptions.component添加子組件構造器的結果很類似。區別在於:

- 1.局部註冊添加的對象配置是在某個組件下,而全局註冊添加的子組件是在根實例下。 - 2.局部註冊添加的是一個子組件的配置對象,而全局註冊添加的是一個子類構造器。

所以局部註冊中缺乏了一步構建子類構造器的過程,這個過程放在哪裏進行呢? 回到createComponent的源碼,源碼中根據選項是對象仍是函數來區分局部和全局註冊組件,若是選項的值是對象,則該組件是局部註冊的組件,此時在建立子Vnode時會調用 父類的extend方法去建立一個子類構造器。

function createComponent (...) {
  ...
  var baseCtor = context.$options._base;

  // 針對局部組件註冊場景
  if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
  }
}

複製代碼

5.3 組件Vnode渲染真實DOM

根據前面的分析,不論是全局註冊的組件仍是局部註冊的組件,組件並無進行實例化,那麼組件實例化的過程發生在哪一個階段呢?咱們接着看Vnode tree渲染真實DOM的過程。

5.3.1 真實節點渲染流程圖

5.3.2 具體流程分析
    1. 通過vm._render()生成完整的Virtual Dom樹後,緊接着執行Vnode渲染真實DOM的過程,這個過程是vm.update()方法的執行,而其核心是vm.__patch__
    1. vm.__patch__內部會經過 createElm去建立真實的DOM元素,期間遇到子Vnode會遞歸調用createElm方法。
    1. 遞歸調用過程當中,判斷該節點類型是否爲組件類型是經過createComponent方法判斷的,該方法和渲染Vnode階段的方法createComponent不一樣,他會調用子組件的init初始化鉤子函數,並完成組件的DOM插入。
    1. init初始化鉤子函數的核心是new實例化這個子組件並將子組件進行掛載,實例化子組件的過程又回到合併配置,初始化生命週期,初始化事件中心,初始化渲染的過程。實例掛載又會執行$mount過程。
    1. 完成全部子組件的實例化和節點掛載後,最後纔回到根節點的掛載。

__patch__核心代碼是經過createElm建立真實節點,當建立過程當中遇到子vnode時,會調用createChildren,createChildren的目的是對子vnode遞歸調用createElm建立子組件節點。

// 建立真實dom
function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {
  ···
  // 遞歸建立子組件真實節點,直到完成全部子組件的渲染才進行根節點的真實節點插入
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  ···
  var children = vnode.children;
  // 
  createChildren(vnode, children, insertedVnodeQueue);
  ···
  insert(parentElm, vnode.elm, refElm);
}
function createChildren(vnode, children, insertedVnodeQueue) {
  for (var i = 0; i < children.length; ++i) {
    // 遍歷子節點,遞歸調用建立真實dom節點的方法 - createElm
    createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
  }
}
複製代碼

createComponent方法會對子組件Vnode進行處理中,還記得在Vnode生成階段爲子Vnode安裝了一系列的鉤子函數嗎,在這個步驟咱們能夠經過是否擁有這些定義好的鉤子來判斷是不是已經註冊過的子組件,若是條件知足,則執行組件的init鉤子。

init鉤子作的事情只有兩個,實例化組件構造器,執行子組件的掛載流程。(keep-alive分支看具體的文章分析)

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  // 是否有鉤子函數能夠做爲判斷是否爲組件的惟一條件
  if (isDef(i = i.hook) && isDef(i = i.init)) {
    // 執行init鉤子函數
    i(vnode, false /* hydrating */);
  }
  ···
}
var componentVNodeHooks = {
  // 忽略keepAlive過程
  // 實例化
  var child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance);
  // 掛載
  child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
function createComponentInstanceForVnode(vnode, parent) {
  ···
  // 實例化Vue子組件實例
  return new vnode.componentOptions.Ctor(options)
}

複製代碼

顯然Vnode生成真實DOM的過程也是一個不斷遞歸建立子節點的過程,patch過程若是遇到子Vnode,會優先實例化子組件,而且執行子組件的掛載流程,而掛載流程又會回到_render,_update的過程。在全部的子Vnode遞歸掛載後,最終纔會真正掛載根節點。

5.4 創建組件聯繫

平常開發中,咱們能夠經過vm.$parent拿到父實例,也能夠在父實例中經過vm.$children拿到實例中的子組件。顯然,Vue在組件和組件之間創建了一層關聯。接下來的內容,咱們將探索如何創建組件之間的聯繫。

不論是父實例仍是子實例,在初始化實例階段有一個initLifecycle的過程。這個過程會**把當前實例添加到父實例的$children屬性中,並設置自身的$parent屬性指向父實例。**舉一個具體的應用場景:

<div id="app">
    <component-a></component-a>
</div>
Vue.component('component-a', {
    template: '<div>a</div>'
})
var vm = new Vue({ el: '#app'})
console.log(vm) // 將實例對象輸出
複製代碼

因爲vue實例向上沒有父實例,因此vm.$parentundefinedvm$children屬性指向子組件componentA 的實例。

子組件componentA$parent屬性指向它的父級vm實例,它的$children屬性指向爲空

源碼解析以下:

function initLifecycle (vm) {
    var options = vm.$options;
    // 子組件註冊時,會把父組件的實例掛載到自身選項的parent上
    var parent = options.parent;
    // 若是是子組件,而且該組件不是抽象組件時,將該組件的實例添加到父組件的$parent屬性上,若是父組件是抽象組件,則一直往上層尋找,直到該父級組件不是抽象組件,並將,將該組件的實例添加到父組件的$parent屬性
    if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
        }
        parent.$children.push(vm);
    }
    // 將自身的$parent屬性指向父實例。
    vm.$parent = parent;
    vm.$root = parent ? parent.$root : vm;

    vm.$children = [];
    vm.$refs = {};

    vm._watcher = null;
    vm._inactive = null;
    vm._directInactive = false;
    // 該實例是否掛載
    vm._isMounted = false;
    // 該實例是否被銷燬
    vm._isDestroyed = false;
    // 該實例是否正在被銷燬
    vm._isBeingDestroyed = false;
}

複製代碼

最後簡單講講抽象組件,在vue中有不少內置的抽象組件,例如<keep-alive></keep-alive>,<slot><slot>等,這些抽象組件並不會出如今子父級的路徑上,而且它們也不會參與DOM的渲染。

5.5 小結

這一小節,結合了實際的例子分析了組件註冊流程到組件掛載渲染流程,Vue中咱們能夠定義全局的組件,也能夠定義局部的組件,全局組件須要進行全局註冊,核心方法是Vue.component,他須要在根組件實例化前進行聲明註冊,緣由是咱們須要在實例化前拿到組件的配置信息併合併到options.components選項中。註冊的本質是調用extend建立一個子類構造器,全局和局部的不一樣是局部建立子類構造器是發生在建立子組件Vnode階段。而建立子Vnode階段最關鍵的一步是定義了不少內部使用的鉤子。有了一個完整的Vnode tree接下來會進入真正DOM的生成,在這個階段若是遇到子組件Vnode會進行子構造器的實例化,並完成子組件的掛載。遞歸完成子組件的掛載後,最終才又回到根組件的掛載。 有了組件的基本知識,下一節咱們重點分析一下組件的進階用法。


相關文章
相關標籤/搜索