Vue2.0源碼閱讀筆記(七):組件

  傳統的頁面開發主張將內容、樣式和行爲分開,便於開發和維護。等到React、Vue等MVVM前端框架大行其道時,人們更傾向於使用html、css、js聚合在一塊兒建立組件,經過編寫小型、獨立和一般可複用的組件來構建大型應用。
  組件是現代開發框架的基石,下面詳細介紹Vue組件的實現原理。
css

1、註冊組件

  在Vue中組件註冊分爲兩種:局部註冊、全局註冊。全局註冊是經過 Vue.component 方法進行的,局部註冊是經過在實例化組件時添加 components 選項完成的。
  下面詳細介紹組件註冊以及相關內容。
html

一、Vue.options.components

  Vue.optionscomponents 屬性是在 /src/core/global-api/index.js 文件中調用 initGlobalAPI 函數來定義的。
前端

initGlobalAPI(Vue)

// initGlobalAPI 代碼
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)

// ASSET_TYPES
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

// builtInComponents
import KeepAlive from './keep-alive'
export default { KeepAlive }
複製代碼

  在 /src/platforms/web/runtime/index.js 文件中會對 Vue.options.components 進一步賦值。
vue

import platformComponents from './components/index'

extend(Vue.options.components, platformComponents)

// platformComponents
import Transition from './transition'
import TransitionGroup from './transition-group'

export default {
  Transition,
  TransitionGroup
}
複製代碼

  最終 Vue.options.components 中會包含三個內置組件:
node

Vue.options.components = {
  KeepAlive: {/* ... */},
  Transition: {/* ... */},
  TransitionGroup: {/* ... */}
}
複製代碼

  在《選項合併》一文中講過,資源選項的合併是經過 mergeAssets 函數進行的。合併策略是以父選項對象爲原型,所以:
react

// vm 爲Vue實例,即Vue組件
vm.$options.components.prototype = Vue.options.components = {
  KeepAlive: {/* ... */},
  Transition: {/* ... */},
  TransitionGroup: {/* ... */}
}
複製代碼

二、Vue.extends

  Vue.extends 用來根據傳入的配置選項建立一個Vue構造函數的「子類」,精簡代碼以下:
web

Vue.extend = function (extendOptions) {
  /* ... */
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  /* ... */
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  if (name) {
    Sub.options.components[name] = Sub
  }
  /* ... */
  return Sub
}
複製代碼

  能夠看到 Vue.extend 返回的函數 VueComponent 跟Vue構造函數同樣,都是調用 _init 方法進行初始化。VueComponent 函數自身也會添加跟Vue相同的靜態屬性和方法。
  VueComponent 與 Vue 的主要區別是靜態屬性 options 不一樣。VueComponent.options 是將 Vue.extend 參數和原有構造函數的 options 參數經過 mergeOptions 函數進行合併而獲得的。
  另外,會將構造函數添加到自身的 options.components 對象屬性上,也就是說經過 VueComponent 實例化的對象上的屬性 $options.components.prototype 上除了內置組件還會有自定義組件的構造函數。
api

三、Vue.component全局註冊

  Vue關於資源的靜態方法(Vue.component、Vue.directive、Vue.filter)定義以下:
數組

initAssetRegisters(Vue)

function initAssetRegisters (Vue) {
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (id,definition ){
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}
複製代碼

  單看 Vue.component 方法其定義以下所示:
前端框架

Vue.component = function (id, definition){
  if (!definition) {
    return this.options.components[id]
  } else {
      // 組件名合法性檢測
      validateComponentName(id)

      if (isPlainObject(definition)) {
        definition.name = definition.name || id
        definition = Vue.extend(definition)
      }
      Vue.options.components[id] = definition
      return definition
  }
}
複製代碼

  由以上代碼可知,全局註冊的實質是根據全局註冊組件選項生成Vue子構造函數,而後將該子構造函數添加到Vue.options.components對象上

四、component選項局部註冊

  使用 components 選項來註冊組件,會將要註冊組件信息存儲在當前組件實例的 $options.components 對象上。
  在局部組件根據渲染函數生成對應VNode時,是由 createComponent 函數來最終生成VNode的。

function createComponent (Ctor,data,context,children,tag) {
  /*...*/
  var baseCtor = context.$options._base;
  // Ctor 爲組件配置對象
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }
  /*...*/
}
複製代碼

  由上述代碼可知,在生成VNode的過程當中會調用所在組件實例的 extend 方法根據註冊信息生成對應的子構造函數。

