隨著初始化函式的執行,例項的生命週期也開始運轉,在初始化函式裡可以看到每個模組向例項整合的功能,這些功能的具體內容以後在單獨的文章裡繼續探索。現在來詳細看看類初始化函式的詳細程式碼。
頭部引用
*下面程式碼位於vue/src/core/instance/init.js
import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'
複製程式碼
頭部注入的一些方法是在生命週期執行中開始初始化的功能,之前在核心類實現的文章中有提到過,在這裡不展開。config
物件是作為基本的配置引數,在不同執行環境裡會更改當中的屬性值來適應不同的平臺需求,在這個檔案中只用到了其中的效能檢測屬性,與具體的類的實現沒有太大關聯,與引入的mark
、measure
、formatComponentName
方法配合主要是做效能評估用的。
在初始化元件的時候主要用到的是工具方法extend
、mergeOptions
。
輔助函式extend
extend
函式是一個很簡單的為物件擴充套件屬性的方法,程式碼位於這個檔案中vue/src/shared/util.js,具體實現非常基礎,看看就好。
/**
* Mix properties into target object.
*/
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
複製程式碼
輔助函式mergeOptions
mergeOptions
函式程式碼位於
vue/src/core/util/options.js中,它是初始化合並options物件時非常重要的函式,為了看明白它在初始化函式裡的用途,稍微花點時間來仔細看一下它的具體實現。
// 該函式用於將兩個配置物件合併為一個新的配置物件,
// 核心實體既用於例項化也用於繼承
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
// 匯出mergeOptions函式
// 接收Object型別的parent、child引數,Component型別的vm引數
// 函式返回物件
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 非生產環境時檢查child物件的components屬性中是否有不合適的引用元件名稱
// 不合適的組建名主要是指與Vue內建html標籤或保留標籤名相同的元件名稱如slot,component
// 有興趣瞭解的可以參照同一檔案中的L246到L269檢視具體實現
// 其中的輔助工具函式位於src/shared/util.js的L94到L112
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
// 如果child傳入的是函式物件,則將函式的options屬性賦值給child,確保child引用options
if (typeof child === 'function') {
child = child.options
}
// 下面三個函式都是將child的各個屬性格式化成預定好的物件格式
// 標準化屬性
normalizeProps(child, vm)
// 標準化注入
normalizeInject(child, vm)
// 標準化指令
normalizeDirectives(child)
// 定義擴充套件
const extendsFrom = child.extends
// 如果存在則向下遞迴合併
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
// 如果存在mixins,則合併每一個mixin物件
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
// 定義以空物件options
const options = {}
let key
// 對每一個parent中的屬性進行合併,新增到options中
for (key in parent) {
mergeField(key)
}
// 如果parent中不含有key屬性,則對每一個child中key屬性進行合併
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 定義mergeField函式,接收key引數
function mergeField (key) {
// 如果strats[key]有定義好的合併策略函式,則複製給strat
// 否則將預設的defaultStrat方法賦給strat
const strat = strats[key] || defaultStrat
// 合併屬性
options[key] = strat(parent[key], child[key], vm, key)
}
// 返回最終options物件
return options
}
複製程式碼
儘管 mergeOptions
函式的實現有些複雜,但它的作用其實比較明確,就是解決初始化的過程中對繼承的類的options物件和新傳入的options物件之間同名屬性的衝突,即使用繼承的屬性值還是新傳入的屬性值的問題。在程式碼的一開始官方就已說明它是一個遞迴函式,可以一併解決新增了擴充套件內容和使用了mixins的場景,總而言之,這個步驟就是確保我們初始化的例項的options物件正確唯一。
程式碼中有幾個標準化屬性的函式,具體實現也在以上程式碼的同一檔案中,雖然有一堆程式碼,但實現還是比較簡單,主要目的就是把傳入的options物件的各個屬性格式化成基於物件的預定格式,在以後的執行中方便使用。
hasOwn
函式是對 Object.prototype.hasOwnProperty
方法的一個包裝,比較簡單,需要了解的話就去util工具函式檔案中檢視。
值得一提的是 strats
的使用。在程式碼的一開始的部分就定義 strats
變數,並說明它是用來處理父子選項合併屬性的功能。
/**
* Option overwriting strategies are functions that handle
* how to merge a parent option value and a child option
* value into the final value.
*/
const strats = config.optionMergeStrategies
複製程式碼
對於 el
和 propsData
屬性的合併策略賦予 defaultStrat
函式,該函式的原則是child物件屬性優先,沒有child物件屬性則返回parent的對應屬性。
/**
* Options with restrictions
*/
if (process.env.NODE_ENV !== 'production') {
strats.el = strats.propsData = function (parent, child, vm, key) {
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
return defaultStrat(parent, child)
}
}
複製程式碼
data
、watch
、props
、methods
、inject
、computed
、provide
、各種鉤子函式和ASSET_TYPES
裡包含的component
、directive
、 filter
三個屬性都分別定義了相關的合併方法,有興趣繼續瞭解的同學可以在同一分檔案中檢視,程式碼太長但是實現比較基礎,所以沒什麼好詳說的,可以關注一下的是某些屬性是替換覆蓋,而某些屬性是合併成陣列如各種鉤子的監聽函式。
初始化內部元件時options的合併
對於初始化合並options的操作分為了兩個方向,一是建立內部元件時的合併,二是建立非內部元件例項時的合併,先來說說內部元件初始化的詳細內容,在類的實現中對應著這一句程式碼 initInternalComponent(vm, options)
// 輸出initInternalComponent函式
// 接受Component型別的vm引數和InternalComponentOptions型別的options引數
// 這裡vm和options分別是建立例項時將要傳入的例項物件和配置物件
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// 定義opts,為opts和vm.$options建立以vm.constructor.options為原型的物件
const opts = vm.$options = Object.create(vm.constructor.options)
// 以下為手動賦值,目的為了提升效能,因為比通過動態列舉屬性來賦值的過程快
// doing this because it's faster than dynamic enumeration.
// 定義父虛擬節點parentVnode並賦值
const parentVnode = options._parentVnode
// 設定opts物件parent和_parentVnode屬性
opts.parent = options.parent
opts._parentVnode = parentVnode
// 定義vnodeComponentOptions並賦值
const vnodeComponentOptions = parentVnode.componentOptions
// 定義opts各屬性
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
// options.render屬性存在,則設定opts的render和staticRenderFns屬性
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
複製程式碼
可以看出 initInternalComponent
函式的內容比較簡單,主要是為建立的內部元件的options物件手動賦值,提升效能,因為按照官方註釋的說法是所有的內部元件的初始化都沒有列外可以同樣處理。至於什麼時候會建立內部元件,這種場景目前還不太瞭解,能確定的是通常建立Vue例項來初始化檢視頁面的用法是非內部元件性質的。在這裡留下一個疑問。
初始化例項時options的合併
下面三個函式就是初始化例項合併options這條線時用到的方法,後兩個函式作輔助用。對應如下帶程式碼。主要是用來解決建構函式的預設配置選項和擴充套件選項之間的合併問題。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
複製程式碼
// 匯出resolveConstructorOptions函式,接受Component建構函式Ctor引數
export function resolveConstructorOptions (Ctor: Class<Component>) {
// 定義傳入的建構函式的options屬性
let options = Ctor.options
// 如果Ctor.super存在則執行下面程式碼,這裡是用來判斷例項物件是否有繼承
// 如果有的話遞迴的把繼承的父級物件的options都拿出來合併
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// 如果父級的option變化了則要更新
// 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)
// 如果返回有修改的選項,則擴充套件Ctor.extendOptions
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
// 合併繼承選項和擴充套件選項
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
// 設定options.components[options.name]的引用
if (options.name) {
options.components[options.name] = Ctor
}
}
}
// 返回options
return options
}
// 以下兩個函式是為了解決#4976問題的方案
// 定義resolveModifiedOptions函式,接受Ctor引數,返回Object
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
// 定義modified變數儲存最終要選擇保留的屬性
let modified
// 分別定義最新、擴充套件和密封的配置選項
const latest = Ctor.options
const extended = Ctor.extendOptions
const sealed = Ctor.sealedOptions
// 遍歷傳入的配置選項物件
for (const key in latest) {
// 如果最新的屬性與密封的屬性不相等,則執行去重處理
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
modified[key] = dedupe(latest[key], extended[key], sealed[key])
}
}
// 返回modified
return modified
}
// 定義dedupe函式,接收latest最新物件,extended擴充套件物件,sealed密封物件
function dedupe (latest, extended, sealed) {
// 合併選項的時候比較最新和密封的屬性,確保生命週期鉤子不重複
// compare latest and sealed to ensure lifecycle hooks won't be duplicated
// between merges
// 如果latest是陣列
if (Array.isArray(latest)) {
// 定義res變數
const res = []
// 格式化sealed和extended為陣列物件
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 {
// 否則直接返回latest
return latest
}
}
複製程式碼
使用 resolveConstructorOptions
函式解決了繼承的建構函式的選項之後,新建立的例項vm的$options物件就是繼承選項和建立時傳入的options選項的合併。其中雖然有很多複雜的遞迴呼叫,但是這些函式的目的都是為了確定最終的選項,理解這個目的非常重要。
初始化函式的執行不僅在於開始生命週期的執行,對於options物件的各個屬性值如何取捨的問題給出了非常複雜但健全的解決方法,這為生命週期正常執行鋪墊了非常堅實的基礎,有了清晰的options選項,之後的功能才能如期順利執行。在這裡也可以看出Vue處理各種屬性的合併原則,對此有良好的理解可以確保在使用時立即定位遇到的相關問題。