深刻剖析Vue源碼 - Vue動態組件的概念,你會亂嗎?

前面花了兩節的內容介紹了組件,從組件的原理講到組件的應用,包括異步組件和函數式組件的實現和使用場景。衆所周知,組件是貫穿整個Vue設計理念的東西,而且也是指導咱們開發的核心思想,因此接下來的幾篇文章,將從新回到組件的內容去作源碼分析,首先會從經常使用的動態組件開始,包括內聯模板的原理,最後會簡單的提到內置組件的概念,爲以後的文章埋下伏筆。html

12.1 動態組件

動態組件我相信大部分在開發的過程當中都會用到,當咱們須要在不一樣的組件之間進行狀態切換時,動態組件能夠很好的知足咱們的需求,其中的核心是component標籤和is屬性的使用。vue

12.1.1 基本用法

例子是一個動態組件的基本使用場景,當點擊按鈕時,視圖根據this.chooseTabs值在組件child1,child2,child3間切換。node

// vue
<div id="app">
  <button @click="changeTabs('child1')">child1</button>
  <button @click="changeTabs('child2')">child2</button>
  <button @click="changeTabs('child3')">child3</button>
  <component :is="chooseTabs">
  </component>
</div>
// js
var child1 = {
  template: '<div>content1</div>',
}
var child2 = {
  template: '<div>content2</div>'
}
var child3 = {
  template: '<div>content3</div>'
}
var vm = new Vue({
  el: '#app',
  components: {
    child1,
    child2,
    child3
  },
  methods: {
    changeTabs(tab) {
      this.chooseTabs = tab;
    }
  }
})
複製代碼
12.1.2 AST解析

<component>的解讀和前面幾篇內容一致,會從AST解析階段提及,過程也不會專一每個細節,而是把和以往處理方式不一樣的地方特別說明。針對動態組件解析的差別,集中在processComponent上,因爲標籤上is屬性的存在,它會在最終的ast樹上打上component屬性的標誌。算法

//  針對動態組件的解析
function processComponent (el) {
  var binding;
  // 拿到is屬性所對應的值
  if ((binding = getBindingAttr(el, 'is'))) {
    // ast樹上多了component的屬性
    el.component = binding;
  }
  if (getAndRemoveAttr(el, 'inline-template') != null) {
    el.inlineTemplate = true;
  }
}
複製代碼

最終的ast樹以下:bash

12.1.3 render函數

有了ast樹,接下來是根據ast樹生成可執行的render函數,因爲有component屬性,render函數的產生過程會走genComponent分支。app

// render函數生成函數
var code = generate(ast, options);

// generate函數的實現
function generate (ast,options) {
  var state = new CodegenState(options);
  var code = ast ? genElement(ast, state) : '_c("div")';
  return {
    render: ("with(this){return " + code + "}"),
    staticRenderFns: state.staticRenderFns
  }
}

function genElement(el, state) {
  ···
  var code;
  // 動態組件分支
  if (el.component) {
    code = genComponent(el.component, el, state);
  }
}

複製代碼

針對動態組件的處理邏輯其實很簡單,當沒有內聯模板標誌時(後面會講),拿到後續的子節點進行拼接,和普通組件惟一的區別在於,_c的第一個參數再也不是一個指定的字符串,而是一個表明組件的變量。異步

// 針對動態組件的處理
  function genComponent (
    componentName,
    el,
    state
  ) {
    // 擁有inlineTemplate屬性時,children爲null
    var children = el.inlineTemplate ? null : genChildren(el, state, true);
    return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
  }
複製代碼

12.1.4 普通組件和動態組件的對比

其實咱們能夠對比普通組件和動態組件在render函數上的區別,結果一目瞭然。函數

普通組件的render函數

"with(this){return _c('div',{attrs:{"id":"app"}},[_c('child1',[_v(_s(test))])],1)}"源碼分析

動態組件的render函數

"with(this){return _c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component"})],1)}"post


簡單的總結,動態組件和普通組件的區別在於:

    1. ast階段新增了component屬性,這是動態組件的標誌
    1. 產生render函數階段因爲component屬性的存在,會執行genComponent分支,genComponent會針對動態組件的執行函數進行特殊的處理,和普通組件不一樣的是,_c的第一個參數再也不是不變的字符串,而是指定的組件名變量。
    1. rendervnode階段和普通組件的流程相同,只是字符串換成了變量,並有{ tag: 'component' }data屬性。例子中chooseTabs此時取的是child1

有了render函數,接下來從vnode到真實節點的過程和普通組件在流程和思路上基本一致,這一階段能夠回顧以前介紹組件流程的分析


12.1.5 疑惑

