Vue2.0源碼閱讀筆記(一):選項合併

  Vue本質是上來講是一個函數,在其經過new關鍵字構造調用時,會完成一系列初始化過程。經過Vue框架進行開發,基本上是經過向Vue函數中傳入不一樣的參數選項來完成的。參數選項每每須要加以合併,主要有兩種狀況:html

一、Vue函數自己擁有一些靜態屬性,在實例化時開發者會傳入同名的屬性。
二、在使用繼承的方式使用Vue時,須要將父類和子類上同名屬性加以合併。
vue

  Vue函數定義在 /src/core/instance/index.js中。
ios

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
複製代碼

  在Vue實例化時會將選項集 options 傳入到實例原型上的 _init 方法中加以初始化。 initMixin 函數的做用就是向Vue實例的原型對象上添加 _init 方法, initMixin 函數在 /src/core/instance/init.js 中定義。
  在 _init 函數中,會對傳入的選項集進行合併處理。
web

// merge options
if (options && options._isComponent) {
    initInternalComponent(vm, options)
} else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
}
複製代碼

  在開發過程當中基本不會傳入 _isComponent 選項,所以在實例化時走 else 分支。經過 mergeOptions 函數來返回合併處理以後的選項並將其賦值給實例的 $options 屬性。 mergeOptions 函數接收三個參數,其中第一個參數是將生成實例的構造函數傳入 resolveConstructorOptions 函數中處理以後的返回值。
npm

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}
複製代碼

  resolveConstructorOptions 函數的參數爲實例的構造函數,在構造函數的沒有父類時,簡單的返回構造函數的 options 屬性。反之,則走 if 分支,合併處理構造函數及其父類的 options 屬性,如若構造函數的父類仍存在父類則遞歸調用該方法,最終返回惟一的 options 屬性。在研究實例化合並選項時,爲行文方便,將該函數返回的值統一稱爲選項合併的父選項集合,實例化時傳入的選項集合稱爲子選項集合
json

1、Vue構造函數的靜態屬性options

  在合併選項時,在沒有繼承關係存在的狀況,傳入的第一個參數爲Vue構造函數上的靜態屬性 options ,那麼這個靜態屬性到底包含什麼呢?爲了弄清楚這個問題,首先要搞清楚運行 npm run dev 命令來生成 /dist/vue.js 文件的過程當中發生了什麼。
  在 package.json 文件中 scripts 對象中有:api

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
複製代碼

  在使用rollup打包時,依據 scripts/config.js 中的配置,並將 web-full-dev 做爲環境變量TARGET的值。
數組

// Runtime+compiler development build (Browser)
'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
},
複製代碼

  上述文件路徑是在 scripts/alias.js 文件中配置過別名的。由此可知,執行 npm run dev 命令時,入口文件爲 src/platforms/web/entry-runtime-with-compiler.js ,生成符合 umd 規範的 vue.js 文件。依照該入口文件對Vue函數的引用,按圖索驥,逐步找到Vue構造函數所在的文件。以下圖所示:
瀏覽器

  Vue構造函數定義在 /src/core/instance/index.js中。在該js文件中,經過各類Mixin向 Vue.prototype 上掛載一些屬性和方法。以後在 /src/core/index.js 中,經過 initGlobalAPI 函數向Vue構造函數上添加靜態屬性和方法。
框架

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)
複製代碼

  在initGlobalAPI 函數中有向Vue構造函數中添加 options 屬性的定義。

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 變成這樣:

Vue.options = {
	components: {
		KeepAlive
	},
	directives: Object.create(null),
	filters: Object.create(null),
  _base: Vue
}
複製代碼

  在 /src/platforms/web/runtime/index.js 中,經過以下代碼向 Vue.options 屬性上添加平臺化指令以及內置組件。

import platformDirectives from './directives/index'
import platformComponents from './components/index'

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
複製代碼

  最終 Vue.options 屬性內容以下所示:

Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
     },
    filters: Object.create(null),
    _base: Vue
}
複製代碼

2、選項合併函數mergeOptions

  合併選項的函數 mergeOptions/src/core/util/options.js 中定義。

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)

  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, 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
}
複製代碼

一、組件命名規則

  合併選項時,在非生產環境下首先檢測聲明的組件名稱是否合乎標準:

if (process.env.NODE_ENV !== 'production') {
  checkComponents(child)
}
複製代碼

   checkComponents 函數是 對子選項集合components 屬性中每一個屬性使用 validateComponentName 函數進行命名有效性檢測。

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

  validateComponentName 函數定義了組件命名的規則:

export function validateComponentName (name: string) {
  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'can only contain alphanumeric characters and the hyphen, ' +
      'and must start with a letter.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}
複製代碼

  由上述代碼可知,有效性命名規則有兩條:

一、組件名稱可使用字母、數字、符號 _、符號 - ,且必須以字母爲開頭。
二、組件名稱不能是Vue內置標籤 slotcomponent;不能是 html內置標籤;不能使用部分SVG標籤。

二、選項規範化

  傳入Vue的選項形式每每有多種,這給開發者提供了便利。在Vue內部合併選項時卻要把各類形式進行標準化,最終轉化成一種形式加以合併。

normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
複製代碼

  上述三條函數調用分別標準化選項 propsinjectdirectives

(一)、props選項的標準化

  props 選項有兩種形式:數組、對象,最終都會轉化成對象的形式。
  若是props 選項是數組,則數組中的值必須都爲字符串。若是字符串擁有連字符則轉成駝峯命名的形式。好比:

props: ['propOne', 'prop-two']
複製代碼

  該props將被規範成:

props: {
  propOne:{
    type: null
  },
  propTwo:{
    type: null
  }
}
複製代碼

  若是props 選項是對象,其屬性有兩種形式:字符串、對象。屬性名有連字符則轉成駝峯命名的形式。若是屬性是對象,則不變;若是屬性是字符串則轉變成對象,屬性值變成新對象的 type 屬性。好比:

props: {
  propOne: Number,
  "prop-two": Object,
  propThree: {
    type: String,
    default: ''
  }
}
複製代碼

  該props將被規範成:

props: {
  propOne: {
    type: Number
  },
  propTwo: {
    type: Object
  },
  propThree: {
    type: String,
    default: ''
  }
}
複製代碼

  props對象的屬性值爲對象時,該對象的屬性值有效的有四種:

一、type:基礎的類型檢查。
二、required: 是否爲必須傳入的屬性。
三、default:默認值。
四、validator:自定義驗證函數。

(二)、inject選項的標準化

  inject 選項有兩種形式:數組、對象,最終都會轉化成對象的形式。
  若是inject 選項是數組,則轉化爲對象,對象的屬性名爲數組的值,屬性的值爲僅擁有 from 屬性的對象, from 屬性的值爲與數組對應的值相同。好比:

inject: ['test']
複製代碼

  該 inject 將被規範成:

inject: {
  test: {
    from: 'test'
  }
}
複製代碼

  若是inject 選項是對象,其屬性有三種形式:字符串、symbol、對象。若是是對象,則添加屬性 from ,其值與屬性名相等。若是是字符串或者symbol,則轉化爲對象,對象擁有屬性 from ,其值等於該字符串或symbol。好比:

inject: {
  a: 'value1',
  b: {
    default: 'value2'
  }
}
複製代碼

  該 inject 將被規範成:

inject: {
  a: {
    from: 'value1'
  },
  b: {
    from: 'b',
    default: 'value2'
  }
}
複製代碼
(三)、directives選項的標準化

  自定義指令選項 directives 只接受對象類型。通常具體的自定義指令是一個對象。 directives 選項的寫法較爲統一,那麼爲何還會有這個規範化的步驟呢?那是由於具體的自定義指令對象的屬性通常是各個鉤子函數。可是Vue提供了一種簡寫的形式:在 bindupdate 時觸發相同行爲,而不關心其它的鉤子時,能夠直接定義自定義指令爲一個函數,而不是對象。
  Vue內部合併 directives 選項時,要將這種函數簡寫,轉化成對象的形式。以下:

directive:{
  'color'function (el, binding) {
    el.style.backgroundColor = binding.value
  })
}
複製代碼

  該 directive 將被規範成:

directive:{
  'color':{
    bind:function (el, binding) {
      el.style.backgroundColor = binding.value
    }),
    update: function (el, binding) {
      el.style.backgroundColor = binding.value
    })
  }
}
複製代碼

三、選項extends、mixins的處理

  mixins 選項接受一個混入對象的數組。這些混入實例對象能夠像正常的實例對象同樣包含選項。以下所示:

var mixin = {
  created: function () { console.log(1) }
}
var vm = new Vue({
  created: function () { console.log(2) },
  mixins: [mixin]
})
// => 1
// => 2
複製代碼

  extends 選項容許聲明擴展另外一個組件,能夠是一個簡單的選項對象或構造函數。以下所示:

var CompA = { ... }

// 在沒有調用 `Vue.extend` 時候繼承 CompA
var CompB = {
  extends: CompA,
  ...
}
複製代碼

  Vue內部在處理選項extends或mixins時,會先經過遞歸調用 mergeOptions 函數,將extends對象或mixins數組中的對象做爲子選項集合父選項集合中合併。這就是選項extends和mixins中的內容與並列的其餘選項有衝突時的合併規則的依據。

