Vue源碼解析: 豐富的選項(xiàng)合并策略

選項(xiàng)合并.png

簡(jiǎn)介

Vue的初始化過(guò)程中,最開(kāi)始的階段就是選項(xiàng)合并階段。它通過(guò)調(diào)用mergeOptions函數(shù)將兩個(gè)選項(xiàng)配置合并成一個(gè)選項(xiàng)配置。這里的選項(xiàng)options的形式實(shí)際上就是我們平時(shí)開(kāi)發(fā)時(shí)在Vue中寫(xiě)的對(duì)象配置,形式如下:

{ 
  components: {}, 
  filters: {},
  data() { return {} }, 
  computed: {}, 
  created: {}, 
  methods: {},
  ... 
}

因此,選項(xiàng)合并實(shí)際可以簡(jiǎn)單的看作是兩個(gè)上面的對(duì)象合并成一個(gè)對(duì)象。

由于mergeOptions是實(shí)現(xiàn)實(shí)例化(new Vue(options))、繼承(Vue.extend)和混入(Vue.mixin)三大功能的核心函數(shù),所以分析它的實(shí)現(xiàn)是理解Vue實(shí)例化過(guò)程和繼承的必經(jīng)之路。 下面我們將從以下幾個(gè)方面來(lái)全面了解Vue中的選項(xiàng)合并:

  1. 實(shí)例化過(guò)程中的選項(xiàng),了解我們需要合并的選項(xiàng)是怎樣的。

  2. mergeOptions的實(shí)現(xiàn),了解各個(gè)合并策略。

  3. 繼承(Vue.extendextends:{})的選項(xiàng)合并。

  4. 混入(Vue.mixinmixins:[])的選項(xiàng)合并。

  5. 為什么實(shí)例化過(guò)程中有時(shí)用initInternalComponent而不是mergeOptions。

實(shí)例化過(guò)程中的選項(xiàng)

從上一節(jié)的學(xué)習(xí)我們知道,Vue的實(shí)例化過(guò)程調(diào)用的是core/instance/init.js文件中的_init方法。

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
  
    ...
  
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  
    ...
  }

實(shí)例化的過(guò)程中第一個(gè)重要的處理就是選項(xiàng)的合并,這里第二個(gè)參數(shù)比較容易理解,就是我們平時(shí)寫(xiě)的Vue的配置項(xiàng)。第一個(gè)參數(shù)則是通過(guò)resolveConstructorOptions(vm.constructor)生成,找到resolveConstructorOptions方法,代碼如下

