天天看點

一起學習vue源碼 - Vue2.x的生命周期(初始化階段)

作者:小洋芋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拉你進群):

碼字不易,點贊鼓勵喲~