作者:小土豆biubiubiu
博客園:https://www.cnblogs.com/HouJiao/
掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d
簡書:https://www.jianshu.com/u/cb1c3884e6d5
微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)
歡迎大家掃描微信二維碼進入群聊討論(若二維碼失效可添加微信JEmbrace拉你進群):
碼字不易,點贊鼓勵喲~
溫馨提示
本篇文章內容過長,一次看完會有些乏味,建議大家可以先收藏,分多次進行閱讀,這樣更好理解。
前言
相信很多人和我一樣,在剛開始了解和學習Vue生命明周期的時候,會做下面一系列的總結和學習。
總結1
Vue的實例在創建時會經過一系列的初始化:
設置數據監聽、編譯模板、將實例掛載到DOM並在數據變化時更新DOM等
總結2
在這個初始化的過程中會運行一些叫做"生命周期鈎子"的函數:
beforeCreate:組件創建前
created:組件創建完畢
beforeMount:組件掛載前
mounted:組件掛載完畢
beforeUpdate:組件更新之前
updated:組件更新完畢
beforeDestroy:組件銷毀前
destroyed:組件銷毀完畢
示例1
關於每個鈎子函數里組件的狀態示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue的生命周期</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<h3>{{info}}</h3>
<button v-on:click='updateInfo'>修改數據</button>
<button v-on:click='destoryComponent'>銷毀組件</button>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
info: 'Vue的生命周期'
},
beforeCreate: function(){
console.log("beforeCreated-組件創建前");
console.log("el:");
console.log(this.$el);
console.log("data:");
console.log(this.$data);
},
created: function(){
console.log("created-組件創建完畢");
console.log("el:");
console.log(this.$el);
console.log("data:");
console.log(this.$data);
console.log("info:");
console.log(this.$data.info);
},
beforeMount: function(){
console.log("beforeMounted-組件掛載前");
console.log("el:");
console.log(this.$el);
console.log("data:");
console.log(this.$data);
console.log("info:");
console.log(this.$data.info);
},
mounted: function(){
console.log("mounted-組件掛載完畢");
console.log("el:");
console.log(this.$el);
console.log("data:");
console.log(this.$data);
console.log("info:");
console.log(this.$data.info);
},
beforeUpdate: function(){
console.log("beforeUpdate-組件更新前");
console.log("el:");
console.log(this.$el);
console.log("data:");
console.log(this.$data);
console.log("info:");
console.log(this.$data.info);
},
updated: function(){
console.log("updated-組件更新完畢");
console.log("el:");
console.log(this.$el);
console.log("data:");
console.log(this.$data);
console.log("info:");
console.log(this.$data.info);
},
beforeDestroy: function(){
console.log("beforeDestory-組件銷毀前");
//在組件銷毀前嘗試修改data中的數據
this.info="組件銷毀前";
console.log("el:");
console.log(this.$el);
console.log("data:");
console.log(this.$data);
console.log("info:");
console.log(this.$data.info);
},
destroyed: function(){
console.log("destoryed-組件銷毀完畢");
//在組件銷毀完畢后嘗試修改data中的數據
this.info="組件已銷毀";
console.log("el:");
console.log(this.$el);
console.log("data:");
console.log(this.$data);
console.log("info:");
console.log(this.$data.info);
},
methods: {
updateInfo: function(){
// 修改data數據
this.info = '我發生變化了'
},
destoryComponent: function(){
//手動調用銷毀組件
this.$destroy();
}
}
});
</script>
</body>
</html>
總結3:
結合前面示例1的運行結果會有如下的總結。
組件創建前(beforeCreate)

組件創建前,組件需要掛載的DOM元素el和組件的數據data都未被創建。
組件創建完畢(created)

創建創建完畢后,組件的數據已經創建成功,但是DOM元素el還沒被創建。
組件掛載前(beforeMount):

組件掛載前,DOM元素已經被創建,只是data中的數據還沒有應用到DOM元素上。
組件掛載完畢(mounted)