// * 返回構(gòu)造函數(shù)的 options
export function resolveConstructorOptions (Ctor: Class<Component>) {
  // * 如果不是繼承,options 就是原構(gòu)造函數(shù)的 options
  // * 如果是繼承時(shí),options 為合并 superOptions 和 extendOptions 的 options
  // * 此外,這里的 options 還包含了全局注冊(cè)的 組件/指令/過(guò)濾器
  let options = Ctor.options
  // * Ctor.super 存在說(shuō)明是調(diào)用了 extend 方法進(jìn)行繼承生成的構(gòu)造函數(shù)
  // * 詳見(jiàn) /src/core/global-api/extend.js 文件
  // * - superOptions 是父類 options
  // * - extendOptions 是當(dāng)前類傳入的 options (如果與 sealedOptions不同,需要合并)
  // * - options = mergeOptions(superOptions, extendOptions)
  // * - sealedOptions 保存的是當(dāng)前類繼承時(shí) 合并后的 options(是extend的時(shí)候賦值的)
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    // * 如果 superOptions 變動(dòng)了,需要處理新的 options
    if (superOptions !== cachedSuperOptions) {
      Ctor.superOptions = superOptions
      // * sealedOptions 是 seal 的時(shí)候賦值的,
      // * 這里的變動(dòng)可能是 options 在 extend 后繼續(xù)被賦值
      // * 復(fù)現(xiàn):https://jsfiddle.net/vvxLyLvq/2/
      // * 所以需要找出變動(dòng)了的屬性,然后更新到 extendOptions 上
      // * 這里的 extend 只是對(duì)象的合并
      const modifiedOptions = resolveModifiedOptions(Ctor)
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // * 由于 options 變化了,重新合并一次
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        // * 將自身的構(gòu)造函數(shù)也存到了 components 對(duì)象中
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

這里傳入的是當(dāng)前構(gòu)造函數(shù),那么Ctor.options指的是Vue構(gòu)造函數(shù)的options,還記得上一節(jié)中提到的Vue.options嗎?這里將options的內(nèi)容從上一節(jié)中復(fù)制過(guò)來(lái),代碼如下:

// Vue.options 內(nèi)容
{
    components: {
    KeepAlive,
    // 新增 platformComponents
    Transition,
    // 新增 platformComponents
    TransitionGroup
  },
  filters: {},
  directives: {
    // 新增 platformDirectives
    model,
    // 新增 platformDirectives
    show
  },
  _base: Vue
}

再看下一句Ctor.super的判斷,super這個(gè)字段是在core/global-api/extend.js文件中的extend方法調(diào)用時(shí)添加的。如果Ctor.super存在,說(shuō)明Ctor是通過(guò)繼承而來(lái)的子構(gòu)造函數(shù)。但是,如果在extend后,我們又在父構(gòu)造函數(shù)的options上添加新的屬性,這個(gè)時(shí)候子構(gòu)造函數(shù)是無(wú)法繼承新的屬性的。因此,這里需要通過(guò)Ctor.super向上尋找,找出所有父構(gòu)造函數(shù)更新的options屬性,并更新到子構(gòu)造函數(shù)上,這樣就能解決Vue.options被更改的問(wèn)題了。有興趣的話,可以看一下Vueissues#4976。

最后,經(jīng)過(guò)resolveConstructorOptions處理后,最終得到的同樣是一個(gè)Vue的配置選項(xiàng),下一步則是需要將這兩個(gè)配置選項(xiàng)進(jìn)行合并了。

mergeOptions的實(shí)現(xiàn)

選項(xiàng)校驗(yàn)和規(guī)范化

mergeOptions函數(shù)的定義是在/src/core/util/options.js文件當(dāng)中,部分代碼如下:

export function mergeOptions (
  parent: Object,// 選項(xiàng)
  child: Object, // 選項(xiàng)
  vm?: Component // Vue 實(shí)例
): Object {
  // * 1. 校驗(yàn)選項(xiàng)中的 components 里的名稱是否合法。
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }
  
  // * ['type1'], { type2: { type: String, default: '' } } 兩種形式
  // * 2. 都轉(zhuǎn)換成后一種形式
  normalizeProps(child, vm)
  
  // * ['injectKey1'], { injectKey2: { from: 'xxx', default: 'yyy' } } 兩種形式
  // * 3. 都轉(zhuǎn)換成后一種形式
  normalizeInject(child, vm)
  
  // * directive 有兩種形式 function() {} 或者是 { bind, update, ... }
  // * 4. 都轉(zhuǎn)換成后一種形式
  normalizeDirectives(child)

  // * 合并策略
    ...
}

在正式合并之前,會(huì)優(yōu)先校驗(yàn)components里的組件名稱是否合法,如果不合法會(huì)進(jìn)行提示。

function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}

// 校驗(yàn)組件名稱是否合法
export function validateComponentName (name: string) {
  // 1. 判斷組件名是否合法,如數(shù)字開(kāi)頭的則不合法
  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.'
    )
  }
  // 2. 判斷組件是否是自身定義的組件名,如 slot 等。
  // 3. 判斷組件是否是 html 中的標(biāo)簽名,如 div 等
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