2、組件解析

  組件的解析過程和普通標籤同樣:

一、根據模板生成渲染函數。
二、根據渲染函數生成虛擬DOM。
三、根據虛擬DOM生成真實DOM。

  下面以一個簡單的例子來講明組件的解析過程:

<body>
  <div id="app"></div>
</body>
<script> var ComponentA = { template: '<div>組件A</div>' } var vue = new Vue({ el: '#app', template: `<div id="app" class="home"><component-a></component-a></div>`, components: { "component-a": ComponentA } }) </script>
複製代碼

一、渲染函數

  模板中組件生成的渲染函數比較簡單,跟標籤同樣由 _c() 函數包裹。_c() 的第一個參數爲組件名,第二個參數爲組件屬性對象,第三個參數爲使用 <slot> 接收的內容。

function anonymous() {
  with(this){
    return _c(
      'div',
      {staticClass:"home",attrs:{"id":"app"}},
      [_c('component-a')],
      1
    )
  }
}
複製代碼

  組件的具體配置參數信息存儲在 vm.$options.components 中:

vm.$options.components = {
  "component-a" : {
    template: "<div>組件A</div>"
  }
}
複製代碼

二、VNode

  組件生成VNode是調用渲染函數中的 _c() 完成的,_c() 最終會調用 _createElement 來生成VNode。_createElement 中關於組件處理的代碼以下所示:

// context 爲當前組件實例
if ((!data || !data.pre) && 
  isDef(Ctor = resolveAsset(context.$options, 'components', tag){
    vnode = createComponent(Ctor, data, context, children, tag);
}
複製代碼

(一)resolveAsset 獲取組件註冊信息

  resolveAsset 對組件類型資源的處理代碼以下:

function resolveAsset (options,type,id,warnMissing) {
  if (typeof id !== 'string') { return }
    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] }

    var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
    if (warnMissing && !res) {
      warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id,options);
    }
    return res
  }
複製代碼

  該函數對組件的處理比較有意思,首先是關於組件名稱的問題:在模板中使用的組件名稱,在組件註冊時能夠有三種形式。註冊時能夠跟使用時保持一致,也可使用駝峯命名或者首字母大寫的駝峯命名。
  其次是關於組件局部註冊以及全局註冊的問題:局部註冊的組件會保存在 vm.$options.components 中,全局註冊的組件保存在 Vue.options.components 中,而 Vue.options.componentsvm.$options.components 的原型鏈上。
  resolveAsset 函數查詢組件註冊信息會先查註冊的局部變量,若是找不到再沿着原型鏈查詢。這就是局部組件只能自身使用,全局註冊的組件可以全局使用的緣由。

(二)createComponent

  組件VNode生成函數 createComponent 的精簡代碼以下所示:

function createComponent (Ctor,data,context,children,tag){
  if (isUndef(Ctor)) { return }

  const baseCtor = context.$options._base
  if (isObject(Ctor)) {Ctor = baseCtor.extend(Ctor)}
  /* 省略異步組件相關處理代碼 */
  data = data || {}
  resolveConstructorOptions(Ctor)
  /* 省略v-model相關處理代碼 */
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  /* 省略函數式組件相關處理代碼 */
  const listeners = data.on
  data.on = data.nativeOn
  /* 省略抽象組件相關處理代碼 */
  installComponentHooks(data)

  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  /* 省略WEEX相關代碼 */
  return vnode
}
複製代碼

  首先是根據局部組件註冊信息調用 extend 方法生成子構造函數,而後調用 resolveConstructorOptions 函數來更新子構造函數的 options 屬性。這裏會有一個疑問:在 extend 方法中已經使用 mergeOptions 方法完成對子構造函數 options 屬性合併更新,爲何還要調用 resolveConstructorOptions 函數處理 options?

