深刻剖析Vue源碼 - 組件進階

咱們知道,組件是Vue體系的核心,熟練使用組件是掌握Vue進行開發的基礎。上一節中,咱們深刻了解了Vue組件註冊到使用渲染的完整流程。這一節咱們會在上一節的基礎上介紹組件的兩個高級用法:異步組件和函數式組件。vue

6.1 異步組件

6.1.1 使用場景

Vue做爲單頁面應用遇到最棘手的問題是首屏加載時間的問題,單頁面應用會把頁面腳本打包成一個文件,這個文件包含着全部業務和非業務的代碼,而腳本文件過大也是形成首頁渲染速度緩慢的緣由。所以做爲首屏性能優化的課題,最經常使用的處理方法是對文件的拆分和代碼的分離。按需加載的概念也是在這個前提下引入的。咱們每每會把一些非首屏的組件設計成異步組件,部分不影響初次視覺體驗的組件也能夠設計爲異步組件。這個思想就是按需加載。通俗點理解,按需加載的思想讓應用在須要使用某個組件時纔去請求加載組件代碼。咱們藉助webpack打包後的結果會更加直觀。node

webpack遇到異步組件,會將其從主腳本中分離,減小腳本體積,加快首屏加載時間。當遇到場景須要使用該組件時,纔會去加載組件腳本。

6.1.2 工廠函數

Vue中容許用戶經過工廠函數的形式定義組件,這個工廠函數會異步解析組件定義,組件須要渲染的時候纔會觸發該工廠函數,加載結果會進行緩存,以供下一次調用組件時使用。 具體使用:webpack

// 全局註冊:
Vue.component('asyncComponent', function(resolve, reject) {
  require(['./test.vue'], resolve)
})
// 局部註冊:
var vm = new Vue({
  el: '#app',
  template: '<div id="app"><asyncComponent></asyncComponent></div>',
  components: {
    asyncComponent: (resolve, reject) => require(['./test.vue'], resolve),
    // 另外寫法
    asyncComponent: () => import('./test.vue'),
  }
})
複製代碼
6.1.3 流程分析

有了上一節組件註冊的基礎,咱們來分析異步組件的實現邏輯。簡單回憶一下上一節的流程,實例的掛載流程分爲根據渲染函數建立Vnode和根據Vnode產生真實節點的過程。期間建立Vnode過程,若是遇到子的佔位符節點會調用creatComponent,這裏會爲子組件作選項合併和鉤子掛載的操做,並建立一個以vue-component-爲標記的子Vnode,而異步組件的處理邏輯也是在這個階段處理。es6

// 建立子組件過程
  function createComponent (
    Ctor, // 子類構造器
    data,
    context, // vm實例
    children, // 子節點
    tag // 子組件佔位符
  ) {
    ···
    // 針對局部註冊組件建立子類構造器
    if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
    }
    // 異步組件分支
    var asyncFactory;
    if (isUndef(Ctor.cid)) {
      // 異步工廠函數
      asyncFactory = Ctor;
      // 建立異步組件函數
      Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
      if (Ctor === undefined) {
        return createAsyncPlaceholder(
          asyncFactory,
          data,
          context,
          children,
          tag
        )
      }
    }
    ···
    // 建立子組件vnode
    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
  }
複製代碼

**工廠函數的用法使得Vue.component(name, options)的第二個參數不是一個對象,所以不管是全局註冊仍是局部註冊,都不會執行Vue.extend生成一個子組件的構造器,**因此Ctor.cid不會存在,代碼會進入異步組件的分支。web

異步組件分支的核心是resolveAsyncComponent,它的處理邏輯分支衆多,咱們先關心工廠函數處理部分。算法

function resolveAsyncComponent (
    factory,
    baseCtor
  ) {
    if (!isDef(factory.owners)) {

      // 異步請求成功處理
      var resolve = function() {}
      // 異步請求失敗處理
      var reject = function() {}

      // 建立子組件時會先執行工廠函數,並將resolve和reject傳入
      var res = factory(resolve, reject);

      // resolved 同步返回
      return factory.loading
        ? factory.loadingComp
        : factory.resolved
    }
  }
複製代碼

若是常用promise進行開發,咱們很容易發現,這部分代碼像極了promsie原理內部的實現,針對異步組件工廠函數的寫法,大體能夠總結出如下三個步驟:數組

    1. 定義異步請求成功的函數處理,定義異步請求失敗的函數處理;
    1. 執行組件定義的工廠函數;
    1. 同步返回請求成功的函數處理。

