做者:小土豆biubiubiujavascript
博客園:www.cnblogs.com/HouJiao/html
簡書:www.jianshu.com/u/cb1c3884e…vue
微信公衆號:土豆媽的碎碎念(掃碼關注,一塊兒吸貓,一塊兒聽故事,一塊兒學習前端技術)java
歡迎你們掃描微信二維碼進入羣聊討論(若二維碼失效可添加微信JEmbrace拉你進羣):node
碼字不易,點贊鼓勵喲~ ![]()
本篇文章內容過長,一次看完會有些乏味,建議你們能夠先收藏,分屢次進行閱讀,這樣更好理解。react
相信不少人和我同樣,在剛開始瞭解和學習Vue
生命明週期的時候,會作下面一系列的總結和學習。webpack
Vue
的實例在建立時會通過一系列的初始化:web
設置數據監聽、編譯模板、將實例掛載到DOM並在數據變化時更新DOM等
複製代碼
在這個初始化的過程當中會運行一些叫作"生命週期鉤子"的函數:npm
beforeCreate:組件建立前
created:組件建立完畢
beforeMount:組件掛載前
mounted:組件掛載完畢
beforeUpdate:組件更新以前
updated:組件更新完畢
beforeDestroy:組件銷燬前
destroyed:組件銷燬完畢
複製代碼
關於每一個鉤子函數裏組件的狀態示例:
<!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>
複製代碼
結合前面示例1的運行結果會有以下的總結。
組件建立前,組件須要掛載的DOM元素el和組件的數據data都未被建立。
複製代碼
建立建立完畢後,組件的數據已經建立成功,可是DOM元素el還沒被建立。
複製代碼
組件掛載前,DOM元素已經被建立,只是data中的數據尚未應用到DOM元素上。
複製代碼
組件掛載完畢後,data中的數據已經成功應用到DOM元素上。
複製代碼
組件更新前,data數據已經更新,組件掛載的DOM元素的內容也已經同步更新。
複製代碼
組件更新完畢後,data數據已經更新,組件掛載的DOM元素的內容也已經同步更新。
(感受和beforeUpdate的狀態基本相同)
複製代碼
組件銷燬前,組件已經再也不受vue管理,咱們能夠繼續更新數據,可是模板已經再也不更新。
複製代碼
組件銷燬完畢,組件已經再也不受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 (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的env屬性 它返回當前用戶環境信息。可是這個信息不是直接訪問就能獲取到值,而是須要經過設置才能獲取。
能夠看到我沒有設置這個屬性,因此訪問得到的結果是undefined
。
而後咱們在看一下Vue
項目中的webpack
對process.env.NODE_ENV
的設置說明:
執行
npm run dev
時會將process.env.NODE_MODE
設置爲'development'
執行npm run build
時會將process.env.NODE_MODE
設置爲'production'
該配置在Vue項目根目錄下的package.json scripts
中設置
因此設置process.env.NODE_ENV
的做用就是爲了區分當前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
方法是定義在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
方法中都幹了寫什麼呢?
大體瀏覽一下_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
的相關內容,因此就很少說了。
接着就是爲組件實例的_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
內部按順序調用的那些方法。
//源碼位置備註:/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
元素或者組件實例。
//源碼位置備註:/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
屬性,並給其賦值一個空對象。那_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></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
屬性保存的只是父組件綁定在當前組件上的事件,而不是組件中全部的事件。
_hasHookEvent
屬性表示的是父組件是否經過v-hook:鉤子函數名稱
把鉤子函數綁定到當前組件上。
對於這個函數,咱們首先須要關注的是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.$emit的用法在 Vue父子組件通訊 一文中有詳細的示例。
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
之間的關聯和差別。
//源碼位置備註:/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
的解釋,那爲了方便,我仍是寫一個示例。
<!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
是Vue
中做用域插槽的內容,和vm.$slot
查很少的原理,就很少說了。
在這裏暫時給
vm.$scopeSlots
賦值了一個空對象,後續會在掛載組件調用vm.$mount
時爲其賦值。
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
後在學習其內部實現。
這兩個屬性是有關組件通訊的實例屬性,賦值方式也很是簡單,不在多說。
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這個函數是個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中的數據變成響應式的,利用的也是雙向數據綁定的那一套原理。
//源碼位置備註:/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
這一步中完成的。
//源碼位置備註:/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
。
到這個階段已經順序執行完initLifecycle
、initState
、initRender
、callhook('beforeCreate')
、initInjections
、initProvide
這些方法,而後就會調用created
鉤子函數。
callHook
內部實如今前面已經說過,這裏也是同樣的,因此再也不重複說明。
到這裏,Vue2.x的生命週期的初始化階段
就解讀完畢了。這裏咱們將初始化階段作一個簡單的總結。
源碼仍是很強大的,學習的過程仍是比較艱難枯燥的,可是會發現不少有意思的寫法,還有咱們常常看過的一些理論內容在源碼中的真實實踐,因此必定要堅持下去。期待下一篇文章[你還不知道Vue的生命週期嗎?帶你從Vue源碼瞭解Vue2.x的生命週期(模板編譯階段)]
。
做者:小土豆biubiubiu
簡書:www.jianshu.com/u/cb1c3884e…
微信公衆號:土豆媽的碎碎念(掃碼關注,一塊兒吸貓,一塊兒聽故事,一塊兒學習前端技術)
歡迎你們掃描微信二維碼進入羣聊討論(若二維碼失效可添加微信JEmbrace拉你進羣):
碼字不易,點贊鼓勵喲~ ![]()