function resolveConstructorOptions (Ctor) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      Ctor.superOptions = superOptions
      const modifiedOptions = resolveModifiedOptions(Ctor)
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}
複製代碼

  這是爲了防止在組件構造函數建立之後使用全局 mixins 更改父構造函數的選項,resolveConstructorOptions 函數的做用就是根據原型鏈上對象的 options 值來更新子構造函數的 options
  接着調用 extractPropsFromVNodeData 函數來從當前實例中提取局部組件 props 的值,調用 installComponentHooks 來在 data 屬性上安裝組件的鉤子函數。
  最後使用 new VNode() 來生成組件類型VNode,傳入的第一個參數是根據組件名拼接處理的;第三個參數不傳,也就是說組件VNode沒有 children 屬性;與生成其餘類型VNode不一樣,第七個參數會傳入組件選項對象 componentOptions;第八個參數會根據是否爲異步組件而傳入不一樣的值。

(三)installComponentHooks

  組件鉤子安裝函數 installComponentHooks 以及相關代碼以下所示:

const componentVNodeHooks = {
  init (vnode, hydrating) {/* 省略具體實現 */},
  prepatch (oldVnode, vnode) {/* 省略具體實現 */},
  insert (vnode) {/* 省略具體實現 */},
  destroy (vnode) {/* 省略具體實現 */}
}

