上一章節我們知道了在new Vue()
時,內部會執行一個this._init()
方法,這個方法是在initMixin(Vue)
內定義的:
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
...
}
}
複製程式碼
當執行new Vue()
執行後,觸發的一系列初始化都在_init
方法中啟動,它的實現如下:
let uid = 0
Vue.prototype._init = function(options) {
const vm = this
vm._uid = uid++ // 唯一標識
vm.$options = mergeOptions( // 合併options
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
...
initLifecycle(vm) // 開始一系列的初始化
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
複製程式碼
先需要交代下,每一個元件都是一個Vue
建構函式的子類,這個之後會說明為何如此。從上往下我們一步步看,首先會定義_uid
屬性,這是為每個元件每一次初始化時做的一個唯一的私有屬性標識,有時候會有些作用。
有一個使用它的小例子,找到一個元件所有的兄弟元件並剔除自己:
<div>
...
<child-components />
<child-components /> // 找到它的兄弟元件
... 其他元件
<child-components />
</div>
複製程式碼
首先要找的元件需要定義name
屬性,當然定義name
屬性也是一個好的書寫習慣。首先通過自己的父元件($parent)
的所有子元件($children)
過濾出相同name
集合的元件,這個時候他們就是同一個元件了,雖然它們name
相同,但是_uid
不同,最後在集合內根據_uid
剔除掉自己即可。
合併options配置
回到主線任務,接著會合並options
並在例項上掛載一個$options
屬性。合併什麼東西了?這裡是分兩種情況的:
- 初始化new Vue
在執行new Vue
建構函式時,引數就是一個物件,也就是使用者的自定義配置;會將它和vue
之前定義的原型方法,全域性API
屬性;還有全域性的Vue.mixin
內的引數,將這些都合併成為一個新的options
,最後賦值給一個的新的屬性$options
。
- 子元件初始化
如果是子元件初始化,除了合併以上那些外,還會將父元件的引數進行合併,如有父元件定義在子元件上的event
、props
等等。
經過合併之後就可以通過this.$options.data
訪問到使用者定義的data
函式,this.$options.name
訪問到使用者定義的元件名稱,這個合併後的屬性很重要,會被經常使用到。
接下里會順序的執行一堆初始化方法,首先是這三個:
1. initLifecycle(vm)
2. initEvents(vm)
3. initRender(vm)
複製程式碼
1. initLifecycle(vm): 主要作用是確認元件的父子關係和初始化某些例項屬性。
export function initLifecycle(vm) {
const options = vm.$options // 之前合併的屬性
let parent = options.parent;
if (parent && !options.abstract) { // 找到第一個非抽象父元件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent // 找到後賦值
vm.$root = parent ? parent.$root : vm // 讓每一個子元件的$root屬性都是根元件
vm.$children = []
vm.$refs = {}
vm._watcher = null
...
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
複製程式碼
vue
是元件式開發的,所以當前例項可能會是其他元件的子元件的同時也可能是其他元件的父元件。
首先會找到當前元件第一個非抽象型別的父元件,所以如果當前元件有父級且當前元件不是抽象元件就一直向上查詢,直至找到後將找到的父級賦值給例項屬性vm.$parent
,然後將當前例項push
到找到的父級的$children
例項屬性內,從而建立元件的父子關係。接下來的一些_
開頭是私有例項屬性我們記住是在這裡定義的即可,具體意思也是以後用到的時候再做說明。
2. initEvents(vm): 主要作用是將父元件在使用v-on
或@
註冊的自定義事件新增到子元件的事件中心中。
首先看下這個方法定義的地方:
export function initEvents (vm) {
vm._events = Object.create(null) // 事件中心
...
const listeners = vm.$options._parentListeners // 經過合併options得到的
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
複製程式碼
我們首先要知道在vue
中事件分為兩種,他們的處理方式也各有不同:
2.1 原生事件
在執行initEvents
之前的模板編譯階段,會判斷遇到的是html
標籤還是元件名,如果是html
標籤會在轉為真實dom
之後使用addEventListener
註冊瀏覽器原生事件。繫結事件是掛載dom
的最後階段,這裡只是初始化階段,這裡主要是處理自定義事件相關,也就是另外一種,這裡宣告下,大家不要理會錯了執行順序。
2.2 自定義事件
在經歷過合併options
階段後,子元件就可以從vm.$options._parentListeners
讀取到父元件傳過來的自定義事件:
<child-components @select='handleSelect' />
複製程式碼
傳過來的事件資料格式是{select:function(){}}
這樣的,在initEvents
方法內定義vm._events
用來儲存傳過來的事件集合。
內部執行的方法updateComponentListeners(vm, listeners)
主要是執行updateListeners
方法。這個方法有兩個執行時機,首先是現在的初始化階段,還一個就是最後patch
時的原生事件也會用到。它的作用是比較新舊事件的列表來確定事件的新增和移除以及事件修飾符的處理,現在主要看自定義事件的新增,它的作用是藉助之前定義的$on
,$emit
方法,完成父子元件事件的通訊,(詳細的原理說明會在之後的全域性API
章節統一說明)。首先使用$on
往vm.events
事件中心下建立一個自定義事件名的陣列集合項,陣列內的每一項都是對應事件名的回撥函式,例如:
vm._events.select = [function handleSelect(){}, ...] // 可以有多個
複製程式碼
註冊完成之後,使用$emit
方法執行事件:
this.$emit('select')
複製程式碼
首先會讀取到事件中心內$emit
方法第一個引數select
的物件的陣列集合,然後將陣列內每個回撥函式順序執行一遍即完成了$emit
做的事情。
不知道大家有沒有注意到this.$emit
這個方法是在當前元件例項觸發的,所以事件的原理可能跟大部分人理解的不一樣,並不是父元件監聽,子元件往父元件去派發事件。
而是子元件往自身的例項上派發事件,只是因為回撥函式是在父元件的作用域下定義的,所以執行了父元件內定義的方法,就造成了父子之間事件通訊的假象。知道這個原理特性後,我們可以做一些更cool
的事情,例如:
<div>
<parent-component> // $on新增事件
<child-component-1>
<child-component-2>
<child-component-3 /> // $emit觸發事件
</child-component-2>
</child-components-1>
</parent-component>
</div>
複製程式碼
我們可不可以在parent-component
內使用$on
新增事件到當前例項的事件中心,而在child-components-3
內找到parent-component
的元件例項並在它的事件中心觸發對應的事件實現跨元件通訊了,答案是可以了!這一原理發現再開發元件庫時會有一定幫助。
3. initRender(vm): 主要作用是掛載可以將render
函式轉為vnode
的方法。
export function initRender(vm) {
vm._vnode = null
...
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) //轉化編譯器的
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // 轉化手寫的
...
}
複製程式碼
主要作用是掛載vm._c
和vm.$createElement
兩個方法,它們只是最後一個引數不同,這兩個方法都可以將render
函式轉為vnode
,從命名大家應該可以看出區別,vm._c
轉換的是通過編譯器將template
轉換而來的render
函式;而vm.$createElement
轉換的是使用者自定義的render
函式,比如:
new Vue({
data: {
msg: 'hello Vue!'
},
render(h) { // 這裡的 h 就是vm.$createElement
return h('span', this.msg);
}
}).$mount('#app');
複製程式碼
render
函式的引數h
就是vm.$createElement
方法,將內部定義的樹形結構資料轉為Vnode
的例項。
4. callHook(vm, 'beforeCreate')
終於我們要執行例項的第一個生命週期鉤子beforeCreate
,這裡callHook
的原理是怎樣的,我們之後的生命週期章節會說明,現在這裡只需要知道它會執行使用者自定義的生命週期方法,如果有mixin
混入的也一併執行。
好吧,例項的第一個生命週期鉤子階段的初始化工作完成了,一句話來主要說明下他們做了什麼事情:
- initLifecycle(vm):確認元件(也是
vue
例項)的父子關係 - initEvents(vm):將父元件的自定義事件傳遞給子元件
- initRender(vm):提供將
render
函式轉為vnode
的方法 - beforeCreate:執行元件的
beforeCreate
鉤子函式
最後還是以一道vue
容易被問道的面試題作為本章節的結束吧:
面試官微笑而又不失禮貌的問道:
- 請問可以在
beforeCreate
鉤子內通過this
訪問到data
中定義的變數麼,為什麼以及請問這個鉤子可以做什麼?
懟回去:
- 是不可以訪問的,因為在
vue
初始化階段,這個時候data
中的變數還沒有被掛載到this
上,這個時候訪問值會是undefined
。beforeCreate
這個鉤子在平時業務開發中用的比較少,而像外掛內部的instanll
方法通過Vue.use
方法安裝時一般會選在beforeCreate
這個鉤子內執行,vue-router
和vuex
就是這麼幹的。
順手點個贊或關注唄,找起來也方便~
參考:
分享一個元件庫給大家,可能會用的上 ~ ↓