組件掛載完畢后,data中的數據已經成功應用到DOM元素上。
組件更新前(beforeUpdate)

組件更新前,data數據已經更新,組件掛載的DOM元素的內容也已經同步更新。
組件更新完畢(updated)

組件更新完畢后,data數據已經更新,組件掛載的DOM元素的內容也已經同步更新。
(感覺和beforeUpdate的狀態基本相同)
組件銷毀前(beforeDestroy)

組件銷毀前,組件已經不再受vue管理,我們可以繼續更新數據,但是模板已經不再更新。
組件銷毀完畢(destroyed)

組件銷毀完畢,組件已經不再受vue管理,我們可以繼續更新數據,但是模板已經不再更新。
組件生命周期圖示
最后的總結,就是來自Vue官網的生命周期圖示。

那到這里,前期對Vue生命周期的學習基本就足夠了。那今天,我將帶大家從Vue源碼了解Vue2.x的生命周期的初始化階段,開啟Vue生命周期的進階學習。
Vue官網的這張生命周期圖示非常關鍵和實用,后面我們的學習和總結都會基於這個圖示。
創建組件實例
對於一個組件,Vue框架要做的第一步就是創建一個Vue實例:即new Vue()。那new Vue()都做了什么事情呢,我們來看一下Vue構造函數的源碼實現。
//源碼位置備注:/vue/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
從Vue構造函數的源碼可以看到有兩個重要的內容:if條件判斷邏輯和_init方法的調用。那下面我們就這兩個點進行抽絲破繭,看一看它們的源碼實現。
在這里需要說明的是
index.js文件的引入會早於new Vue代碼的執行,因此在new Vue之前會先執行initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin。這些方法內部大致就是在為組件實例定義一些屬性和實例方法,並且會為屬性賦初值。我不會詳細去解讀這幾個方法內部的實現,因為本篇主要是分析學習
new Vue的源碼實現。那我在這里說明這個是想讓大家大致了解一下和這部分相關的源碼的執行順序,因為在Vue構造函數中調用的_init方法內部有很多實例屬性的訪問、賦值以及很多實例方法的調用,那這些實例屬性和實例方法就是在index.js引入的時候通過執行initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin這幾個方法定義的。
創建組件實例 - if條件判斷邏輯
if條件判斷邏輯如下:
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
我們先看一下&&前半段的邏輯。
process是node環境內置的一個全局變量,它提供有關當前Node.js進程的信息並對其進行控制。如果本機安裝了node環境,我們就可以直接在命令行輸入一下這個全局變量。

這個全局變量包含的信息非常多,這里只截出了部分屬性。
對於process的evn屬性 它返回當前用戶環境信息。但是這個信息不是直接訪問就能獲取到值,而是需要通過設置才能獲取。

可以看到我沒有設置這個屬性,所以訪問獲得的結果是undefined。
然后我們在看一下Vue項目中的webpack對process.evn.NODE_EVN的設置說明:

執行
npm run dev時會將process.env.NODE_MODE設置為'development'
執行npm run build時會將process.env.NODE_MODE設置為'production'
該配置在Vue項目根目錄下的package.json scripts中設置
所以設置process.evn.NODE_EVN的作用就是為了區分當前Vue項目的運行環境是開發環境還是生產環境,針對不同的環境webpack在打包時會啟用不同的Plugin。
&&前半段的邏輯說完了,在看下&&后半段的邏輯:this instanceof Vue。
這個邏輯我決定用一個示例來解釋一下,這樣會非常容易理解。
我們先寫一個function。
function Person(name,age){
this.name = name;
this.age = age;
this.printThis = function(){
console.log(this);
}
//調用函數時,打印函數內部的this
this.printThis();
}
關於JavaScript的函數有兩種調用方式:以普通函數方式調用和以構造函數方式調用。我們分別以兩種方式調用一下Person函數,看看函數內部的this是什么。
// 以普通函數方式調用
Person('小土豆biubiubiu',18);
// 以構造函數方式創建
var pIns = new Person('小土豆biubiubiu');
上面這段代碼在瀏覽器的執行結果如下:

從結果我們可以總結:
以普通函數方式調用Person,Person內部的this對象指向的是瀏覽器全局的window對象
以構造函數方式調用Person,Person內部的this對象指向的是創建出來的實例對象
這里其實是JavaScript語言中this指向的知識點。
那我們可以得出這樣的結論:當以構造函數方式調用某個函數Fn時,函數內部this instanceof Fn邏輯的結果就是true。
啰嗦了這么多,if條件判斷的邏輯已經很明了了:
如果當前是非生產環境且沒有使用new Vue的方式來調用Vue方法,就會有一個警告:
Vue is a constructor and should be called with the `new`keyword
即Vue是一個構造函數應該使用關鍵字new來調用Vue
創建組件實例 - _init方法的調用
_init方法是定義在Vue原型上的一個方法:
//源碼位置備注:/vue/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
Vue的構造函數所在的源文件路徑為/vue/src/core/instance/index.js,在該文件中有一行代碼initMixin(Vue),該方法調用后就會將_init方法添加到Vue的原型對象上。這個我在前面提說過index.js和new Vue的執行順序,相信大家已經能理解。
那這個_init方法中都干了寫什么呢?
vm.$options
大致瀏覽一下_init內部的代碼實現,可以看到第一個就是為組件實例設置了一個$options屬性。
//源碼位置備注:/vue/src/core/instance/init.js
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
首先if分支的options變量是new Vue時傳遞的選項。

那滿足if分支的邏輯就是如果options存在且是一個組件。那在new Vue的時候顯然不滿足if分支的邏輯,所以會執行else分支的邏輯。
使用
Vue.extend方法創建組件的時候會滿足if分支的邏輯。
在else分支中,resolveConstructorOptions的作用就是通過組件實例的構造函數獲取當前組件的選項和父組件的選項,在通過mergeOptions方法將這兩個選項進行合並。
這里的父組件不是指組件之間引用產生的父子關系,還是跟
Vue.extend相關的父子關系。目前我也不太了解Vue.extend的相關內容,所以就不多說了。
vm._renderProxy
接着就是為組件實例的_renderProxy賦值。
//源碼位置備注:/vue/src/core/instance/init.js
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
如果是非生產環境,調用initProxy方法,生成vm的代理對象_renderProxy;否則_renderProxy的值就是當前組件的實例。
然后我們看一下非生產環境中調用的initProxy方法是如何為vm._renderProxy賦值的。
//源碼位置備注:/vue/src/core/instance/proxy.js
const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
在initProxy方法內部實際上是利用ES6中Proxy對象為將組件實例vm進行包裝,然后賦值給vm._renderProxy。
關於Proxy的用法如下:

那我們簡單的寫一個關於Proxy的用法示例。
let obj = {
'name': '小土豆biubiubiu',
'age': 18
};
let handler = {
get: function(target, property){
if(target[property]){
return target[property];
}else{
console.log(property + "屬性不存在,無法訪問");
return null;
}
},
set: function(target, property, value){
if(target[property]){
target[property] = value;
}else{
console.log(property + "屬性不存在,無法賦值");
}
}
}
obj._renderProxy = null;
obj._renderProxy = new Proxy(obj, handler);
這個寫法呢,仿照源碼給vm設置Proxy的寫法,我們給obj這個對象設置了Proxy。
根據handler函數的實現,當我們訪問代理對象_renderProxy的某個屬性時,如果屬性存在,則直接返回對應的值;如果屬性不存在則打印'屬性不存在,無法訪問',並且返回null。
當我們修改代理對象_renderProxy的某個屬性時,如果屬性存在,則為其賦新值;如果不存在則打印'屬性不存在,無法賦值'。
接着我們把上面這段代碼放入瀏覽器的控制台運行,然后訪問代理對象的屬性:

然后在修改代理對象的屬性:

結果和我們前面描述一致。然后我們在說回initProxy,它實際上也就是在訪問vm上的某個屬性時做一些驗證,比如該屬性是否在vm上,訪問的屬性名稱是否合法等。
總結這塊的作用,實際上就是在非生產環境中為我們的代碼編寫的代碼做出一些錯誤提示。
連續多個函數調用
最后就是看到有連續多個函數被調用。
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
我們把最后這幾個函數的調用順序和Vue官網的生命周期圖示對比一下:

可以發現代碼和這個圖示基本上是一一對應的,所以_init方法被稱為是Vue實例的初始化方法。下面我們將逐個解讀_init內部按順序調用的那些方法。
initLifecycle-初始化生命周期
//源碼位置備注:/vue/src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
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
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
在初始化生命周期這個函數中,vm是當前Vue組件的實例對象。我們看到函數內部大多數都是給vm這個實例對象的屬性賦值。
以
$開頭的屬性稱為組件的實例屬性,在Vue官網中都會有明確的解釋。
$parent屬性表示的是當前組件的父組件,可以看到在while循環中會一直遞歸尋找第一個非抽象的父級組件:parent.$options.abstract && parent.$parent。
非抽象類型的父級組件這里不是很理解,有伙伴知道的可以在評論區指導一下。
$root屬性表示的是當前組件的跟組件。如果當前組件存在父組件,那當前組件的根組件會繼承父組件的$root屬性,因此直接訪問parent.$root就能獲取到當前組件的根組件;如果當前組件實例不存在父組件,那當前組件的跟組件就是它自己。
$children屬性表示的是當前組件實例的直接子組件。在前面$parent屬性賦值的時候有這樣的操作:parent.$children.push(vm),即將當前組件的實例對象添加到到父組件的$children屬性中。所以$children數據的添加規則為:當前組件為父組件的$children屬性賦值,那當前組件的$children則由其子組件來負責添加。
$refs屬性表示的是模板中注冊了ref屬性的DOM元素或者組件實例。
initEvents-初始化事件
//源碼位置備注:/vue/src/core/instance/events.js
export function initEvents (vm: Component) {
// Object.create(null):創建一個原型為null的空對象
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
vm._events
在初始化事件函數中,首先給vm定義了一個_events屬性,並給其賦值一個空對象。那_events表示的是什么呢?我們寫一段代碼驗證一下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue的生命周期</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var ChildComponent = Vue.component('child', {
mounted() {
console.log(this);
},
methods: {
triggerSelf(){
console.log("triggerSelf");
},
triggerParent(){
this.$emit('updateinfo');
}
},
template: `<div id="child">
<h3>這里是子組件child</h3>
<p>
<button v-on:click="triggerSelf">觸發本組件事件
</button>
</p>
<p>
<button v-on:click="triggerParent">觸發父組件事件
</button>
</p>
</div>`
})
</script>
</head>
<body>
<div id="app">
<h3>這里是父組件App</h3>
<button v-on:click='destoryComponent'>銷毀組件</button>
<child v-on:updateinfo='updateInfo'>
</child>
</div>
<script>
var vm = new Vue({
el: '#app',
mounted() {
console.log(this);
},
methods: {
updateInfo: function() {
},
destoryComponent: function(){
},
}
});
</script>
</body>
</html>
我們將這段代碼的邏輯簡單梳理一下。
首先是child組件。
創建一個名為child組件的組件,在該組件中使用v-on聲明了兩個事件。
一個事件為triggerSelf,內部邏輯打印字符串'triggerSelf'。
另一個事件為triggetParent,內部邏輯是使用$emit觸發父組件updateinfo事件。
我們還在組件的mounted鈎子函數中打印了組件實例this的值。
接着是App組件的邏輯。
App組件中定義了一個名為destoryComponent的事件。
同時App組件還引用了child組件,並且在子組件上綁定了一個為updateinfo的native DOM事件。
App組件的mounted鈎子函數也打印了組件實例this的值。
因為在
App組件中引用了child組件,因此App組件和child組件構成了父子關系,且App組件為父組件,child組件為子組件。
邏輯梳理完成后,我們運行這份代碼,查看一下兩個組件實例中_events屬性的打印結果。


從打印的結果可以看到,當前組件實例的_events屬性保存的只是父組件綁定在當前組件上的事件,而不是組件中所有的事件。
vm._hasHookEvent
_hasHookEvent屬性表示的是父組件是否通過v-hook:鈎子函數名稱把鈎子函數綁定到當前組件上。
updateComponentListeners(vm, listeners)
對於這個函數,我們首先需要關注的是listeners這個參數。我們看一下它是怎么來的。
// init parent attached events
const listeners = vm.$options._parentListeners
從注釋翻譯過來的意思就是初始化父組件添加的事件。到這里不知道大家是否有和我相同的疑惑,我們前面說_events屬性保存的是父組件綁定在當前組件上的事件。這里又說_parentListeners也是父組件添加的事件。這兩個屬性到底有什么區別呢?
我們將上面的示例稍作修改,添加一條打印信息(這里只將修改的部分貼出來)。
<script>
// 修改子組件child的mounted方法:打印屬性
var ChildComponent = Vue.component('child', {
mounted() {
console.log("this._events:");
console.log(this._events);
console.log("this.$options._parentListeners:");
console.log(this.$options._parentListeners);
},
})
</script>
<!--修改引用子組件的代碼:增加兩個事件綁定(並且帶有事件修飾符) -->
<child v-on:updateinfo='updateInfo'
v-on:sayHello.once='sayHello'
v-on:SayBye.capture='SayBye'>
</child>
<script>
// 修改App組件的methods方法:增加兩個方法sayHello和sayBye
var vm = new Vue({
methods: {
sayHello: function(){
},
SayBye: function(){
},
}
});
</script>
接着我們在瀏覽器中運行代碼,查看結果。

從這個結果我們其實可以看到,_events和_parentListeners保存的內容實際上都是父組件綁定在當前組件上的事件。只是保存的鍵值稍微有一些區別:
區別一:
前者事件名稱這個key直接是事件名稱
后者事件名稱這個key保存的是一個字符串和事件名稱的拼接,這個字符串是對修飾符的一個轉化(.once修飾符會轉化為~;.capture修飾符會轉化為!)
區別二:
前者事件名稱對應的value是一個數組,數組里面才是對應的事件回調
后者事件名稱對應的vaule直接就是回調函數
Ok,繼續我們的分析。
接着就是判斷這個listeners:假如listeners存在的話,就執行updateComponentListeners(vm, listeners)方法。我們看一下這個方法內部實現。
//源碼位置備注:/vue/src/core/instance/events.js
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
可以看到在該方法內部又調用到了updateListeners,先看一下這個函數的參數吧。
listeners:這個參數我們剛說過,是父組件中添加的事件。
oldListeners:這參數根據變量名翻譯就是舊的事件,具體是什么目前還不太清楚。但是在初始化事件的整個過程中,調用到updateComponentListeners時傳遞的oldListeners參數值是一個空值。所以這個值我們暫時不用關注。(在/vue/src/目錄下全局搜索updateComponentListeners這個函數,會發現該函數在其他地方有調用,所以該參數應該是在別的地方有用到)。
add: add是一個函數,函數內部邏輯代碼為:
function add (event, fn) {
target.$on(event, fn)
}
remove: remove也是一個函數,函數內部邏輯代碼為:
function remove (event, fn) {
target.$off(event, fn)
}
createOnceHandler:
vm:這個參數就不用多說了,就是當前組件的實例。
這里我們主要說一下add函數和remove函數中的兩個重要代碼:target.$on和target.$off。
首先target是在event.js文件中定義的一個全局變量:
//源碼位置備注:/vue/src/core/instance/events.js
let target: any
在updateComponentListeners函數內部,我們能看到將組件實例賦值給了target:
//源碼位置備注:/vue/src/core/instance/events.js
target = vm
所以target就是組件實例。當然熟悉Vue的同學應該很快能反應上來$on、$off方法本身就是定義在組件實例上和事件相關的方法。那組件實例上有關事件的方法除了$on和$off方法之外,還有兩個方法:$once和$emit。
在這里呢,我們暫時不詳細去解讀這四個事件方法的源碼實現,只截圖貼出Vue官網對這個四個實例方法的用法描述。
vm.$on

vm.$once

vm.$emit

vm.$emit的用法在 Vue父子組件通信 一文中有詳細的示例。
vm.$off

updateListeners函數的參數基本解釋完了,接着我們在回歸到updateListeners函數的內部實現。
//源碼位置備注:/vue/src/vdom/helpers/update-listener.js
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
// 循環斷當前組件的父組件上的事件
for (name in on) {
// 根據事件名稱獲取事件回調函數
def = cur = on[name]
// oldOn參數對應的是oldListeners,前面說過這個參數在初始化的過程中是一個空對象{},所以old的值為undefined
old = oldOn[name]
event = normalizeEvent(name)
if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
// 將父級的事件添加到當前組件的實例中
add(event.name, cur, event.capture, event.passive, event.params)
}
}
}
首先是normalizeEvent這個函數,該函數就是對事件名稱進行一個分解。假如事件名稱name='updateinfo.once',那經過該函數分解后返回的event對象為:
{
name: 'updateinfo',
once: true,
capture: false,
passive: false
}
關於
normalizeEvent函數內部的實現也非常簡單,這里就直接將結論整理出來。感興趣的同學可以去看下源碼實現,源碼所在位置:/vue/src/vdom/helpers/update-listener.js。
接下來就是在循環父組件事件的時候做一些if/else的條件判斷,將父組件綁定在當前組件上的事件添加到當前組件實例的_events屬性中;或者從當前組件實例的_events屬性中移除對應的事件。
將父組件綁定在當前組件上的事件添加到當前組件的_events屬性中這個邏輯就是add方法內部調用vm.$on實現的。詳細可以去看下vm.$on的源碼實現,這里不再多說。而且從vm.$on函數的實現,也能看出_events和_parentListener之間的關聯和差異。
initRender-初始化模板
//源碼位置備注:/vue/src/core/instance/render.js
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
//將createElement fn綁定到組件實例上
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
initRender函數中,基本上是在為組件實例vm上的屬性賦值:$slots、$scopeSlots、$createElement、$attrs、$listeners。
那接下來就一一分析一下這些屬性就知道initRender在執行的過程的邏輯了。
vm.$slots