const hooksToMerge = Object.keys(componentVNodeHooks)

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];
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
    }
  }
}
function mergeHook (f1, f2) {
  const merged = (a, b) => {
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}
複製代碼

  組件鉤子函數生成邏輯比較簡單:將 data.hookcomponentVNodeHooks 中的函數加以合併,合併策略爲將同名函數合併到同一函數中。
  若是本來 data.hook 中沒有鉤子函數,則最終 data.hook 的值以下所示:

data.hook = componentVNodeHooks = {
  init (vnode, hydrating) {/* 省略具體實現 */},
  prepatch (oldVnode, vnode) {/* 省略具體實現 */},
  insert (vnode) {/* 省略具體實現 */},
  destroy (vnode) {/* 省略具體實現 */}
}
複製代碼

  最終有四個鉤子函數:init、prepatch、insert、destroy。鉤子函數的具體功能在後面用到時再詳細講解。

(四)使用new VNode()生成組件VNode

  構造函數 VNode() 中關於生成組件實例的代碼以下所示:

export default class VNode {
  constructor (tag,data,children,text,elm,
    context,componentOptions,asyncFactory) {
    this.tag = tag
    this.data = data
    this.context = context
    this.componentOptions = componentOptions
    this.asyncFactory = asyncFactory
    /*省略...*/
  }
  get child (){
    return this.componentInstance
  }
}
複製代碼

  例子中的組件VNode最終以下所示:

vnode = {
  tag: 'vue-component-1-component-a',
  data:{
    on: undefined,
    hook:{
      init (vnode, hydrating) {/* 省略具體實現 */},
      prepatch (oldVnode, vnode) {/* 省略具體實現 */},
      insert (vnode) {/* 省略具體實現 */},
      destroy (vnode) {/* 省略具體實現 */}
    }
  },
  componentOptions:{
    Ctor: function VueComponent(options){/*組件構造函數*/}
    tag: "component-a"
    children: undefined
    listeners: undefined
    propsData: undefined
  },
  asyncFactory: undefined,
  componentInstance: undefined
  /*省略...*/
}
複製代碼

三、patch

  在 patch 的過程當中,組件類型VNode生成真實DOM是調用函數 createPatchFunction 中的內部函數 createComponent 來完成的。

(一)createComponent

  函數 createComponent 代碼以下所示:

function createComponent(vnode,insertedVnodeQueue,parentElm,refElm){
  var i = vnode.data
  if (isDef(i)) {
    var isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false)
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
複製代碼

  在不考慮 keepAlive 的狀況下,組件類型VNode生成DOM的過程爲:

一、調用 data.hook.init 方法生成組件實例 componentInstance 屬性,並完成組件的掛載。
二、調用 initComponent 函數使用鉤子函數完成組件初始化。
三、調用 insert 方法將生成的DOM插入。

(二)init鉤子函數

  鉤子函數 init 函數代碼以下所示:

init (vnode, hydrating) {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    const mountedNode = vnode
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = 
    vnode.componentInstance = 
    createComponentInstanceForVnode(vnode,activeInstance)

    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
}
複製代碼

  keepAlive 的狀況在後續講解內置組件時闡述。通常狀況下會走 else 分支,使用 createComponentInstanceForVnode 函數建立 VNode 的組件實例屬性。最後調用組件實例的 $mount 方法掛載實例。

function createComponentInstanceForVnode (vnode,parent) {
  var options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent: parent
  };
  var inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  return new vnode.componentOptions.Ctor(options)
}
複製代碼

  createComponentInstanceForVnode 函數主要做用是調取組件的構造函數生成組件構造實例。

(三)初始化組件函數initComponent

  initComponent 函數在組件 tag 存在的狀況下,主要做用是調用局部變量 cbs.create 中的各類鉤子函數來完成初始化,cbs.createVirtual DOM一文中有詳細介紹。 以後使用 setScope 設置 style 做用域。

function initComponent (vnode, insertedVnodeQueue) {
  if (isDef(vnode.data.pendingInsert)) {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
    vnode.data.pendingInsert = null;
  }
  vnode.elm = vnode.componentInstance.$el;
  if (isPatchable(vnode)) {
    invokeCreateHooks(vnode, insertedVnodeQueue);
    setScope(vnode);
  } else {
    registerRef(vnode);
    insertedVnodeQueue.push(vnode);
  }
}
複製代碼

3、函數式組件

  函數式組件跟 react 裏面的無狀態組件很類似,函數式組件無狀態 (沒有響應式數據),也沒有實例 (沒有 this 上下文)。由於只是函數,沒有實例,因此函數式組件相對於普通組件來講渲染開銷較低。
  下面先簡單介紹函數式組件的使用,再闡述其源碼實現。

一、函數式組件的使用

  函數式組件的使用方式通常有兩種:

一、使用 Vue.component 聲明組件時,在選項中將 functional 屬性置爲 true,且手動實現 render 函數。
二、在單文件組件中,使用 <template functional> 代替 <template> 聲明模板。

  函數式組件的中的 render 函數除了第一個 createElement 參數以外,還添加了第二個參數 context 對象。組件須要的一切都是經過 context 參數傳遞。
  context 對象包含屬性以下:

context = {
  props:{ /*提供全部 prop 的對象 */ },
  children: [ /*VNode 子節點的數組*/ ],
  slots: () => {},/*一個返回了包含全部插槽的對象的函數*/
  scopedSlots: { /*暴露傳入的做用域插槽的對象*/ },
  data: { /*不是數據對象,是組件屬性,createElement第二個參數*/ },
  parent: { /*對父組件的引用*/ },
  listeners: { /*事件監聽器的對象,data.on 的一個別名*/ },
  injections: { /*被 inject 選項注入的屬性。*/ },
}
複製代碼

  下面是一個簡單的函數式組件的例子,後續以此爲例闡述函數式組件原理。

<body>
  <div id="app"></div>
</body>
<script> var ComponentA = { functional: true, render: function(createElement,context) { return createElement('div',context.props.name) } } var vue = new Vue({ el: '#app', template: `<div id="app" class="home"> <component-a name='組件A'></component-a> </div>`, components: { "component-a": ComponentA } }) </script>
複製代碼

二、函數式組件實現原理

  依舊按照組件的編譯順序來探究其實現原理。

(一)生成渲染函數

  上述示例由模板生成的渲染函數以下所示,能夠看到,函數式組件生成渲染函數與普通組件並沒有不一樣之處。

with(this){
  return _c(
    'div',
    {staticClass:"home",attrs:{"id":"app"}},
    [
      _c('component-a',{attrs:{"name":"組件A"}})
    ],
    1
  )
}
複製代碼

(二)生成VNode

  在由渲染函數生成VNode的過程當中,會調用生成組件VNode的函數 createComponent,在該函數中有對函數式組件的特殊處理。

function createComponent(Ctor,data,context,children,tag){
  /* 省略... */
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor,propsData,data,context,children)
  }
  /* 省略... */
}
複製代碼

  從以上代碼中能夠看出,函數式組件的VNode是 createFunctionalComponent 函數的返回值。