除此之外, Vue還做了以下幾點(diǎn)處理。通過(guò)選項(xiàng)形式的轉(zhuǎn)換,將多種寫(xiě)法的選項(xiàng)轉(zhuǎn)換成統(tǒng)一形式:

  1. normalizeProps方法:處理props,將數(shù)組形式定義的props轉(zhuǎn)換成對(duì)象形式。

  2. normalizeInject方法:處理inject,將數(shù)組形式定義的inject轉(zhuǎn)換成對(duì)象形式。

  3. normalizeDirectives方法:處理directives,將指令數(shù)組里函數(shù)形式定義的directive轉(zhuǎn)換成對(duì)象形式。

處理前和處理后對(duì)比結(jié)果如下:

// ===> 處理前 <===
{
    props: ['user-name'],
  inject: ['id'],
  directives: [function add() {}]
}

// ===> 處理后 <====
{
  // 對(duì)象形式
    props: {
    userName: { // 轉(zhuǎn)換成駝峰命名
        type: null
    }
  },
  // 對(duì)象形式
  inject: {
    id: {
        from: 'id'
    }
  },
  directives: [{
    // 對(duì)象形式
    bind: function add() {}
    update: function add() {}
  }]
}

在校驗(yàn)完成之后,接下里就是正式的合并流程了,Vue針對(duì)每個(gè)規(guī)定的配置選項(xiàng)都有定義好的合并策略,例如data,component,mounted,methods等。如果Vue父子選項(xiàng)配置具有相應(yīng)的選項(xiàng),那么直接按照相應(yīng)的合并策略進(jìn)行合并。合并的入口如下:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
    ...
  // * 合并策略
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    // 根據(jù) key 獲取相應(yīng)的合并策略
    const strat = strats[key] || defaultStrat
    // 用相應(yīng)的合并策略進(jìn)行合并
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

以上通過(guò)兩個(gè)for循環(huán),遍歷parentchildkeykey這里指的是data/methods/created等),然后依次調(diào)用mergeField方法。mergeField則是通過(guò)keystrats中找到對(duì)應(yīng)的合并策略,然后用該合并策略進(jìn)行相應(yīng)合并。如果找不到合并策略,則使用默認(rèn)合并策略defaultStrat。

這里的strats已經(jīng)在該文件中定義,現(xiàn)在重點(diǎn)來(lái)看一下Vuestrats是如何定義合并策略的。

data合并

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )
      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

可以看出當(dāng)vm不存在時(shí),如果childValdata不為函數(shù)形式,那么在非開(kāi)發(fā)環(huán)境下就會(huì)報(bào)錯(cuò),這也是為什么我們平時(shí)在寫(xiě)組件data時(shí)需要寫(xiě)成函數(shù)形式的原因。

但是這里的vm在什么情況下不存在呢?我們可以全局搜索一下mergeOptions(,看看哪些位置調(diào)用了該方法:

img

可以看出,在extend,mixin中,由于處理構(gòu)造函數(shù)階段時(shí),是沒(méi)有實(shí)例的,所以也就不會(huì)傳vm。這里我們主要討論extend

接下來(lái)我們?nèi)炙阉饕幌?code>extend在哪些地方被調(diào)用了。

img

可以看到,extend方法主要在兩個(gè)位置被調(diào)用。

第一個(gè)位置在 src/core/global-api/assets.js文件

ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
        ...
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // * 通過(guò)繼承,返回新的構(gòu)造函數(shù)(相當(dāng)于 子組件 的構(gòu)造函數(shù))
          definition = this.options._base.extend(definition)
        }
            ...
        // * component / directive / filter 
        // * 將注冊(cè)的內(nèi)容全部添加到 Vue 構(gòu)造函數(shù)的 options 上
        this.options[type + 's'][id] = definition
        return definition
    }
  })

通過(guò)遍歷ASSET_TYPES,在Vue構(gòu)造函數(shù)上添加了component靜態(tài)屬性,即當(dāng)我們使用Vue.component的時(shí)候,實(shí)際上會(huì)執(zhí)行這里的this.options._base.extend(definition),即調(diào)用了extend方法來(lái)將傳入的組件選項(xiàng)合并后返回新的構(gòu)造函數(shù)。

