前幾篇文章中我們講到了resolveConstructorOptions,它的主要功能是解析當前例項建構函式上的options,不太明白的同學們可以看本系列的前幾篇文章。在解析完其建構函式上的options之後,需要把建構函式上的options和例項化時傳入的options進行合併操作並生成一個新的options。這個合併操作就是今天要講的mergeOptions。如果大家不想看枯燥的講解,可以直接點選人人都能懂的Vue原始碼系列—04—mergeOptions-下,翻到文章最後,檢視整個mergeOptions的流程圖。
Merge two option objects into a new one.
Core utility used in both instantiation and inheritance.
先來看原始碼中對mergeOptions方法的註釋。mergeOptions的功能是合併兩個options物件,並生成一個新的物件。是例項化和繼承中使用的核心方法。可見mergeOptions方法的重要性。既然這麼重要,那我就帶大家一行一行的來解析程式碼。保證大家看完這篇文章後,能徹底的理解mergeOptions的作用。
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)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, 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
}
首先看傳入的三個引數,parent,child,vm,這三個引數分別代表的是該例項建構函式上的options,例項化時傳入的options,vm例項本身。結合Vue作者寫的註釋,我們明白了,原來mergeoptions方法是要合併建構函式和傳入的options這兩個物件。
明白了這點之後,接下來往下看
if (process.env.NODE_ENV !== `production`) {
checkComponents(child) // 檢查元件名稱是否合法
}
這段程式碼主要是判斷當前環境是不是生產環境,如果不是,則呼叫checkComponents方法來檢查元件名稱是否是可用名稱。我們來看看checkComponents的邏輯。
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
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
)
}
}
如果child的options(例項化傳入的options)有components屬性。如下面這種情況
const app = new Vue({
el: `#app`,
...
components: {
childComponent
}
...
})
那麼就呼叫validateComponentName來驗證傳入的元件名稱是否符合以下特徵。
- 包含數字,字母,下劃線,連線符,並且以字母開頭
- 是否和html標籤名稱或svg標籤名稱相同
- 是否和關鍵字名稱相同,如undefined, infinity等
如果滿足第一條,並且第2,3條都是不相同的話,那麼元件名稱可用。
我們再回到mergeOptions原始碼中
if (typeof child === `function`) {
child = child.options
}
如果child是function型別的話,我們取其options屬性作為child。
接下來看這三個方法以normalize開頭的方法
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
這三個方法的功能類似,分別是把options中的props,inject,directives屬性轉換成物件的形式。因為有些傳入的時候可能會是陣列的形式。如
Vue.component(`blog-post`, {
props: [`postTitle`],
template: `<h3>{{ postTitle }}</h3>`
})
normalizeProps
我們先來看props處理的邏輯
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === `string`) {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== `production`) {
warn(`props must be strings when using array syntax.`)
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== `production`) {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
首先明確這兩個方法裡的引數是什麼,options傳入的是child,即例項化時傳入的options。vm是例項。知道了這兩個引數是什麼,我們繼續來研究程式碼。
const props = options.props
if (!props) return
const res = {}
let i, val, name
上面的程式碼主要是宣告一些變數。res用來存放修改後的props,最後把res賦給新的props。下面的邏輯可以分為兩種情況來考慮
props是陣列
當props是陣列的時候,如下面這種情況
Vue.component(`blog-post`, {
props: [`postTitle`],
template: `<h3>{{ postTitle }}</h3>`
})
它的處理邏輯是,遍歷props陣列,把陣列的每一項的值作為res物件的key,value值等於{type: null},即把上面例子中的[`postTitle`]轉換成下面這種形式
{
postTitle: { type: null }
}
props是物件
當props是物件時,如下面這種情況
Vue.component(`my-component`, {
props: {
// 必填的字串
propC: {
type: String,
required: true
}
}
})
這種情況的處理邏輯是遍歷物件,先把物件的key值轉換成駝峰的形式。然後再判斷物件的值,如果是純物件(即呼叫object.prototype.toString方法的結果是[object Object]),則直接把物件的值賦值給res,如果不是,則把{ type: 物件的值}賦給res。最終上面這種形式會轉換成
{
propC: {
type: String,
required: true
}
}
如果傳入的props不是純物件也不是陣列,且當前環境也不是生產環境,則丟擲警告。
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
最後,把處理過的props重新賦值給options.props。
normalizeInject
這個方法的邏輯和normalizeProps類似,主要是處理inject。inject屬性如果大家平時不是寫庫或者外掛的話,可能很少接觸到,可以先檢視inject的使用,inject的傳入和props類似。可以傳入object,也可以傳入array
// array
var Child = {
inject: [`foo`],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
// object
const Child = {
inject: {
foo: {
from: `bar`,
default: `foo`
}
}
}
由於這個方法和normalizeProps邏輯基本一樣,這裡也不具體展開講了。上面的demo最終會被轉換成如下形式
// array
{
foo: { from: `foo`}
}
// object
{
foo: {
from: `bar`,
default: `foo`
}
}
normalizeDirectives
這個方法主要是處理一些自定義指令,如果不瞭解自定義指令的同學可以自定義指令。這裡的方法處理邏輯主要針對自定義指令中函式簡寫的情況。如下
Vue.directive(`color`, function (el, binding) {
el.style.backgroundColor = binding.value
})
normalizeDirectives建構函式會把這個指令傳入的引數,最終轉換成下面這種形式
color: {
bind: function (el, binding) {
el.style.backgroundColor = binding.value
},
update: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
由於文章篇幅所限,本篇文章先講解到這裡,下篇繼續帶大家來看mergeOptions的實現。