這是來自官網對vm.$slots的解釋,那為了方便,我還是寫一個示例。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue的生命周期</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var ChildComponent = Vue.component('child', {
mounted() {
console.log("Clild組件,this.$slots:");
console.log(this.$slots);
},
template:'<div id="child">子組件Child</div>'
})
</script>
</head>
<body>
<div id="app">
<h1 slot='root'>App組件,slot='root'</h1>
<child>
<h3 slot='first'>這里是slot=first</h3>
<h3 slot='first'>這里是slot=first</h3>
<h3>這里沒有設置slot</h3>
<h3 slot='last'>這里是slot=last</h3>
</child>
</div>
<script>
var vm = new Vue({
el: '#app',
mounted() {
console.log("App組件,this.$slots:");
console.log(this.$slots);
}
});
</script>
</body>
</html>
運行代碼,看一下結果。

可以看到,child組件的vm.$slots打印結果是一個包含三個鍵值對的對象。其中key為first的值保存了兩個VNode對象,這兩個Vnode對象就是我們在引用child組件時寫的slot=first的兩個h3元素。那key為last的值也是同樣的道理。
key為default的值保存了四個Vnode,其中有一個是引用child組件時寫沒有設置slot的那個h3元素,另外三個Vnode實際上是四個h3元素之間的換行,假如把child內部的h3這樣寫:
<child>
<h3 slot='first'>這里是slot=first</h3><h3 slot='first'>這里是slot=first</h3><h3>這里沒有設置slot</h3><h3 slot='last'>這里是slot=last</h3>
</child>
那最終打印
key為default對應的值就只包含我們沒有設置slot的h1元素。
所以源代碼中的resolveSlots函數就是解析模板中父組件傳遞給當前組件的slot元素,並且轉化為Vnode賦值給當前組件實例的$slots對象。
vm.$scopeSlots
vm.$scopeSlots是Vue中作用域插槽的內容,和vm.$slot查不多的原理,就不多說了。
在這里暫時給
vm.$scopeSlots賦值了一個空對象,后續會在掛載組件調用vm.$mount時為其賦值。
vm.$createElement
vm.$createElement是一個函數,該函數可以接收兩個參數:
第一個參數:HTML元素標簽名
第二個參數:一個包含Vnode對象的數組
vm.$createElement會將Vnode對象數組中的Vnode元素編譯成為html節點,並且放入第一個參數指定的HTML元素中。
那前面我們講過vm.$slots會將父組件傳遞給當前組件的slot節點保存起來,且對應的slot保存的是包含多個Vnode對象的數組,因此我們就借助vm.$slots來寫一個示例演示一下vm.$createElement的用法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue的生命周期</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var ChildComponent = Vue.component('child', {
render:function(){
return this.$createElement('p',this.$slots.first);
}
})
</script>
</head>
<body>
<div id="app">
<h1 slot='root'>App組件,slot='root'</h1>
<child>
<h3 slot='first'>這里是slot=first</h3>
<h3 slot='first'>這里是slot=first</h3>
<h3>這里沒有設置slot</h3>
<h3 slot='last'>這里是slot=last</h3>
</child>
</div>
<script>
var vm = new Vue({
el: '#app'
});
</script>
</body>
</html>
這個示例代碼和前面介紹vm.$slots的代碼差不多,就是在創建子組件時編寫了render函數,並且使用了vm.$createElement返回模板的內容。那我們瀏覽器中的結果。