function createFunctionalComponent(Ctor,propsData,data,contextVm,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 {
    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
    if (isDef(data.props)) { mergeProps(props, data.props); }
  }

  var renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  );

  var vnode = options.render.call(null, renderContext._c, renderContext);

  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  } else if (Array.isArray(vnode)) {
    var vnodes = normalizeChildren(vnode) || [];
    var res = new Array(vnodes.length);
    for (var i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext);
    }
    return res
  }
}
複製代碼

  createFunctionalComponent 函數主要有四個功能:

一、將 attrs、props 上的值都合併到 props 中。
二、根據傳入的 context 值,合併生成上下文參數對象 renderContext。
三、由手寫的 render 函數生成VNode。
四、克隆VNode,而後添加fnContext、fnOptions等屬性。

  這裏能夠看出函數式組件與普通組件最大的區別:普通組件生成組件VNode,VNode對上有指向組件實例的componentInstance屬性。函數式組件根據render函數生成VNode,自己並無相應的組件實例。
  根據函數式組件生成的VNode以下所示:

VNode = {
  /* 省略... */
  tag: "div",
  children: [{/*子節點VNode*/}],
  devtoolsMeta: {renderContext: {/*createElement第二個參數對象*/}},
  fnContext: {/*上下文信息*/},
  fnOptions: {/*函數式組件選項*/},
  isCloned: true,
  isRootInsert: true,
  componentInstance: undefined,
  componentOptions: undefined,
  data: undefined
  /* 省略... */
}
複製代碼

(三)patch

  在 patch 階段,由於根據函數式組件生成的VNode上並無組件選項 componentOptions 屬性,根據VNode生成真實DOM的過程與普通組件同樣。
  實際上,函數式組件僅僅是生成包裹內容對應的VNode,在生成真實DOM的時候,函數式組件徹底透明,生成的DOM由根據包裹內容而定的。

4、總結

  組件註冊的方式有兩種:局部註冊、全局註冊。組件註冊的實質是根據傳入的選項生成Vue子構造函數,在使用組件時使用子構造函數生成組件實例。全局註冊組件的信息在局部組件註冊對象的原型上,所以全局註冊的組件能夠不重複註冊而被全局使用。
  根據組件生成的渲染函數除了slot以外跟普通的標籤同樣,組件渲染函數生成的VNode上有組件選項信息屬性 componentOptions。在 patch 的過程當中,首先生成組件實例,而後根據組件實例生成真實DOM並掛載。
  普通組件都會生成對應的組件實例對象,相對而言開銷比較大。函數式組件不會生成專門的VNode以及實例對象,函數式組件至關於一個容器,在組件生成時直接渲染包裹的內容。

歡迎關注公衆號:前端桃花源,互相交流學習!

相關文章
相關標籤/搜索