因爲本身對源碼的理解還不夠透徹,讀了動態組件的建立流程以後,心中產生了一個疑問,從原理的過程分析,動態組件的核心實際上是is這個關鍵字,它在編譯階段就以component屬性將該組件定義爲動態組件,而component做爲標籤好像並無特別大的用途,只要有is關鍵字的存在,組件標籤名設置爲任意自定義標籤均可以達到動態組件的效果?(componenta, componentb)。這個字符串僅以{ tag: 'component' }的形式存在於vnodedata屬性存在。那是否是說明,所謂動態組件只是因爲is的單方面限制?那component標籤的意義又在哪裏?(求教大佬!!)


12.2 內聯模板

因爲動態組件除了有is做爲傳值外,還能夠有inline-template做爲配置,藉此前提,恰好能夠理清楚Vue中內聯模板的原理和設計思想。Vue在官網有一句醒目的話,提示咱們inline-template 會讓模板的做用域變得更加難以理解。所以建議儘可能使用template選項來定義模板,而不是用內聯模板的形式。接下來,咱們經過源碼去定位一下所謂做用域難以理解的緣由。

咱們先簡單調整上面的例子,從使用角度上入手:

// html
<div id="app">
  <button @click="changeTabs('child1')">child1</button>
  <button @click="changeTabs('child2')">child2</button>
  <button @click="changeTabs('child3')">child3</button>
  <component :is="chooseTabs" inline-template>
    <span>{{test}}</span>
  </component>
</div>
// js
var child1 = {
  data() {
    return {
      test: 'content1'
    }
  }
}
var child2 = {
  data() {
    return {
      test: 'content2'
    }
  }
}
var child3 = {
  data() {
    return {
      test: 'content3'
    }
  }
}
var vm = new Vue({
  el: '#app',
  components: {
    child1,
    child2,
    child3
  },
  data() {
    return {
      chooseTabs: 'child1',
    }
  },
  methods: {
    changeTabs(tab) {
      this.chooseTabs = tab;
    }
  }
})
複製代碼

例子中達到的效果和文章第一個例子一致,很明顯和以往認知最大的差別在於,父組件裏的環境能夠訪問到子組件內部的環境變量。初看以爲挺難以想象的。咱們回憶一下以前父組件能訪問到子組件的情形,從大的方向上有兩個:

- 1. 採用事件機制,子組件經過$emit事件,將子組件的狀態告知父組件,達到父訪問子的目的。 - 2. 利用做用域插槽的方式,將子的變量經過props的形式傳遞給父,而父經過v-slot的語法糖去接收,而咱們以前分析的結果是,這種方式本質上仍是經過事件派發的形式去通知父組件。

以前分析過程也有提過父組件沒法訪問到子環境的變量,其核心的緣由在於: 父級模板裏的全部內容都是在父級做用域中編譯的;子模板裏的全部內容都是在子做用域中編譯的。 那麼咱們有理由猜測,內聯模板是否是違背了這一原則,讓父的內容放到了子組件建立過程去編譯呢?咱們接着往下看:

回到ast解析階段,前面分析到,針對動態組件的解析,關鍵在於processComponent函數對is屬性的處理,其中還有一個關鍵是對inline-template的處理,它會在ast樹上增長inlineTemplate屬性。

//  針對動態組件的解析
  function processComponent (el) {
    var binding;
    // 拿到is屬性所對應的值
    if ((binding = getBindingAttr(el, 'is'))) {
      // ast樹上多了component的屬性
      el.component = binding;
    }
    // 添加inlineTemplate屬性
    if (getAndRemoveAttr(el, 'inline-template') != null) {
      el.inlineTemplate = true;
    }
  }
複製代碼

render函數生成階段因爲inlineTemplate的存在,父的render函數的子節點爲null,這一步也決定了inline-template下的模板並非在父組件階段編譯的,那模板是如何傳遞到子組件的編譯過程呢?答案是模板以屬性的形式存在,待到子實例時拿到屬性值

function genComponent (componentName,el,state) {
  // 擁有inlineTemplate屬性時,children爲null
  var children = el.inlineTemplate ? null : genChildren(el, state, true);
  return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}
複製代碼

咱們看看最終render函數的結果,其中模板以{render: function(){···}}的形式存在於父組件的inlineTemplate屬性中。

"_c('div',{attrs:{"id":"app"}},[_c(chooseTabs,{tag:"component",inlineTemplate:{render:function(){with(this){return _c('span',[_v(_s(test))])}},staticRenderFns:[]}})],1)"

最終vnode結果也顯示,inlineTemplate對象會保留在父組件的data屬性中。

// vnode結果
{
  data: {
    inlineTemplate: {
      render: function() {}
    },
    tag: 'component'
  },
  tag: "vue-component-1-child1"
}
複製代碼

有了vnode後,來到了關鍵的最後一步,根據vnode生成真實節點的過程。從根節點開始,遇到vue-component-1-child1,會經歷實例化建立子組件的過程,實例化子組件前會先對inlineTemplate屬性進行處理。