可以看到,正如我們所說,vm.$createElement將$slots中frist對應的 包含兩個Vnode對象的數組編譯成為兩個h3元素,並且放入第一個參數指定的p元素中,在經過子組件的render函數將vm.$createElement的返回值進行處理,就看到了瀏覽器中展示的效果。
vm.$createElement內部實現暫時不深入探究,因為牽扯到Vue中Vnode的內容,后面了解Vnode后在學習其內部實現。
vm.$attr和vm.$listener
這兩個屬性是有關組件通信的實例屬性,賦值方式也非常簡單,不在多說。
callHook(beforeCreate)-調用生命周期鈎子函數
callhook函數執行的目的就是調用Vue的生命周期鈎子函數,函數的第二個參數是一個字符串,具體指定調用哪個鈎子函數。那在初始化階段,順序執行完 initLifecycle、initState、initRender后就會調用beforeCreate鈎子函數。
接下來看下源碼實現。
//源碼位置備注:/vue/src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
// 根據鈎子函數的名稱從組件實例中獲取組件的鈎子函數
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
首先根據鈎子函數的名稱從組件實例中獲取組件的鈎子函數,接着調用invokeWithErrorHandling,invokeWithErrorHandling函數的第三個參數為null,所以invokeWithErrorHandling內部就是通過apply方法實現鈎子函數的調用。
我們應該看到源碼中是循環
handlers然后調用invokeWithErrorHandling函數。那實際上,我們在編寫組件的時候是可以寫多個名稱相同的鈎子,但是實際上Vue在處理的時候只會在實例上保留最后一個重名的鈎子函數,那這個循環的意義何在呢?為了求證,我在
beforeCrated這個鈎子中打印了this.$options['before'],然后發現這個結果是一個數組,而且只有一個元素。
這樣想來就能理解這個循環的寫法了。
initInjections-初始化注入
initInjections這個函數是個Vue中的inject相關的內容。所以我們先看一下官方文檔度對inject的解釋。