第二個(gè)位置在src/core/vdom/create-component.js文件

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
    ...
  // * 這里的 Ctor 有幾種形式
  // * 1. 全局形式定義的 component,那么 Ctor 是構(gòu)造函數(shù)形式
  // * 2. 局部定義的 component,那么是 對(duì)象形式。會(huì)對(duì)對(duì)象形式進(jìn)行 extend 處理
  const baseCtor = context.$options._base

  // 對(duì)象形式。會(huì)對(duì)對(duì)象形式進(jìn)行 extend 處理
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
    ...
  return vnode
}

這段代碼在patch階段執(zhí)行的(僅做了解)。傳入的Ctor一種情況是全局定義的組件,此時(shí)Ctor通過(guò)extend創(chuàng)建,傳入的是構(gòu)造函數(shù)形式。另外一種情況則是局部注冊(cè)的組件,傳入的是選項(xiàng)配置形式,此時(shí)會(huì)執(zhí)行baseCtor.extend(Ctor),同樣會(huì)通過(guò)extend來(lái)創(chuàng)建構(gòu)造函數(shù)。

因此,無(wú)論是全局注冊(cè)的組件還是局部組件,最終都會(huì)調(diào)用extend方法,而extend方法在合并選項(xiàng)的時(shí)候會(huì)校驗(yàn)傳入的data是否是函數(shù)形式,這也就是為什么在定義組件時(shí)data必須是以函數(shù)形式定義。

現(xiàn)在我們已經(jīng)知道了組件data定義時(shí)必須使用函數(shù),但是為什么需要用函數(shù)定義呢,用普通的對(duì)象形式不行嗎?答案是不行。我們知道,Vue組件是可以復(fù)用的,也就是同一個(gè)extend出來(lái)的Vue構(gòu)造函數(shù)(也就是組件的構(gòu)造函數(shù)),可以被多次實(shí)例化,但是數(shù)據(jù)是不應(yīng)該被共享的。

如果data是對(duì)象形式,那么多個(gè)實(shí)例引用都是指向的同一個(gè)data對(duì)象。那么每次更改其中一個(gè)實(shí)例的data里的屬性時(shí),其他實(shí)例也會(huì)跟隨著改變。

如果data是函數(shù)形式,每次實(shí)例化時(shí)都會(huì)執(zhí)行該函數(shù),并且返回一個(gè)全新的data對(duì)象,這樣多個(gè)實(shí)例之間就各自有一份獨(dú)立的data了,從而解決數(shù)據(jù)被共享的問(wèn)題。

好了,了解完了data在組件中為什么要為函數(shù)形式后,我們繼續(xù)看data的后續(xù)合并過(guò)程。mergeDataOrFn函數(shù)執(zhí)行時(shí)最終調(diào)用的都是mergeData函數(shù):

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // key 為 __ob__ 則跳過(guò) 
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    // * 自身不存在這個(gè)key,那么使用將 from 的 key 和 value 添加到 to 上
    // * 如果 to 原本是響應(yīng)式的,那么新增的 key 值也需要是響應(yīng)式的
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      // * 如果都是對(duì)象,繼續(xù)合并
      mergeData(toVal, fromVal)
    }
  }
  return to
}

這里的mergeData比較簡(jiǎn)單,實(shí)際上就是遞歸將兩個(gè)對(duì)象合并。需要注意的是,在合并的過(guò)程中,如果data是響應(yīng)式的,那么合并后添加的屬性也需要是響應(yīng)式的。

生命周期合并

生命周期的鉤子是在src/shared/constant.js中定義

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

mergeHook是生命周期鉤子合并的策略,其核心是將父選項(xiàng)和子選項(xiàng)的對(duì)應(yīng)生命周期合并成數(shù)組形式,如果存在相同的生命周期執(zhí)行函數(shù),那么會(huì)進(jìn)行去重處理。

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
        // 都存在時(shí),拼接數(shù)組
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
                // parent 不存在時(shí)
        ? childVal
        : [childVal]
        // child 不存在,使用 parent
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

