Vue原理解析(二):初始化时beforeCreate以前作了什么?

上一篇:Vue原理解析(一):Vue究竟是什么?html

上一章节咱们知道了在new Vue()时,内部会执行一个this._init()方法,这个方法是在initMixin(Vue)内定义的:vue

export function initMixin(Vue) {
  Vue.prototype._init = function(options) {
    ...
  }
}
复制代码

当执行new Vue()执行后,触发的一系列初始化都在_init方法中启动,它的实现以下:node

let uid = 0

Vue.prototype._init = function(options) {

  const vm = this
  vm._uid = uid++  // 惟一标识
  
  vm.$options = mergeOptions(  // 合并options
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
  ...
  initLifecycle(vm) // 开始一系列的初始化
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm)
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
复制代码

先须要交代下,每个组件都是一个Vue构造函数的子类,这个以后会说明为什么如此。从上往下咱们一步步看,首先会定义_uid属性,这是为每一个组件每一次初始化时作的一个惟一的私有属性标识,有时候会有些做用。面试

有一个使用它的小例子,找到一个组件全部的兄弟组件并剔除本身:vue-router

<div>
  ...
  <child-components />
  <child-components />  // 找到它的兄弟组件
  ... 其余组件
  <child-components />
</div>
复制代码

首先要找的组件须要定义name属性,固然定义name属性也是一个好的书写习惯。首先经过本身的父组件($parent)的全部子组件($children)过滤出相同name集合的组件,这个时候他们就是同一个组件了,虽然它们name相同,可是_uid不一样,最后在集合内根据_uid剔除掉本身便可。vuex

合并options配置

回到主线任务,接着会合并options并在实例上挂载一个$options属性。合并什么东西了?这里是分两种状况的:数组

  1. 初始化new Vue

在执行new Vue构造函数时,参数就是一个对象,也就是用户的自定义配置;会将它和vue以前定义的原型方法,全局API属性;还有全局的Vue.mixin内的参数,将这些都合并成为一个新的options,最后赋值给一个的新的属性$options浏览器

  1. 子组件初始化

若是是子组件初始化,除了合并以上那些外,还会将父组件的参数进行合并,若有父组件定义在子组件上的eventprops等等。bash

通过合并以后就能够经过this.$options.data访问到用户定义的data函数,this.$options.name访问到用户定义的组件名称,这个合并后的属性很重要,会被常用到。app

接下里会顺序的执行一堆初始化方法,首先是这三个:

1. initLifecycle(vm)
2. initEvents(vm)
3. initRender(vm)
复制代码

1. initLifecycle(vm): 主要做用是确认组件的父子关系和初始化某些实例属性。

export function initLifecycle(vm) {
  const options = vm.$options  // 以前合并的属性
  
  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  // 让每个子组件的$root属性都是根组件
  
  vm.$children = []
  vm.$refs = {}
  
  vm._watcher = null
  ...
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
复制代码

vue是组件式开发的,因此当前实例可能会是其余组件的子组件的同时也多是其余组件的父组件。

首先会找到当前组件第一个非抽象类型的父组件,因此若是当前组件有父级且当前组件不是抽象组件就一直向上查找,直至找到后将找到的父级赋值给实例属性vm.$parent,而后将当前实例push到找到的父级的$children实例属性内,从而创建组件的父子关系。接下来的一些_开头是私有实例属性咱们记住是在这里定义的便可,具体意思也是之后用到的时候再作说明。

2. initEvents(vm): 主要做用是将父组件在使用v-on@注册的自定义事件添加到子组件的事件中心中。

首先看下这个方法定义的地方:

export function initEvents (vm) {
  vm._events = Object.create(null)  // 事件中心
  ...
  const listeners = vm.$options._parentListeners  // 通过合并options获得的
  if (listeners) {
    updateComponentListeners(vm, listeners) 
  }
}
复制代码

咱们首先要知道在vue中事件分为两种,他们的处理方式也各有不一样:

2.1 原生事件

在执行initEvents以前的模板编译阶段,会判断遇到的是html标签仍是组件名,若是是html标签会在转为真实dom以后使用addEventListener注册浏览器原生事件。绑定事件是挂载dom的最后阶段,这里只是初始化阶段,这里主要是处理自定义事件相关,也就是另一种,这里声明下,你们不要理会错了执行顺序。

2.2 自定义事件

在经历过合并options阶段后,子组件就能够从vm.$options._parentListeners读取到父组件传过来的自定义事件:

<child-components @select='handleSelect' />
复制代码

传过来的事件数据格式是{select:function(){}}这样的,在initEvents方法内定义vm._events用来存储传过来的事件集合。

内部执行的方法updateComponentListeners(vm, listeners)主要是执行updateListeners方法。这个方法有两个执行时机,首先是如今的初始化阶段,还一个就是最后patch时的原生事件也会用到。它的做用是比较新旧事件的列表来肯定事件的添加和移除以及事件修饰符的处理,如今主要看自定义事件的添加,它的做用是借助以前定义的$on$emit方法,完成父子组件事件的通讯,(详细的原理说明会在以后的全局API章节统一说明)。首先使用$onvm.events事件中心下建立一个自定义事件名的数组集合项,数组内的每一项都是对应事件名的回调函数,例如:

vm._events.select = [function handleSelect(){}, ...]  // 能够有多个
复制代码

注册完成以后,使用$emit方法执行事件:

this.$emit('select')
复制代码

首先会读取到事件中心内$emit方法第一个参数select的对象的数组集合,而后将数组内每一个回调函数顺序执行一遍即完成了$emit作的事情。

不知道你们有没有注意到this.$emit这个方法是在当前组件实例触发的,因此事件的原理可能跟大部分人理解的不同,并非父组件监听,子组件往父组件去派发事件。

而是子组件往自身的实例上派发事件,只是由于回调函数是在父组件的做用域下定义的,因此执行了父组件内定义的方法,就形成了父子之间事件通讯的假象。知道这个原理特性后,咱们能够作一些更cool的事情,例如:

<div>
  <parent-component>  // $on添加事件
    <child-component-1>
      <child-component-2>
        <child-component-3 />  // $emit触发事件
      </child-component-2>
    </child-components-1>
  </parent-component>
</div>
复制代码

咱们可不能够在parent-component内使用$on添加事件到当前实例的事件中心,而在child-components-3内找到parent-component的组件实例并在它的事件中心触发对应的事件实现跨组件通讯了,答案是能够了!这一原理发现再开发组件库时会有必定帮助。

3. initRender(vm): 主要做用是挂载能够将render函数转为vnode的方法。

export function initRender(vm) {
  vm._vnode = null
  ...
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)  //转化编译器的
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)  // 转化手写的
  ...
}
复制代码