resolve, reject的實現,都是once方法執行的結果,因此咱們先關注一下高級函數once的原理。爲了防止當多個地方調用異步組件時,resolve,reject不會重複執行,once函數保證了函數在代碼只執行一次。也就是說,once緩存了已經請求過的異步組件promise

// once函數保證了這個調用函數只在系統中調用一次
function once (fn) {
  // 利用閉包特性將called做爲標誌位
  var called = false;
  return function () {
    // 調用過則再也不調用
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  }
}
複製代碼

成功resolve和失敗reject的詳細處理邏輯以下:緩存

// 成功處理
var resolve = once(function (res) {
  // 轉成組件構造器,並將其緩存到resolved屬性中。
  factory.resolved = ensureCtor(res, baseCtor);
  if (!sync) {
    //強制更新渲染視圖
    forceRender(true);
  } else {
    owners.length = 0;
  }
});
// 失敗處理
var reject = once(function (reason) {
  warn(
    "Failed to resolve async component: " + (String(factory)) +
    (reason ? ("\nReason: " + reason) : '')
  );
  if (isDef(factory.errorComp)) {
    factory.error = true;
    forceRender(true);
  }
});
複製代碼

異步組件加載完畢,會調用resolve定義的方法,方法會經過ensureCtor將加載完成的組件轉換爲組件構造器,並存儲在resolved屬性中,其中 ensureCtor的定義爲:性能優化

