Vue源碼分析—組件化(三)

合并配置

new Vue的過程通常有2種場景,一種是外部我們的代碼主動調(diào)用new Vue(options)的方式實例化一個Vue對象耍共;另一種是內(nèi)部通過new Vue(options)實例化子組件。
無論哪種場景猎塞,都會執(zhí)行實例的_init(options)方法试读,它首先會執(zhí)行一個merge options的邏輯,相關(guān)的代碼在src/core/instance/init.js中:

Vue.prototype._init = function (options?: Object) {
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
}

可以看到不同場景對于options的合并邏輯是不一樣的荠耽,并且傳入的options值也有非常大的不同钩骇,接下來分開介紹2種場景的options合并過程。
為了更直觀铝量,我們可以舉個簡單的示例:

import Vue from 'vue'

let childComp = {
  template: '<div>{{msg}}</div>',
  created() {
    console.log('child created')
  },
  mounted() {
    console.log('child mounted')
  },
  data() {
    return {
      msg: 'Hello Vue'
    }
  }
}

Vue.mixin({
  created() {
    console.log('parent created')
  }
})

let app = new Vue({
  el: '#app',
  render: h => h(childComp)
})

外部調(diào)用場景

當(dāng)執(zhí)行new Vue的時候倘屹,在執(zhí)行this._init(options)的時候,就會執(zhí)行如下邏輯去合并options

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

這里通過調(diào)用mergeOptions方法來合并慢叨,它實際上就是把resolveConstructorOptions(vm.constructor)的返回值和options做合并纽匙,resolveConstructorOptions的實現(xiàn)先不考慮,在我們這個場景下拍谐,它還是簡單返回vm.constructor.options烛缔,相當(dāng)于Vue.options馏段,那么這個值又是什么呢,其實在initGlobalAPI(Vue) 的時候定義了這個值践瓷,代碼在src/core/global-api/index.js中:

export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)
  // ...
}

首先通過Vue.options = Object.create(null)創(chuàng)建一個空對象毅弧,然后遍歷ASSET_TYPESASSET_TYPES的定義在src/shared/constants.js中:

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

所以上面遍歷ASSET_TYPES后的代碼相當(dāng)于:

Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}

接著執(zhí)行了Vue.options._base = Vue当窗。
最后通過extend(Vue.options.components, builtInComponents)把一些內(nèi)置組件擴展到Vue.options.components上够坐,Vue的內(nèi)置組件目前有<keep-alive><transition><transition-group>組件崖面,這也就是為什么我們在其它組件中使用<keep-alive>組件不需要注冊的原因元咙。
那么回到mergeOptions這個函數(shù),它的定義在src/core/util/options.js中:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

mergeOptions主要功能就是把parentchild這兩個對象根據(jù)一些合并策略巫员,合并成一個新對象并返回庶香。比較核心的幾步,先遞歸把extendsmixins 合并到parent上简识,然后遍歷parent赶掖,調(diào)用mergeField,然后再遍歷child七扰,如果key不在parent的自身屬性上奢赂,則調(diào)用mergeField

這里有意思的是mergeField函數(shù)颈走,它對不同的key有著不同的合并策略膳灶。舉例來說,對于生命周期函數(shù)立由,它的合并策略是這樣的:

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

這其中的LIFECYCLE_HOOKS的定義在src/shared/constants.js中:

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]

這里定義了Vue.js所有的鉤子函數(shù)名稱轧钓,所以對于鉤子函數(shù),他們的合并策略都是mergeHook函數(shù)锐膜。這個函數(shù)的實現(xiàn)也非常有意思毕箍,用了一個多層3元運算符,邏輯就是如果不存在childVal 道盏,就返回parentVal而柑;否則再判斷是否存在parentVal,如果存在就把childVal添加到 parentVal 后返回新數(shù)組捞奕;否則返回 childVal 的數(shù)組牺堰。所以回到mergeOptions函數(shù),一旦parentchild都定義了相同的鉤子函數(shù)颅围,那么它們會把2個鉤子函數(shù)合并成一個數(shù)組。

關(guān)于其它屬性的合并策略的定義都可以在src/core/util/options.js文件中看到恨搓。
通過執(zhí)行mergeField函數(shù)院促,把合并后的結(jié)果保存到options對象中筏养,最終返回它。
因此常拓,在我們當(dāng)前這個case下渐溶,執(zhí)行完如下合并后:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

vm.$options的值差不多是如下這樣:

vm.$options = {
  components: { },
  created: [
    function created() {
      console.log('parent created')
    }
  ],
  directives: { },
  filters: { },
  _base: function Vue(options) {
    // ...
  },
  el: "#app",
  render: function (h) {
    //...
  }
}

組件場景