主要做用是挂载vm._cvm.$createElement两个方法,它们只是最后一个参数不一样,这两个方法均可以将render函数转为vnode,从命名你们应该能够看出区别,vm._c转换的是经过编译器将template转换而来的render函数;而vm.$createElement转换的是用户自定义的render函数,好比:

new Vue({
  data: {
    msg: 'hello Vue!'
  },
  render(h) { // 这里的 h 就是vm.$createElement
    return h('span', this.msg);  
  }
}).$mount('#app');
复制代码

render函数的参数h就是vm.$createElement方法,将内部定义的树形结构数据转为Vnode的实例。

4. callHook(vm, 'beforeCreate')

终于咱们要执行实例的第一个生命周期钩子beforeCreate,这里callHook的原理是怎样的,咱们以后的生命周期章节会说明,如今这里只须要知道它会执行用户自定义的生命周期方法,若是有mixin混入的也一并执行。

好吧,实例的第一个生命周期钩子阶段的初始化工做完成了,一句话来主要说明下他们作了什么事情:

  • initLifecycle(vm):确认组件(也是vue实例)的父子关系
  • initEvents(vm):将父组件的自定义事件传递给子组件
  • initRender(vm):提供将render函数转为vnode的方法
  • beforeCreate:执行组件的beforeCreate钩子函数

最后仍是以一道vue容易被问道的面试题做为本章节的结束吧:

面试官微笑而又不失礼貌的问道:

  • 请问能够在beforeCreate钩子内经过this访问到data中定义的变量么,为何以及请问这个钩子能够作什么?

怼回去:

  • 是不能够访问的,由于在vue初始化阶段,这个时候data中的变量尚未被挂载到this上,这个时候访问值会是undefinedbeforeCreate这个钩子在平时业务开发中用的比较少,而像插件内部的instanll方法经过Vue.use方法安装时通常会选在beforeCreate这个钩子内执行,vue-routervuex就是这么干的。

下一篇:Vue原理解析(三):初始化时created以前作了什么?

顺手点个赞或关注呗,找起来也方便~

参考:

Vue.js源码全方位深刻解析

Vue.js深刻浅出

Vue.js组件精讲

剖析 Vue.js 内部运行机制

相关文章
相关标签/搜索