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

白馬笑西風發表於2019-03-18

  Vue本質是上來說是一個函式,在其通過new關鍵字構造呼叫時,會完成一系列初始化過程。通過Vue框架進行開發,基本上是通過向Vue函式中傳入不同的引數選項來完成的。引數選項往往需要加以合併,主要有兩種情況:

1、Vue函式本身擁有一些靜態屬性,在例項化時開發者會傳入同名的屬性。
2、在使用繼承的方式使用Vue時,需要將父類和子類上同名屬性加以合併。

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

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 函式中,會對傳入的選項集進行合併處理。

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

  在開發過程中基本不會傳入 _isComponent 選項,因此在例項化時走 else 分支。通過 mergeOptions 函式來返回合併處理之後的選項並將其賦值給例項的 $options 屬性。 mergeOptions 函式接收三個引數,其中第一個引數是將生成例項的建構函式傳入 resolveConstructorOptions 函式中處理之後的返回值。

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 屬性。在研究例項化合並選項時,為行文方便,將該函式返回的值統一稱為選項合併的父選項集合,例項化時傳入的選項集合稱為子選項集合

一、Vue建構函式的靜態屬性options

  在合併選項時,在沒有繼承關係存在的情況,傳入的第一個引數為Vue建構函式上的靜態屬性 options ,那麼這個靜態屬性到底包含什麼呢?為了弄清楚這個問題,首先要搞清楚執行 npm run dev 命令來生成 /dist/vue.js 檔案的過程中發生了什麼。
  在 package.json 檔案中 scripts 物件中有:

"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建構函式所在的檔案。如下圖所示:

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

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

二、選項合併函式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
}
複製程式碼

1、元件命名規則

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

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
    )
  }
}
複製程式碼

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

1、元件名稱可以使用字母、數字、符號 _、符號 - ,且必須以字母為開頭。
2、元件名稱不能是Vue內建標籤 slotcomponent;不能是 html內建標籤;不能使用部分SVG標籤。

2、選項規範化

  傳入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物件的屬性值為物件時,該物件的屬性值有效的有四種:

1、type:基礎的型別檢查。
2、required: 是否為必須傳入的屬性。
3、default:預設值。
4、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
    })
  }
}
複製程式碼

3、選項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中的內容與並列的其他選項有衝突時的合併規則的依據。

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

  選項的數量比較多,合併規則也不盡相同。Vue內部採用策略模式來合併選項。各種策略方法mergeOptions 函式外實現,環境物件strats 物件。
  strats 物件是在 /src/core/config.js 檔案中的 optionMergeStrategies 物件的基礎上,進行一系列策略函式新增而得到的物件。環境物件接受請求,來決定委託哪一個策略來處理。這也是使用者可以通過全域性配置 optionMergeStrategies 來自定義選項合併規則的原因。

三、選項合併策略

  環境物件 strats 上擁有的屬性以及屬性對應的函式如下圖所示:

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

1、選項el、propsData以及strats物件不包括的屬性物件的合併策略

  選項 elpropsData以及圖中沒有的選項都採用預設策略函式 defaultStrat 進行合併。

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

  預設策略比較簡單:如果子選項集合中有相應的選項,則直接使用子選項的值;否則使用父選項的值。

2、選項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 不存在的情況下,有三種情況:

1、子選項不存在,則返回父選項。
2、父選項不存在,則返回子選項。
3、如果父子選項都存在,則返回函式 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 的值。

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

  生命週期鉤子選項使用 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
}
複製程式碼

  生命週期鉤子陣列按順序執行,因此先執行父選項中的鉤子函式,後執行子選項中的鉤子函式。

4、資源選項(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 為內建指令,不用註冊就可以直接使用。

5、選項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屬性,如果沒有,不做合併處理。
  如果子選項不存在,則返回以父選項為原型的空物件。
  如果父選項不存在,先檢查子選項是否為純物件,再返回子選項。
  如果父子選項都存在,則先將父選項各屬性複製到合併物件上,然後檢查子選項上的各個屬性。
  在子選項上而不在父選項上的屬性,是陣列則直接新增到合併物件上。如果不是陣列,則填充到新陣列中,將該陣列新增到合併物件上。
  父子選項上都存在的屬性,將父選項上該屬性變成陣列格式,再向陣列中新增子選項上的對應屬性。

6、選項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
複製程式碼

  首先檢查子選項是否為純物件,如果不是純物件,在非生產環境報錯。
  如果父選項不存在,則直接返回子選項。
  如果父選項存在,先建立一個沒有原型的空物件作為合併選項物件,將父選項上的各屬性複製到合併選項物件上。如果子選項存在,則將子選項物件上的全部屬性複製到合併物件上,因此父子選項上有相同屬性則以取子選項上該屬性的值。最後返回合併選項物件。

7、選項合併策略總結

1、elpropsData 以及採用預設策略合併的選項:有子選項就選用子選項的值,否則選用父選項的值。
2、選項 dataprovide :返回一個函式,該函式的返回值是合併之後的物件。以子選項物件為基礎,如果存在子選項上沒有而父選項上有的屬性,則將該屬性轉變成響應式屬性後加入到子選項物件上。
3、生命週期鉤子選項:合併成函式陣列,父選項排在子選項之前,按順序執行。
4、資源選項(components、directives、filters):定義一個沒有原型的空合併物件,子選項存在,則將子選項上的屬性複製到合併物件;父選項存在,則以父選項物件為原型。
5、選項 watch :子選項不存在,則返回以父選項為原型的空物件;父選項不存在,返回子選項;父子選項都存在,則和生命週期合併策略類似,以子選項屬性為主,轉化成陣列形式,父選項也存在該屬性,則推入陣列中。
6、選項props、methods、inject、computed:將父子選項上的屬性新增到一個沒有原型的空物件上,父子選項上有相同屬性的則取子選項的值。
7、子選項中 extendsmixins :將這兩項的值作為子選項與父選項合併,合併規則依照上述規則合併,最後再分項與子選項的同名屬性按上述規則合併。

四、總結

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

相關文章