人人都能懂的Vue原始碼系列(三)—resolveConstructorOptions函式

淼淼真人發表於2018-05-24

上篇文章介紹了Vue建構函式的部分實現,如果當前Vue例項不是元件,則執行mergeOptions方法。

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)
複製程式碼

關於mergeOptions方法,我們之後的博文會做詳細介紹。今天主要來研究resolveConstructorOptions,從字面意思來看,它是來解析constructor上options屬性的,具體來看原始碼。

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 有super屬性,說明Ctor是Vue.extend構建的子類
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions // Vue建構函式上的options,如directives,filters,....
    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
}
複製程式碼

這個方法要分成兩種情況來說明,第一種是Ctor是基礎Vue構造器的情況,另一種是Ctor是通過Vue.extend方法擴充套件的情況。

Ctor是基礎Vue構造器

當Ctor(Ctor其實就是建構函式)是基礎Vue構造器時,如通過new關鍵字來新建Vue建構函式的例項

const vm = new Vue({
  el: '#app',
    data: {
      message: 'Hello Chris'
    }
})
複製程式碼

這個時候options就是Vue建構函式上的options。如下圖

global options
那麼這個options是在哪裡定義的呢?在之前的程式碼中好像沒有看到options的定義在哪裡?此時我們應該怎麼去找這個options定義的地方呢?

這裡教大家一個方法,首先找到package.json,在這裡可以找到我們平時用到的一些npm指令碼。以npm run dev為例。實際上npm run dev是執行了下列的命令 "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"

rollup是類似於webpack的打包工具。我們可以看到這條命令指向了一個地址scripts/config,之後還指定了一個Target。找到script/config,發現這個檔案裡有TARGET為web-full-dev的配置。

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

來分析上面的程式碼,入口檔案的地址在web/entry-runtime-with-compiler.js。這個檔案就是對Vue建構函式進行的第一層包裝了。由於今天分析的是options相關的內容,而這層包裝裡沒有options相關的內容,所以這個檔案我們不展開講(之後有文章會詳細介紹)。但是注意這裡的程式碼

...
import Vue from './runtime/index'
...
複製程式碼

我們Vue建構函式的第二層包裝,就在這個檔案裡了。忽略其他的程式碼,我們來看關於Vue.options的部分

...
import Vue from 'core/index' // 第三層包裝
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)
...

// platformDirectives相關
// 這裡匯出Vue全域性指令model,show
import model from './model'
import show from './show'
export default {
  model,
  show
}

// platformComponents相關
// 這裡匯出Vue全域性元件Transition,TransitionGroup
import Transition from './transition'
import TransitionGroup from './transition-group'
export default {
  Transition,
  TransitionGroup
}
複製程式碼

上面的程式碼主要是給Vue.options.directives新增model,show屬性,給Vue.options.components新增Transition,TransitionGroup屬性。那麼還有filters,_base屬性,以及components中的KeepAlive又是怎麼來的呢?

這就要看Vue的第三層包裝裡都做了些什麼?找到core/index,同樣我們只看Vue.options相關程式碼。

mport Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
...
initGlobalAPI(Vue)
...
複製程式碼

instance/index 就是我們第二篇文章——建構函式定義的那個檔案。這個檔案我們之前看過,沒有和Vue建構函式options相關的程式碼。那麼我們剩下的沒有配置的options一定是在initGlobalAPI上配置了。接來下看看/global-api/index的程式碼。

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
...
export function initGlobalAPI (Vue: GlobalAPI) {
  ...
  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)
  ...
}

// shared/constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

// core/components/index
import KeepAlive from './keep-alive'
export default {
  KeepAlive
}
複製程式碼

至此,filters,_base和components中的KeepAlive就都有了。通過這三層包裝,Vue建構函式的options物件就油然而生,看這些文字可能有點繞,我們直接上圖。

包裝options的過程
回到resolveConstructorOptions的原始碼中,當Ctor.super不存在時,直接返回基礎構造器的options。即上圖經過兩次包裝的options。那麼Ctor.super是什麼呢?

Ctor.super是通過Vue.extend構造子類的時候。Vue.extend方法會為Ctor新增一個super屬性,指向其父類構造器。

Vue.extend = function (extendOptions: Object): Function {
  ...
  Sub['super'] = Super
  ...
}
複製程式碼

所以當Ctor時基礎構造器的時候,resolveConstructorOptions方法返回基礎構造器的options。

Ctor是Vue.extend建立的"子類"