四、使用策略模式合併選項

  選項的數量比較多,合併規則也不盡相同。Vue內部採用策略模式來合併選項。各類策略方法mergeOptions 函數外實現,環境對象strats 對象。
  strats 對象是在 /src/core/config.js 文件中的 optionMergeStrategies 對象的基礎上,進行一系列策略函數添加而獲得的對象。環境對象接受請求,來決定委託哪個策略來處理。這也是用戶能夠經過全局配置 optionMergeStrategies 來自定義選項合併規則的緣由。

3、選項合併策略

  環境對象 strats 上擁有的屬性以及屬性對應的函數以下圖所示:

一、選項el、propsData以及strats對象不包括的屬性對象的合併策略

  選項 elpropsData以及圖中沒有的選項都採用默認策略函數 defaultStrat 進行合併。

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}
複製代碼

  默認策略比較簡單:若是子選項集合中有相應的選項,則直接使用子選項的值;不然使用父選項的值。

二、選項data、provide的合併策略

  選項 dataprovide 的策略函數雖然都是 mergeDataOrFn,可是選項 provide 合併時是向 mergeDataOrFn函數中傳入三個參數:父選項、子選項、實例。選項 data 的合併分兩種狀況:經過Vue.extends()處理子組件選項時、正常實例化時。前一種狀況沒有實例 vm,向 mergeDataOrFn函數傳入兩個參數:父選項和子選項;後一種狀況則跟選項 provide 傳入的參數同樣。
  mergeDataOrFn函數代碼以下所示,只有在合併 data 選項,且是經過Vue.extends()處理子組件選項時,纔會走 if 分支。處理正常的實例化選項 dataprovide 時,都是走 else 分支。

export function mergeDataOrFn (parentVal: any,childVal: any,vm?: Component): ?Function {
  if (!vm) {
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn () {
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}
複製代碼

  在實例 vm 不存在的狀況下,有三種狀況:

一、子選項不存在,則返回父選項。
二、父選項不存在,則返回子選項。
三、若是父子選項都存在,則返回函數 mergedDataFn

  函數 mergedDataFn將分別提取父子選項函數的返回值,將該純對象傳入 mergeData 函數,最終返回 mergeData 函數的返回值。若是父子選項都不存在,則不會走到這個函數中,所以不加以考慮。
  爲何前面說在 if 分支中的父子選項都爲函數呢?由於走該分支,只能是經過Vue.extends()處理子組件 data 選項時。而當一個組件被定義時, data 必須聲明爲返回一個純對象的函數,這樣能防止多個組件實例共享一個數據對象。定義組件時, data 選項是一個純對象,在非生產環境下,Vue會有錯誤警告。
  在 else 分支中,返回函數 mergedInstanceDataFn ,在該函數中,若是子選項存在則分別提取父子選項函數的返回值,將該純對象傳入 mergeData 函數;不然,將返回純對象形式的父選項。
  在該場景下 mergeData 函數的做用是將父選項對象中有而子選項對象沒有的屬性,經過 set 方法將該屬性添加到子選項對象上並改爲響應式數據屬性。
  分析完各類狀況,發現選項 dataprovide 策略函數是一個高階函數,返回值是一個返回合併對象的函數。這是爲何呢?這個緣由前面說過,是爲了保證各組件實例有惟一的數據副本,防止組件實例共享同一數據對象。
  選項 dataprovide選項合併處理的結果是一個函數,並且該函數在合併階段並無執行,而是在初始化的時候執行的,這又是爲何呢?在 /src/core/instance/init.js 進行初始化時有以下代碼:

initInjections(vm)
initState(vm)
initProvide(vm) 
複製代碼

   函數 initState 有以下代碼:

if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
  initData(vm)
} else {
  observe(vm._data = {}, true /* asRootData */)
}
複製代碼

  由上述代碼可知: dataprovide 的初始化是在 injectprops 以後進行的。在初始化時執行合併函數的返回函數,可以使用 injectprops 的值來初始化 dataprovide 的值。

三、生命週期鉤子選項的合併策略

  生命週期鉤子選項使用 mergeHook 函數合併。

function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}
複製代碼

  Vue官方API文檔上說生命週期鉤子選項只能是函數類型的,從這段源碼中能夠看出,開發者能夠傳入函數數組類型的生命週期選項。由於能夠將數組中各函數加以合併,所以傳入函數數組實用性不大。
  還有一個點比較有意思:若是父選項存在,一定是一個數組。雖然生命週期選項能夠是數組,可是開發者通常傳入的都是函數,那麼爲何這裏父選項一定是數組呢?
  這是由於生命週期父選項存在的狀況有兩種:Vue.extends()、Mixins。在上面 選項extends、mixins的處理 部分已經說過,處理這兩種狀況時,會將其中的選項做爲子選項遞歸調用 mergeOptions 函數進行合併。也就說聲明週期父選項都是通過 mergeHook 函數處理以後的返回值,因此若是生命週期父選項存在,一定是函數數組。
  函數 mergeHook 返回值若是存在,會將返回值傳入 dedupeHooks 函數進行處理,目的是爲了剔除選項合併數組中的重複值。

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
}
複製代碼

  生命週期鉤子數組按順序執行,所以先執行父選項中的鉤子函數,後執行子選項中的鉤子函數。