由于組件的構(gòu)造函數(shù)是通過Vue.extend繼承自Vue的,先回顧一下這個過程弄抬,代碼定義在src/core/global-api/extend.js中茎辐。

/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  // ...
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )

  // ...
  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // ...
  return Sub
}

我們只保留關(guān)鍵邏輯,這里的extendOptions對應(yīng)的就是前面定義的組件對象掂恕,它會和Vue.options合并到Sub.opitons中拖陆。

接下來我們再回憶一下子組件的初始化過程,代碼定義在 src/core/vdom/create-component.js 中:

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}

這里的vnode.componentOptions.Ctor就是指向 Vue.extend 的返回值Sub懊亡, 所以執(zhí)行new vnode.componentOptions.Ctor(options)接著執(zhí)行this._init(options)依啰,因為options._isComponenttrue,那么合并options的過程走到了initInternalComponent(vm, options)邏輯店枣。先來看一下它的代碼實現(xiàn)速警,在src/core/instance/init.js中:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

initInternalComponent方法首先執(zhí)行const opts = vm.$options = Object.create(vm.constructor.options),這里的vm.constructor就是子組件的構(gòu)造函數(shù)Sub鸯两,相當(dāng)于vm.$options = Object.create(Sub.options)闷旧。

接著又把實例化子組件傳入的子組件父VNode實例parentVnode、子組件的父Vue實例parent保存到vm.$options中钧唐,另外還保留了parentVnode配置中的如propsData等其它的屬性鸠匀。

這么看來,initInternalComponent只是做了簡單一層對象賦值逾柿,并不涉及到遞歸缀棍、合并策略等復(fù)雜邏輯。

因此机错,在我們當(dāng)前這個case下爬范,執(zhí)行完如下合并后:

initInternalComponent(vm, options)

vm.$options的值差不多是如下這樣:

vm.$options = {
  parent: Vue /*父Vue實例*/,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode /*父VNode實例*/,
  _renderChildren:undefined,
  __proto__: {
    components: { },
    directives: { },
    filters: { },
    _base: function Vue(options) {
        //...
    },
    _Ctor: {},
    created: [
      function created() {
        console.log('parent created')
      }, function created() {
        console.log('child created')
      }
    ],
    mounted: [
      function mounted() {
        console.log('child mounted')
      }
    ],
    data() {
       return {
         msg: 'Hello Vue'
       }
    },
    template: '<div>{{msg}}</div>'
  }
}

總結(jié)

那么至此,Vue初始化階段對于options的合并過程就介紹完了弱匪,我們需要知道對于options的合并有2種方式青瀑,子組件初始化過程通過initInternalComponent方式要比外部初始化Vue通過mergeOptions的過程要快,合并完的結(jié)果保留在vm.$options中萧诫。

縱觀一些庫斥难、框架的設(shè)計幾乎都是類似的,自身定義了一些默認(rèn)配置帘饶,同時又可以在初始化階段傳入一些定義配置哑诊,然后去merge默認(rèn)配置,來達(dá)到定制化不同需求的目的及刻。只不過在Vue的場景下镀裤,會對merge的過程做一些精細(xì)化控制竞阐,雖然我們在開發(fā)自己的JSSDK的時候并沒有Vue這么復(fù)雜,但這個設(shè)計思想是值得我們借鑒的暑劝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末骆莹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子担猛,更是在濱河造成了極大的恐慌幕垦,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件傅联,死亡現(xiàn)場離奇詭異先改,居然都是意外死亡,警方通過查閱死者的電腦和手機纺且,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門盏道,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人载碌,你說我怎么就攤上這事猜嘱。” “怎么了嫁艇?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵朗伶,是天一觀的道長。 經(jīng)常有香客問我步咪,道長论皆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任猾漫,我火速辦了婚禮点晴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘悯周。我一直安慰自己粒督,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布禽翼。 她就那樣靜靜地躺著屠橄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪闰挡。 梳的紋絲不亂的頭發(fā)上锐墙,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機與錄音长酗,去河邊找鬼溪北。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的刻盐。 我是一名探鬼主播掏膏,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼劳翰,長吁一口氣:“原來是場噩夢啊……” “哼敦锌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起佳簸,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤乙墙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后生均,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體听想,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年马胧,在試婚紗的時候發(fā)現(xiàn)自己被綠了汉买。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡佩脊,死狀恐怖蛙粘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情威彰,我是刑警寧澤出牧,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站歇盼,受9級特大地震影響舔痕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜豹缀,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一伯复、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧邢笙,春花似錦啸如、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至筐骇,卻和暖如春债鸡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背铛纬。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工厌均, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人告唆。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓棺弊,卻偏偏與公主長得像晶密,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子模她,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容