Vue.extend方法我們之後的博文再進行詳細介紹,這裡大家可以先把Vue.extend的功能籠統的理解為繼承。我們接下來看resolveConstructorOptions相關的程式碼,如果Ctor是Vue.extend建立的"子類",那麼在extend的過程中,Ctor物件上就會有super屬性。

Vue.extend = function (extendOptions: Object): Function {
  ...
  Sub['super'] = Super
  ...
}
複製程式碼

如果有super屬性,就會去執行if塊內的程式碼

...
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
...
// Vue.extend相關程式碼
Vue.extend = function (extendOptions: Object): Function {
  ...
  Sub.superOptions = Super.options // Sub.superOptions指向基礎構造器的options
  ...
}
複製程式碼

首先遞迴呼叫resolveConstructorOptions方法,返回"父類"上的options並賦值給superOptions變數。然後把"自身"的options賦值給cachedSuperOptions變數。

然後比較這兩個變數的值,當這兩個變數值不等時,說明"父類"的options改變過了。例如執行了Vue.mixin方法,這時候就需要把"自身"的superOptions屬性替換成最新的,之後檢查是否"自身"的options是否發生變化?resolveModifiedOptions的功能就是這個。

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

說了這麼多,大家可能還是有點陌生,我們直接舉個例子來說明一下。

  var Profile = Vue.extend({
     template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>'
  })
  Vue.mixin({ data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
   }})
   new Profile().$mount('#example')
複製程式碼

由於Vue.mixin改變了"父類"options,原始碼中superOptions和cachedSuperOptions就不相等了,大家可以去jsfiddle試試效果。 接下來看看resolveModifiedOptions都幹了哪些事情?

function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified // 定義modified變數
  const latest = Ctor.options // 自身的options
  const extended = Ctor.extendOptions // 構造"自身"時傳入的options
  const sealed = Ctor.sealedOptions // 執行Vue.extend時封裝的"自身"options,這個屬性就是方便檢查"自身"的options有沒有變化
 // 遍歷當前構造器上的options屬性,如果在"自身"封裝的options裡沒有,則證明是新新增的。執行if內的語句。呼叫dedupe方法,最終返回modified變數(即”自身新新增的options“)
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = dedupe(latest[key], extended[key], sealed[key])
    }
  }
  return modified
}
複製程式碼

那麼dedupe方法又是怎麼處理的呢?

function dedupe (latest, extended, sealed) {
  // compare latest and sealed to ensure lifecycle hooks won't be duplicated
  // between merges
  if (Array.isArray(latest)) {
    const res = []
    sealed = Array.isArray(sealed) ? sealed : [sealed]
    extended = Array.isArray(extended) ? extended : [extended]
    for (let i = 0; i < latest.length; i++) {
      // push original options and not sealed options to exclude duplicated options
      if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
        res.push(latest[i])
      }
    }
    return res
  } else {
    return latest
  }
}
複製程式碼

從作者的註釋可以看到這個方法主要就是防止生命週期建構函式重複。我們來看該方法傳入的3個引數: latest,extended,sealed

  1. lateset表示的是"自身"新增的options。
  2. extended表示的是當前構造器上新增的extended
  3. options,sealed表示的是當前構造器上新增的封裝options。

回到原始碼,如果latest不是陣列的話(lateset是"自身"新增的options),這裡不需要去重,直接返回latest。

如果傳入的latest是陣列(如果latest是陣列,一般這個新增的options就是生命週期鉤子函式),則遍歷該陣列,如果該陣列的某項在extended陣列中有或者在sealed陣列中沒有,則推送到返回陣列中從而實現去重。(這個去重邏輯目前自己還不是特別明白,之後如果明白了會在這裡更新,有明白的同學們請在評論區留言)

現在我們瞭解了resolveModifiedOptions和dedupe方法的作用,接下來回到resolveConstructorOptions原始碼。

  if (modifiedOptions) {
    extend(Ctor.extendOptions, modifiedOptions)
  }
  options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
  if (options.name) {
    options.components[options.name] = Ctor
  }
複製程式碼

如果”自身“有新新增的options,則把新新增的options屬性新增到Ctor.extendOptions屬性上,再呼叫mergeOptions方法合併"父類"構造器上的options和”自身“上的extendOptions(mergeOptions在下一篇博文中介紹),最後返回合併後的options。

看到這裡,可能會感覺到頭暈,為了讓大家更好的理解。我畫了下面的流程圖。

resolveConstructorOptions流程圖
關於resolveConstructorOptions的內容我們已經解析完了,下篇部落格主要講mergeOptions方法,它在整個Vue中屬於比較核心的一個方法。敬請期待!

如果您覺得我的文章還不錯,請不要吝惜你們的點贊!謝謝!

相關文章