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

做者:小土豆biubiubiujavascript

博客園:https://www.cnblogs.com/HouJiao/html

掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d前端

簡書:https://www.jianshu.com/u/cb1c3884e6d5vue

微信公衆號:土豆媽的碎碎念(掃碼關注,一塊兒吸貓,一塊兒聽故事,一塊兒學習前端技術)java

歡迎你們掃描微信二維碼進入羣聊討論(若二維碼失效可添加微信JEmbrace拉你進羣):node


碼字不易,點贊鼓勵喲~react

舒適提示

本篇文章內容過長,一次看完會有些乏味,建議你們能夠先收藏,分屢次進行閱讀,這樣更好理解。webpack

前言

相信不少人和我同樣,在剛開始瞭解和學習Vue生命明週期的時候,會作下面一系列的總結和學習。web

總結1

Vue的實例在建立時會通過一系列的初始化:npm

設置數據監聽、編譯模板、將實例掛載到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以前會先執行initMixinstateMixineventsMixinlifecycleMixinrenderMixin。這些方法內部大體就是在爲組件實例定義一些屬性和實例方法,而且會爲屬性賦初值。

我不會詳細去解讀這幾個方法內部的實現,由於本篇主要是分析學習new Vue的源碼實現。那我在這裏說明這個是想讓你們大體瞭解一下和這部分相關的源碼的執行順序,由於在Vue構造函數中調用的_init方法內部有不少實例屬性的訪問、賦值以及不少實例方法的調用,那這些實例屬性和實例方法就是在index.js引入的時候經過執行initMixinstateMixineventsMixinlifecycleMixinrenderMixin這幾個方法定義的。

建立組件實例 - if條件判斷邏輯

if條件判斷邏輯以下:

if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
}

咱們先看一下&&前半段的邏輯。

processnode環境內置的一個全局變量,它提供有關當前Node.js進程的信息並對其進行控制。若是本機安裝了node環境,咱們就能夠直接在命令行輸入一下這個全局變量。

這個全局變量包含的信息很是多,這裏只截出了部分屬性。

對於process的evn屬性 它返回當前用戶環境信息。可是這個信息不是直接訪問就能獲取到值,而是須要經過設置才能獲取。

能夠看到我沒有設置這個屬性,因此訪問得到的結果是undefined

而後咱們在看一下Vue項目中的webpackprocess.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.jsnew 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方法內部其實是利用ES6Proxy對象爲將組件實例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.$ontarget.$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打印結果是一個包含三個鍵值對的對象。其中keyfirst的值保存了兩個VNode對象,這兩個Vnode對象就是咱們在引用child組件時寫的slot=first的兩個h3元素。那keylast的值也是一樣的道理。

keydefault的值保存了四個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>

那最終打印keydefault對應的值就只包含咱們沒有設置sloth1元素。

因此源代碼中的resolveSlots函數就是解析模板中父組件傳遞給當前組件的slot元素,而且轉化爲Vnode賦值給當前組件實例的$slots對象。

vm.$scopeSlots

vm.$scopeSlotsVue中做用域插槽的內容,和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$slotsfrist對應的 包含兩個Vnode對象的數組編譯成爲兩個h3元素,而且放入第一個參數指定的p元素中,在通過子組件的render函數將vm.$createElement的返回值進行處理,就看到了瀏覽器中展現的效果。

vm.$createElement 內部實現暫時不深刻探究,由於牽扯到VueVnode的內容,後面瞭解Vnode後在學習其內部實現。

vm.$attr和vm.$listener

這兩個屬性是有關組件通訊的實例屬性,賦值方式也很是簡單,不在多說。

callHook(beforeCreate)-調用生命週期鉤子函數

callhook函數執行的目的就是調用Vue的生命週期鉤子函數,函數的第二個參數是一個字符串,具體指定調用哪一個鉤子函數。那在初始化階段,順序執行完 initLifecycleinitStateinitRender後就會調用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()
}

首先根據鉤子函數的名稱從組件實例中獲取組件的鉤子函數,接着調用invokeWithErrorHandlinginvokeWithErrorHandling函數的第三個參數爲null,因此invokeWithErrorHandling內部就是經過apply方法實現鉤子函數的調用。

咱們應該看到源碼中是循環handlers而後調用invokeWithErrorHandling函數。那實際上,咱們在編寫組件的時候是能夠寫多個名稱相同的鉤子,可是實際上Vue在處理的時候只會在實例上保留最後一個重名的鉤子函數,那這個循環的意義何在呢?

爲了求證,我在beforeCrated這個鉤子中打印了this.$options['before'],而後發現這個結果是一個數組,並且只有一個元素。

這樣想來就能理解這個循環的寫法了。

initInjections-初始化注入

initInjections這個函數是個Vue中的inject相關的內容。因此咱們先看一下官方文檔度對inject的解釋

官方文檔中說injectprovide一般是一塊兒使用的,它的做用實際上也是父子組件之間的通訊,可是會建議你們在開發高階組件時使用。

provide 是下文中initProvide的內容。

關於injectprovide的用法會有一個特色:只要父組件使用provide註冊了一個數據,那無論有多深的子組件嵌套,子組件中都能經過inject獲取到父組件上註冊的數據。

大體瞭解injectprovide的用法後,就能猜測到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組件定義的一些屬性:propsmethodsdatacomputedWatch

咱們主要看一下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)-調用生命週期鉤子函數

到這個階段已經順序執行完initLifecycleinitStateinitRendercallhook('beforeCreate')initInjectionsinitProvide這些方法,而後就會調用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拉你進羣):

碼字不易,點贊鼓勵喲~

相關文章
相關標籤/搜索