官方文檔中說inject和provide通常是一起使用的,它的作用實際上也是父子組件之間的通信,但是會建議大家在開發高階組件時使用。
provide是下文中initProvide的內容。
關於inject和provide的用法會有一個特點:只要父組件使用provide注冊了一個數據,那不管有多深的子組件嵌套,子組件中都能通過inject獲取到父組件上注冊的數據。

大致了解inject和provide的用法后,就能猜想到initInjections函數內部是如何處理inject的了:解析獲取當前組件中inject的值,需要查找父組件中的provide中是否注冊了某個值,如果有就返回,如果沒有則需要繼續向上查找父組件。
下面看一下initInjections函數的源碼實現。
// 源碼位置備注:/vue/src/core/instance/inject.js
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
源碼中第一行就調用了resolveInject這個函數,並且傳遞了當前組件的inject配置和組件實例。那這個函數就是我們說的遞歸向上查找父組件的provide,其核心代碼如下:
// source為當前組件實例
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
// 繼續向上查找父組件
source = source.$parent
}
需要說明的是當前組件的_provided保存的是父組件使用provide注冊的數據,所以在while循環里會先判斷 source._provided是否存在,如果該值為 true,則表示父組件中包含使用provide注冊的數據,那么就需要進一步判斷父組件provide注冊的數據是否存在當前組件中inject中的屬性。
遞歸查找的過程中,對弈查找成功的數據,resolveInject函數會將inject中的元素對應的值放入一個字典中作為返回值返回。
例如當前組件中的inject設置為:inject: ['name','age','height'],那經過resolveInject函數處理后會得到這樣的返回結果:
{
'name': '小土豆biubiubiu',
'age': 18,
'height': '180'
}
最后在回到initInjections函數,后面的代碼就是在非生產環境下,將inject中的數據變成響應式的,利用的也是雙向數據綁定的那一套原理。
initState-初始化狀態
//源碼位置備注:/vue/src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
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 */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
初始化狀態這個函數中主要會初始化Vue組件定義的一些屬性:props、methods、data、computed、Watch。