// hooks 去重
function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}

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

下面結(jié)合具體例子看看實(shí)際合并的結(jié)果:

    const extend = {
      created() {
        console.log('extends')
      }
    }
    const mixins = {
      created() {
        console.log('mixins')
      }
    }
    
    // 父構(gòu)造函數(shù)
    const Parent = Vue.extend({
      created() {
        console.log('parent created')
      },
      mixins: [mixins],
      extends: extend,
    })

    // 子構(gòu)造函數(shù)
    const Child = Parent.extend({
      created() {
        console.log('child')
      },
      mixins: [mixins],
      extends: {
        created() {
          console.log('child extends')
        }
      }
    })

    new Child()
    // extends
        // mixins
        // parent created
        // child extends
        // child

由于mixins里的created在合并時(shí)去重了,所以只會(huì)打印一遍mixins。另外可以看出,生命周期在執(zhí)行時(shí),parentextends/mixins里的生命周期都是優(yōu)先于child生命周期執(zhí)行的。

components/filters/directives合并

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

Object.create()方法創(chuàng)建一個(gè)新對(duì)象,使用現(xiàn)有的對(duì)象來(lái)提供新創(chuàng)建的對(duì)象的__proto__

這合并資源選項(xiàng)的時(shí)候,首先會(huì)創(chuàng)建一個(gè)原型指向父選項(xiàng)的空對(duì)象,再將子選項(xiàng)賦值給空對(duì)象。注意這里的父選項(xiàng)是通過(guò)原型鏈訪問(wèn),而子選項(xiàng)是直接添加到對(duì)象上的。例如:

Vue.component('test', {})
  const vm = new Vue({
    components: {
      test: 'test'
    }
  })
console.log('vm.$options ==> ', vm.$options);
// 合并后,父類的 options 通過(guò) __proto__ 訪問(wèn)
{ 
  components: {
    test: "test",
    __proto__: {
      KeepAlive: { ... },
      Transition: { ... },
      TransitionGroup: { ... },
      test: ...
    }
  },
  directives: {},
  filters: {},
  _base: ...
}

這里的__proto__指向的就是復(fù)習(xí)選項(xiàng)的components。

watch合并

watch的策略是:

  • 當(dāng)子選項(xiàng)不存在時(shí),使用父選項(xiàng);

  • 當(dāng)父選項(xiàng)不存在時(shí),使用子選項(xiàng);

  • 當(dāng)父選項(xiàng)和子選項(xiàng)都存在時(shí),如果他們具有相同的觀測(cè)字段,那么將其合并成數(shù)組形式。

strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  ...
  // 子類不存在,使用父類
  if (!childVal) return Object.create(parentVal || null)
  ...
  // 父類不存在,使用子類
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    // 如果父類存在,改寫(xiě)成數(shù)組形式
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    // 拼接父類和子類
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

props,methods,inject,computed合并

這一類的選項(xiàng)合并比較簡(jiǎn)單:

  • 當(dāng)父選項(xiàng)不存在時(shí),使用子選項(xiàng);

  • 當(dāng)子選項(xiàng)不存在時(shí),使用父選項(xiàng);

  • 當(dāng)兩者都存在時(shí),使用子選項(xiàng)覆蓋父選項(xiàng)。

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  // 創(chuàng)建空對(duì)象
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}

provide合并

使用的是mergeDataOrFn合并策略,同data合并。

總結(jié)

到這里我們就已經(jīng)對(duì)所有的合并策略都有所了解了??偨Y(jié)一下就是

  1. data,provide,props,methods,inject,computed,``components,filters,directives基本都是在父子選項(xiàng)同時(shí)存在的情況下,子覆蓋父。

  2. 生命周期在父子選項(xiàng)同時(shí)存在的情況下,會(huì)合并成數(shù)組形式,且去重。

  3. watch在父子選項(xiàng)同時(shí)存在的情況下,會(huì)合并成數(shù)組形式,不去重。