四、資源選項(components、directives、filters)的合併策略

  組件 components ,指令 directives ,過濾器 filters ,被稱爲資源,由於這些均可以做爲第三方應用來提供。
  資源選項經過 mergeAssets 函數進行合併,邏輯比較簡單。

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
  }
}
複製代碼

  先定義合併後選項爲空對象。若是父選項存在,則以父選項爲原型,不然沒有原型。若是子選項爲純對象,則將子選項上的各屬性複製到合併後的選項對象上。
  前面說過 Vue.options 屬性內容以下所示:

Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
     },
    filters: Object.create(null),
    _base: Vue
}
複製代碼

  KeepAliveTransitionTransitionGroup 爲內置組件,modelshow 爲內置指令,不用註冊就能夠直接使用。

五、選項watch的合併策略

  選項 watch 是一個對象,可是對象的屬性卻能夠是多種形式:字符串、函數、對象以及數組。

// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
  assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
  let parent = ret[key]
  const child = childVal[key]
  if (parent && !Array.isArray(parent)) {
    parent = [parent]
  }
  ret[key] = parent
    ? parent.concat(child)
    : Array.isArray(child) ? child : [child]
}
return ret
複製代碼

  由於火狐瀏覽器 Object 原型對象上擁有watch屬性,所以在合併前須要檢查選項集合 options 上是否有開發者添加的 watch屬性,若是沒有,不作合併處理。
  若是子選項不存在,則返回以父選項爲原型的空對象。
  若是父選項不存在,先檢查子選項是否爲純對象,再返回子選項。
  若是父子選項都存在,則先將父選項各屬性複製到合併對象上,而後檢查子選項上的各個屬性。
  在子選項上而不在父選項上的屬性,是數組則直接添加到合併對象上。若是不是數組,則填充到新數組中,將該數組添加到合併對象上。
  父子選項上都存在的屬性,將父選項上該屬性變成數組格式,再向數組中添加子選項上的對應屬性。

六、選項props、methods、inject、computed的合併策略

  選項 propsmethodsinjectcomputed 採用相同的合併策略。選項 methodscomputed 傳入時只接受對象形式,而選項 propsinject 通過前面的標準化以後也是純對象的形式。

if (childVal && process.env.NODE_ENV !== 'production') {
  assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
複製代碼

  首先檢查子選項是否爲純對象,若是不是純對象,在非生產環境報錯。
  若是父選項不存在,則直接返回子選項。
  若是父選項存在,先建立一個沒有原型的空對象做爲合併選項對象,將父選項上的各屬性複製到合併選項對象上。若是子選項存在,則將子選項對象上的所有屬性複製到合併對象上,所以父子選項上有相同屬性則以取子選項上該屬性的值。最後返回合併選項對象。

七、選項合併策略總結

一、elpropsData 以及採用默認策略合併的選項:有子選項就選用子選項的值,不然選用父選項的值。
二、選項 dataprovide :返回一個函數,該函數的返回值是合併以後的對象。以子選項對象爲基礎,若是存在子選項上沒有而父選項上有的屬性,則將該屬性轉變成響應式屬性後加入到子選項對象上。
三、生命週期鉤子選項:合併成函數數組,父選項排在子選項以前,按順序執行。
四、資源選項(components、directives、filters):定義一個沒有原型的空合併對象,子選項存在,則將子選項上的屬性複製到合併對象;父選項存在,則以父選項對象爲原型。
五、選項 watch :子選項不存在,則返回以父選項爲原型的空對象;父選項不存在,返回子選項;父子選項都存在,則和生命週期合併策略相似,以子選項屬性爲主,轉化成數組形式,父選項也存在該屬性,則推入數組中。
六、選項props、methods、inject、computed:將父子選項上的屬性添加到一個沒有原型的空對象上,父子選項上有相同屬性的則取子選項的值。
七、子選項中 extendsmixins :將這兩項的值做爲子選項與父選項合併,合併規則依照上述規則合併,最後再分項與子選項的同名屬性按上述規則合併。

4、總結

  在合併選項前,先對選項 injectpropsdirectives 進行標準化處理。而後將子選項集合中的extends、mixins做爲子選項遞歸調用合併函數與父選項合併。最後使用策略模式合併各個選項。
如需轉載,煩請註明出處:www.cnblogs.com/lidengfeng/…

相關文章
相關標籤/搜索