天天看点

一起学习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拉你进群):

码字不易,点赞鼓励哟~