function ensureCtor (comp, base) {
    if (comp.__esModule ||(hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
      comp = comp.default;
    }
    // comp結果爲對象時,調用extend方法建立一個子類構造器
    return isObject(comp)
      ? base.extend(comp)
      : comp
  }
複製代碼

組件構造器建立完畢,會進行一次視圖的從新渲染,因爲Vue是數據驅動視圖渲染的,而組件在加載到完畢的過程當中,並無數據發生變化,所以須要手動強制更新視圖。forceRender函數的內部會拿到每一個調用異步組件的實例,執行原型上的$forceUpdate方法,這部分的知識等到響應式系統時介紹。

異步組件加載失敗後,會調用reject定義的方法,方法會提示並標記錯誤,最後一樣會強制更新視圖。

回到異步組件建立的流程,執行異步過程會同步爲加載中的異步組件建立一個註釋節點Vnode

function createComponent (){
    ···
    // 建立異步組件函數
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    if (Ctor === undefined) {
      // 建立註釋節點
      return createAsyncPlaceholder(asyncFactory,data,context,children,tag)
    }
  }
複製代碼

createAsyncPlaceholder的定義也很簡單,其中createEmptyVNode以前有介紹過,是建立一個註釋節點vnode,而asyncFactory,asyncMeta都是用來標註該節點爲異步組件的臨時節點和相關屬性。

// 建立註釋Vnode
function createAsyncPlaceholder (factory,data,context,children,tag) {
  var node = createEmptyVNode();
  node.asyncFactory = factory;
  node.asyncMeta = { data: data, context: context, children: children, tag: tag };
  return node
}
複製代碼

執行forceRender觸發組件的從新渲染過程時,又會再次調用resolveAsyncComponent,這時返回值Ctor再也不爲 undefined了,所以會正常走組件的render,patch過程。這時,舊的註釋節點也會被取代。

6.1.4 Promise異步組件

異步組件的第二種寫法是在工廠函數中返回一個promise對象,咱們知道importes6引入模塊加載的用法,可是import是一個靜態加載的方法,它會優先模塊內的其餘語句執行。所以引入了import(),import()是一個運行時加載模塊的方法,能夠用來類比require()方法,區別在於前者是一個異步方法,後者是同步的,且import()會返回一個promise對象。

具體用法:

Vue.component('asyncComponent', () => import('./test.vue'))
複製代碼

源碼依然走着異步組件處理分支,而且大部分的處理過程仍是工廠函數的邏輯處理,區別在於執行異步函數後會返回一個promise對象,成功加載則執行resolve,失敗加載則執行reject.

var res = factory(resolve, reject);
// res是返回的promise
if (isObject(res)) {
  if (isPromise(res)) {
    if (isUndef(factory.resolved)) {
      // 核心處理
      res.then(resolve, reject);
    }
  }
}
複製代碼

其中promise對象的判斷最簡單的是判斷是否有thencatch方法:

// 判斷promise對象的方法
  function isPromise (val) {
    return (isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function')
  }
複製代碼
6.1.5 高級異步組件

爲了在操做上更加靈活,好比使用loading組件處理組件加載時間過長的等待問題,使用error組件處理加載組件失敗的錯誤提示等,Vue在2.3.0+版本新增了返回對象形式的異步組件格式,對象中能夠定義須要加載的組件component,加載中顯示的組件loading,加載失敗的組件error,以及各類延時超時設置,源碼一樣進入異步組件分支。

Vue.component('asyncComponent', () => ({
  // 須要加載的組件 (應該是一個 `Promise` 對象)
  component: import('./MyComponent.vue'),
  // 異步組件加載時使用的組件
  loading: LoadingComponent,
  // 加載失敗時使用的組件
  error: ErrorComponent,
  // 展現加載時組件的延時時間。默認值是 200 (毫秒)
  delay: 200,
  // 若是提供了超時時間且組件加載也超時了,
  // 則使用加載失敗時使用的組件。默認值是:`Infinity`
  timeout: 3000
}))
複製代碼

異步組件函數執行後返回一個對象,而且對象的component執行會返回一個promise對象,所以進入高級異步組件處理分支。

if (isObject(res)) {
  if (isPromise(res)) {}
  // 返回對象,且res.component返回一個promise對象,進入分支
  // 高級異步組件處理分支
  else if (isPromise(res.component)) {
    // 和promise異步組件處理方式相同
    res.component.then(resolve, reject);
    ···
  }
}
複製代碼

異步組件會等待響應成功失敗的結果,與此同時,代碼繼續同步執行。高級選項設置中若是設置了errorloading組件,會同時建立兩個子類的構造器,

if (isDef(res.error)) {
  // 異步錯誤時組件的處理,建立錯誤組件的子類構造器,並賦值給errorComp
  factory.errorComp = ensureCtor(res.error, baseCtor);
}

if (isDef(res.loading)) {
  // 異步加載時組件的處理,建立錯誤組件的子類構造器,並賦值給errorComp
  factory.loadingComp = ensureCtor(res.loading, baseCtor);
}
複製代碼

若是存在delay屬性,則經過settimeout設置loading組件顯示的延遲時間。factory.loading屬性用來標註是不是顯示loading組件。

if (res.delay === 0) {
  factory.loading = true;
} else {
  // 超過期間會成功加載,則執行失敗結果
  setTimeout(function () {
    if (isUndef(factory.resolved) && isUndef(factory.error)) {
      factory.loading = true;
      forceRender(false);
    }
  }, res.delay || 200);
}
複製代碼

若是在timeout時間內,異步組件還未執行resolve的成功結果,即resolve沒有賦值,則進行reject失敗處理。

接下來依然是渲染註釋節點或者渲染loading組件,等待異步處理結果,根據處理結果從新渲染視圖節點,類似過程再也不闡述。

6.1.6 wepack異步組件用法

webpack做爲Vue應用構建工具的標配,咱們須要知道Vue如何結合webpack進行異步組件的代碼分離,而且須要關注分離後的文件名,這個名字在webpack中稱爲chunkNamewebpack爲異步組件的加載提供了兩種寫法。

  • require.ensure:它是webpack傳統提供給異步組件的寫法,在編譯時,webpack會靜態地解析代碼中的 require.ensure(),同時將模塊添加到一個分開的 chunk 中,其中函數的第三個參數爲分離代碼塊的名字。修改後的代碼寫法以下:
Vue.component('asyncComponent', function (resolve, reject) {
   require.ensure([], function () {
     resolve(require('./test.vue'));
   }, 'asyncComponent'); // asyncComponent爲chunkname
})
複製代碼
  • import(/* webpackChunkName: "asyncComponent" */, component): 有了es6,import的寫法是現今官方最推薦的作法,其中經過註釋webpackChunkName來指定分離後組件模塊的命名。修改後的寫法以下:
Vue.component('asyncComponent', () => import(/* webpackChunkName: "asyncComponent" */, './test.vue'))
複製代碼

至此,咱們已經掌握了全部異步組件的寫法,並深刻了解了其內部的實現細節。我相信全面的掌握異步組件對從此單頁面性能優化方面會起到積極的指導做用。

6.2 函數式組件

Vue提供了一種可讓組件變爲無狀態、無實例的函數化組件。從原理上說,通常子組件都會通過實例化的過程,而單純的函數組件並無這個過程,它能夠簡單理解爲一箇中間層,只處理數據,不建立實例,也是因爲這個行爲,它的渲染開銷會低不少。實際的應用場景是,當咱們須要在多個組件中選擇一個來代爲渲染,或者在將children,props,data等數據傳遞給子組件前進行數據處理時,咱們均可以用函數式組件來完成,它本質上也是對組件的一個外部包裝。

6.2.1 使用場景

  • 定義兩個組件對象,test1,test2
var test1 = {
  props: ['msg'],
  render: function (createElement, context) {
    return createElement('h1', this.msg)
  }
}
var test2 = {
  props: ['msg'],
  render: function (createElement, context) {
    return createElement('h2', this.msg)
  }
}
複製代碼
  • 定義一個函數式組件,它會根據計算結果選擇其中一個組件進行選項
Vue.component('test3', {
  // 函數式組件的標誌 functional設置爲true
  functional: true,
  props: ['msg'],
  render: function (createElement, context) {
    var get = function() {
      return test1
    }
    return createElement(get(), context)
  }
})
複製代碼
  • 函數式組件的使用
<test3 :msg="msg" id="test">
</test3>
new Vue({
  el: '#app',
  data: {
    msg: 'test'
  }
})
複製代碼
  • 最終渲染的結果爲:
<h2>test</h2>
複製代碼

6.2.2 源碼分析

函數式組件會在組件的對象定義中,將functional屬性設置爲true,這個屬性是區別普通組件和函數式組件的關鍵。一樣的在遇到子組件佔位符時,會進入createComponent進行子組件Vnode的建立。**因爲functional屬性的存在,代碼會進入函數式組件的分支中,並返回createFunctionalComponent調用的結果。**注意,執行完createFunctionalComponent後,後續建立子Vnode的邏輯不會執行,這也是以後在建立真實節點過程當中不會有子Vnode去實例化子組件的緣由。(無實例)

function createComponent(){
  ···
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }
}
複製代碼

createFunctionalComponent方法會對傳入的數據進行檢測和合並,實例化FunctionalRenderContext,最終調用函數式組件自定義的render方法執行渲染過程。

function createFunctionalComponent(
  Ctor, // 函數式組件構造器
  propsData, // 傳入組件的props
  data, // 佔位符組件傳入的attr屬性
  context, // vue實例
  children// 子節點
){
  // 數據檢測合併
  var options = Ctor.options;
  var props = {};
  var propOptions = options.props;
  if (isDef(propOptions)) {
    for (var key in propOptions) {
      props[key] = validateProp(key, propOptions, propsData || emptyObject);
    }
  } else {
    // 合併attrs
    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
    // 合併props
    if (isDef(data.props)) { mergeProps(props, data.props); }
  }
  var renderContext = new FunctionalRenderContext(data,props,children,contextVm,Ctor);
  // 調用函數式組件中自定的render函數
  var vnode = options.render.call(null, renderContext._c, renderContext)
}
複製代碼

FunctionalRenderContext這個類最終的目的是定義一個和真實組件渲染不一樣的render方法。

function FunctionalRenderContext() {
  // 省略其餘邏輯
  this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };
}
複製代碼

