你还不知道Vue的生命周期吗?带你从Vue源码了解Vue2.x的生命周期(初始化阶段)

做者:小土豆biubiubiujavascript

博客园:www.cnblogs.com/HouJiao/html

掘金:juejin.im/user/58c61b…前端

简书:www.jianshu.com/u/cb1c3884e…vue

微信公众号:土豆妈的碎碎念(扫码关注,一块儿吸猫,一块儿听故事,一块儿学习前端技术)java

欢迎你们扫描微信二维码进入群聊讨论(若二维码失效可添加微信JEmbrace拉你进群):node

码字不易,点赞鼓励哟~

舒适提示

本篇文章内容过长,一次看完会有些乏味,建议你们能够先收藏,分屡次进行阅读,这样更好理解。react

前言

相信不少人和我同样,在刚开始了解和学习Vue生命明周期的时候,会作下面一系列的总结和学习。webpack

总结1

Vue的实例在建立时会通过一系列的初始化:web

设置数据监听、编译模板、将实例挂载到DOM并在数据变化时更新DOM等
复制代码

总结2

在这个初始化的过程当中会运行一些叫作"生命周期钩子"的函数:npm

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的env属性 它返回当前用户环境信息。可是这个信息不是直接访问就能获取到值,而是须要经过设置才能获取。

能够看到我没有设置这个属性,因此访问得到的结果是undefined

而后咱们在看一下Vue项目中的webpackprocess.env.NODE_ENV的设置说明:

执行npm run dev时会将process.env.NODE_MODE设置为'development' 执行npm run build时会将process.env.NODE_MODE设置为'production'
该配置在Vue项目根目录下的package.json scripts中设置

因此设置process.env.NODE_ENV的做用就是为了区分当前Vue项目的运行环境是开发环境仍是生产环境,针对不一样的环境webpack在打包时会启用不一样的Plugin

&&前半段的逻辑说完了,在看下&&后半段的逻辑:this instanceof Vue

这个逻辑我决定用一个示例来解释一下,这样会很是容易理解。

咱们先写一个function

function Person(name,age){
    this.name = name;
    this.age = age;
    this.printThis = function(){
        console.log(this);
    } 
    //调用函数时,打印函数内部的this
    this.printThis();
}
复制代码

关于JavaScript的函数有两种调用方式:以普通函数方式调用和以构造函数方式调用。咱们分别以两种方式调用一下Person函数,看看函数内部的this是什么。

// 以普通函数方式调用
Person('小土豆biubiubiu',18);
// 以构造函数方式建立
var pIns = new Person('小土豆biubiubiu');
复制代码

上面这段代码在浏览器的执行结果以下:

从结果咱们能够总结:

以普通函数方式调用Person,Person内部的this对象指向的是浏览器全局的window对象
以构造函数方式调用Person,Person内部的this对象指向的是建立出来的实例对象
复制代码

这里实际上是JavaScript语言中this指向的知识点。

那咱们能够得出这样的结论:当以构造函数方式调用某个函数Fn时,函数内部this instanceof Fn逻辑的结果就是true

啰嗦了这么多,if条件判断的逻辑已经很明了了:

若是当前是非生产环境且没有使用new Vue的方式来调用Vue方法,就会有一个警告:
    Vue is a constructor and should be called with the `new`keyword
    
即Vue是一个构造函数应该使用关键字new来调用Vue
复制代码

建立组件实例 - _init方法的调用

_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

博客园:www.cnblogs.com/HouJiao/

掘金:juejin.im/user/58c61b…

简书:www.jianshu.com/u/cb1c3884e…

微信公众号:土豆妈的碎碎念(扫码关注,一块儿吸猫,一块儿听故事,一块儿学习前端技术)

欢迎你们扫描微信二维码进入群聊讨论(若二维码失效可添加微信JEmbrace拉你进群):

码字不易,点赞鼓励哟~
相关文章
相关标签/搜索