我們主要看一下data數據的初始化,即initData函數的實現。
//源碼位置備注:/vue/src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
// 省略部分代碼······
// observe data
observe(data, true /* asRootData */)
}
在initData函數里面,我們看到了一行熟悉系的代碼:observe(data)。這個data參數就是Vue組件中定義的data數據。正如注釋所說,這行代碼的作用就是將對象變得可觀測。
在往observe函數內部追蹤的話,就能追到之前 [1W字長文+多圖,帶你了解vue2.x的雙向數據綁定源碼實現] 里面的Observer的實現和調用。
所以現在我們就知道將對象變得可觀測就是在Vue實例初始化階段的initData這一步中完成的。
initProvide-初始化
//源碼位置備注:/vue/src/core/instance/inject.js
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
這個函數就是我們在總結initInjections函數時提到的provide。那該函數也非常簡單,就是為當前組件實例設置_provide。
callHook(created)-調用生命周期鈎子函數
到這個階段已經順序執行完initLifecycle、initState、initRender、callhook('beforeCreate')、initInjections、initProvide這些方法,然后就會調用created鈎子函數。
callHook內部實現在前面已經說過,這里也是一樣的,所以不再重復說明。
總結
到這里,Vue2.x的生命周期的初始化階段就解讀完畢了。這里我們將初始化階段做一個簡單的總結。

源碼還是很強大的,學習的過程還是比較艱難枯燥的,但是會發現很多有意思的寫法,還有我們經常看過的一些理論內容在源碼中的真實實踐,所以一定要堅持下去。期待下一篇文章[你還不知道Vue的生命周期嗎?帶你從Vue源碼了解Vue2.x的生命周期(模板編譯階段)]。
作者:小土豆biubiubiu
博客園:https://www.cnblogs.com/HouJiao/
掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d
簡書:https://www.jianshu.com/u/cb1c3884e6d5
微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)
歡迎大家掃描微信二維碼進入群聊討論(若二維碼失效可添加微信JEmbrace拉你進群):
碼字不易,點贊鼓勵喲~