執行render函數的過程,又會遞歸調用createElement的方法,這時的組件已是真實的組件,開始執行正常的組件掛載流程。

問題:爲何函數式組件須要定義一個不一樣的createElement方法?- 函數式組件createElement和以往惟一的不一樣是,最後一個參數的不一樣,以前章節有說到,createElement會根據最後一個參數決定是否對子Vnode進行拍平,通常狀況下,children編譯生成結果都是Vnode類型,只有函數式組件比較特殊,它能夠返回一個數組,這時候拍平就是有必要的。咱們看下面的例子:

Vue.component('test', {  
  functional: true,  
  render: function (createElement, context) {  
    return context.slots().default  
  }  
}) 

<test> 
     <p>slot1</p> 
     <p>slot</p> 
</test>
複製代碼

此時函數式組件testrender函數返回的是兩個slotVnode,它是以數組的形式存在的,這就是須要拍平的場景。

簡單總結一下函數式組件,從源碼中能夠看出,函數式組件並不會像普通組件那樣有實例化組件的過程,所以包括組件的生命週期,組件的數據管理這些過程都沒有,它只會原封不動的接收傳遞給組件的數據作處理,並渲染須要的內容。所以做爲純粹的函數能夠也大大下降渲染的開銷。

6.3 小結

這一小節在組件基礎之上介紹了兩個進階的用法,異步組件和函數式組件。它們都是爲了解決某些類型場景引入的高級組件用法。其中異步組件是首屏性能優化的一個解決方案,而且Vue提供了多達三種的使用方法,高級配置的用法更讓異步組件的使用更加靈活。固然大部分狀況下,咱們會結合webpack進行使用。另外,函數式組件在多組件中選擇渲染內容的場景做用非凡,因爲是一個無實例的組件,它在渲染開銷上比普通組件的性能更好。


相關文章
相關標籤/搜索