function createComponentInstanceForVnode (vnode,parent) {
    // 子組件的默認選項
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    var inlineTemplate = vnode.data.inlineTemplate;
    // 內聯模板的處理,分別拿到render函數和staticRenderFns
    if (isDef(inlineTemplate)) {
      options.render = inlineTemplate.render;
      options.staticRenderFns = inlineTemplate.staticRenderFns;
    }
    // 執行vue子組件實例化
    return new vnode.componentOptions.Ctor(options)
  }
複製代碼

子組件的默認選項配置會根據vnode上的inlineTemplate屬性拿到模板的render函數。分析到這一步結論已經很清楚了。內聯模板的內容最終會在子組件中解析,因此模板中能夠拿到子組件的做用域這個現象也不足爲奇了。


12.3 內置組件

最後說說Vue思想中的另外一個概念,內置組件,其實vue的官方文檔有對內置組件進行了列舉,分別是component, transition, transition-group, keep-alive, slot,其中<slot>咱們在插槽這一節已經詳細介紹過,而component的使用這一節也花了大量的篇幅從使用到原理進行了分析。然而學習了slot,component以後,我開始意識到slotcomponent並非真正的內置組件。**內置組件是已經在源碼初始化階段就全局註冊好的組件。**而<slot><component>並無被當成一個組件去處理,所以也沒有組件的生命週期。slot只會在render函數階段轉換成renderSlot函數進行處理,而component也只是藉助is屬性將createElement的第一個參數從字符串轉換爲變量,僅此而已。所以從新回到概念的理解,**內置組件是源碼自身提供的組件,**因此這一部份內容的重點,會放在內置組件是何時註冊的,編譯時有哪些不一樣這兩個問題上來。這一部分只是一個拋磚引玉,接下來會有兩篇文章專門詳細介紹keep-alive,transition, transition-group的實現原理。

12.3.1 構造器定義組件

Vue初始化階段會在構造器的components屬性添加三個組件對象,每一個組件對象的寫法和咱們在自定義組件過程的寫法一致,有render函數,有生命週期,也會定義各類數據。

// keep-alive組件選項
var KeepAlive = {
  render: function() {}
}

// transition 組件選項
var Transition = {
  render: function() {}
}

// transition-group 組件選項
var TransitionGroup = {
  render: function() {},
  methods: {},
  ···
}

var builtInComponents = {
  KeepAlive: KeepAlive
};

var platformComponents = {
  Transition: Transition,
  TransitionGroup: TransitionGroup
};

// Vue構造器的選項配置,compoents選項合併
extend(Vue.options.components, builtInComponents);
extend(Vue.options.components, platformComponents);
複製代碼

extend方法咱們在系列的開頭,分析選項合併的時候有說過,將對象上的屬性合併到源對象中,屬性相同則覆蓋。

// 將_from對象合併到to對象,屬性相同時,則覆蓋to對象的屬性
function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}

複製代碼

最終Vue構造器擁有了三個組件的配置選項。

Vue.components = {
  keepAlive: {},
  transition: {},
  transition-group: {},
}
複製代碼
12.3.2 註冊內置組件

僅僅有定義是不夠的。組件須要被全局使用還得進行全局的註冊,這其實在深刻剖析Vue源碼 - 選項合併(下)已經闡述清楚了。Vue實例在初始化過程當中,最重要的第一步是進行選項的合併,而像內置組件這些資源類選項會有專門的選項合併策略,最終構造器上的組件選項會以原型鏈的形式註冊到實例的compoonents選項中(指令和過濾器同理)。

// 資源選項
var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];

// 定義資源合併的策略
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets; // 定義默認策略
});

function mergeAssets (parentVal,childVal,vm,key) {
    var res = Object.create(parentVal || null); // 以parentVal爲原型建立一個空對象
    if (childVal) {
      assertObjectType(key, childVal, vm); // components,filters,directives選項必須爲對象
      return extend(res, childVal) // 子類選項賦值給空對象
    } else {
      return res
    }
  }
複製代碼

關鍵的兩步一個是var res = Object.create(parentVal || null);,它會以parentVal爲原型建立一個空對象,最後是經過extend將用戶自定義的component選項複製到空對象中。選項合併以後,內置組件也所以在全局完成了註冊。

{
  components: {
    child1,
    __proto__: {
      keepAlive: {},
      transition: {},
      transitionGroup: {}
    }
  }
}
複製代碼

最後咱們看看內置組件對象中並無template模板,而是render函數,除了減小了耗性能的模板解析過程,我認爲重要的緣由是內置組件並無渲染的實體。最後的最後,讓咱們一塊兒期待後續對keep-alivetransition的原理分析,敬請期待。


相關文章
相關標籤/搜索