上一篇: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
属性。合并什么东西了?这里是分两种状况的:数组
在执行new Vue
构造函数时,参数就是一个对象,也就是用户的自定义配置;会将它和vue
以前定义的原型方法,全局API
属性;还有全局的Vue.mixin
内的参数,将这些都合并成为一个新的options
,最后赋值给一个的新的属性$options
。浏览器
若是是子组件初始化,除了合并以上那些外,还会将父组件的参数进行合并,若有父组件定义在子组件上的event
、props
等等。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
章节统一说明)。首先使用$on
往vm.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._c
和vm.$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
混入的也一并执行。
好吧,实例的第一个生命周期钩子阶段的初始化工做完成了,一句话来主要说明下他们作了什么事情:
vue
实例)的父子关系render
函数转为vnode
的方法beforeCreate
钩子函数最后仍是以一道vue
容易被问道的面试题做为本章节的结束吧:
面试官微笑而又不失礼貌的问道:
beforeCreate
钩子内经过this
访问到data
中定义的变量么,为何以及请问这个钩子能够作什么?怼回去:
vue
初始化阶段,这个时候data
中的变量尚未被挂载到this
上,这个时候访问值会是undefined
。beforeCreate
这个钩子在平时业务开发中用的比较少,而像插件内部的instanll
方法经过Vue.use
方法安装时通常会选在beforeCreate
这个钩子内执行,vue-router
和vuex
就是这么干的。顺手点个赞或关注呗,找起来也方便~