Vue.extend的實(shí)現(xiàn)

Vue.extend的定義是在core/gloabl-api/extend.js文件里面,主要用于通過(guò)選項(xiàng)配參數(shù)生成新的構(gòu)造函數(shù)。這里的參數(shù)extendOptions就是我們?cè)诙x組件時(shí)傳入的配置選項(xiàng)。

Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    // * 1. 查看配置選項(xiàng)中是否緩存有構(gòu)造函數(shù)
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    // * 2. 校驗(yàn)組件名稱
    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

    // * 3. 繼承
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    
    ...
  }

我們先看前半段代碼,前半段主要做了三件事:

  1. 檢驗(yàn)是否通過(guò)該選項(xiàng)配置生成過(guò)相應(yīng)構(gòu)造函數(shù),如果生成過(guò),那么直接使用生成的構(gòu)造函數(shù)即可。這里相當(dāng)于做了一層優(yōu)化。

  2. 校驗(yàn)組件名稱是否合法

  3. 通過(guò)原型實(shí)現(xiàn)繼承,生成新的構(gòu)造函數(shù)

接下來(lái)就是選項(xiàng)合并了

    // * 4. 合并選項(xiàng)
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )

參數(shù)Super.options就是Vue.options,前面已經(jīng)提過(guò)兩次,這里不再贅述了。這里合并后相當(dāng)于將Vue.options擴(kuò)充了,并將擴(kuò)充后的結(jié)果保存到Sub.options上(即新的構(gòu)造函數(shù)options上),所以在通過(guò)該構(gòu)造函數(shù)實(shí)例化的時(shí)候,擁有extendOptions配置的相關(guān)功能。

最后,會(huì)在新生成的構(gòu)造函數(shù)上添加一些靜態(tài)方法和屬性。注意這里的superOptioins/extendOptions/sealedOptioins都在resolveConstructorOptions方法尋找options中使用到。

    // * 5. 添加 super
    Sub['super'] = Super
            
      ...

    // * 6. 添加一些方法
    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    if (name) {
      Sub.options.components[name] = Sub
    }

    // * 7. 添加一些屬性
    // * 父選項(xiàng)
    Sub.superOptions = Super.options
    // * 傳入的配置選項(xiàng)
    Sub.extendOptions = extendOptions
    // * 合并后的配置相許那個(gè)
    Sub.sealedOptions = extend({}, Sub.options)

最后總結(jié)來(lái)講,Vue.extend方法實(shí)際上就是通過(guò)原型繼承,并將Vue.optionsextendOptions合并,從而實(shí)現(xiàn)一個(gè)新的構(gòu)造函數(shù)。

Vue.mixin的實(shí)現(xiàn)

Vue.mixin`方法的實(shí)現(xiàn)更是簡(jiǎn)單,打開(kāi)`core/global-api/mixin.js
Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }

不難看出,mixin實(shí)際上就是將兩個(gè)選項(xiàng)配置進(jìn)行合并。

結(jié)尾

在結(jié)尾之前,這里再拋出一個(gè)問(wèn)題,在Vue實(shí)例化過(guò)程中有一層判斷,當(dāng)_isComponenttrue時(shí),將不會(huì)進(jìn)行選項(xiàng)合并,這是為什么呢?

if (options && options._isComponent) {
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )

這是因?yàn)楫?dāng)_isComponenttrue時(shí),此時(shí)為組件渲染階段,options的內(nèi)容為

const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
}

此時(shí)并沒(méi)有需要選項(xiàng)合并的項(xiàng),所以也就沒(méi)有必要mergeOptions了。

至此,選項(xiàng)合并的實(shí)現(xiàn)與應(yīng)用我們已經(jīng)學(xué)完了,下一章節(jié)將會(huì)學(xué)習(xí)Vue的核心模塊之一:響應(yīng)式系統(tǒng)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容