本文主要是通过源码了解,vue是怎样把事件传递给子组件并能够给正常调用的。
首先看一个超级简单的demo
// 组件
var aDom = Vue.component('aDom', {
template: ...,
methods: {
xxx() {
this.$emit('aaa')
}
}
})
// vm
const vm = new Vue({
el: '#app',
template: `
<div>
<a-dom @aaa="aaaFun" />
</div>
`,
methods: {
aaaFun(){console.log('函数')}
}
})
demo中有一个a-dom
组件,父级向里边传入了一个aaa
函数,个a-dom
组件中通过this.$emit('aaa')
调用了它,这是一个最简单的事件通信方法
跳过不相干的步骤,我们从创建a-dom
组件的虚拟节点处的源码开始说起
调用:$emit
想了解他的实现原理,可以拆开Event Bus
的整体流程,从$emit
方法下手,来看看子组件中函数调用时都做了什么
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event] // 获取到存储的方法数组
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
(invokeWithErrorHandling
函数就是一个通过apply|call
调用并抛出错误的通用函数,文章最底下会简略说明该函数)
可以看到,$emit
方法实际就是找到vm._events[event]
并调用该素组中的所有方法,那我们现在的目标就是找到vm._events
中的数据怎么存储的
存储 $on
$on
函数的目的是把函数放入vm._event
中
- 首先,判断形参
event
是否为一个数组(一个函数需要存多次),如果是数组,对其循环并再次执行vm.$on(event[i], fn)
- 然后在
push
前会首先判断vm._events[event]
是否有值,如果没有就赋值一个空数组((vm._events[event] || (vm._events[event] = [])).push(fn)
) - 最后进行
push
,在调用vm.$emit
的时候就可以获取到了
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) { // 如果是数组,循环再执行$on
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn) // 创建数组并进行push
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
存储的函数有了,那现在只要知道是怎样调用$on
的就可以理解事件处理的整个流程了,z这个就需要从头开始说起…
_init
初始化
首先我们要看下在模板渲染后组件是怎样获取到父级组件的方法的 下边是简略版的_init
函数,忽略其他的,我们只要看两个函数
initInternalComponent(vm, options)
:获取$options(在这里拿到aaa
方法)initEvents(vm)
:初始化组件传入的方法(此方法为核心)
Vue.prototype._init = function (options?: Object) {
const vm: Component = this // new后的this
// a uid
// 每个实例用_uid标记
vm._uid = uid++
vm._isVue = true
// 合并选项
// _isComponent表示是当前要处理的是一个component,这是在vdom/create-components.js里createComponentInstanceForVnode函数中定义的
if (options && options._isComponent) {
initInternalComponent(vm, options) // 创建$options并赋值父级传过来的数据
} else {
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)
}
vm._renderProxy = vm
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化组件的父子关系 $parent $children $root
initEvents(vm) // 初始化组件传入的方法 @xxx="xxx"
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化data,props,watch,computed,methods
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
initInternalComponent
如果是组件(options && options._isComponent
)就会进入该函数
initInternalComponent
函数的目的是创建
o
p
t
i
o
n
s
并
赋
值
父
级
传
过
来
的
数
据
。
把
‘
p
r
o
p
s
‘
、
‘
l
i
s
t
e
n
e
r
s
‘
等
内
容
存
到
‘
v
m
.
options并赋值父级传过来的数据。把`props`、`listeners`等内容存到`vm.
options并赋值父级传过来的数据。把‘props‘、‘listeners‘等内容存到‘vm.options上,此时你在子组件中
this.$options`就可以看到了~
详见代码:
// 创建$options并赋值父级传过来的数据
function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
const parentVnode = options._parentVnode
...
opts._parentListeners = vnodeComponentOptions.listeners // 组件监听器
...
}
核心 initEvents
initEvents
方法是初始化事件的入口函数,在该方法中首先初始化_events
对象,该对象是用来存储事件的,然后通过上一个方法里创建的$options
上是否有_parentListeners
来判断该组件是否绑定了事件,是否继续执行初始化事件方法
export function initEvents (vm: Component) {
vm._events = Object.create(null) // 初始化vm._events
vm._hasHookEvent = false // hook-event标记
// init parent attached events
const listeners = vm.$options._parentListeners // 所有父级传入的事件
if (listeners) {
// 处理组件标签上所带的事件
updateComponentListeners(vm, listeners)
}
}
updateComponentListeners
该函数内部其实只是临时存储了下当前组件并调用updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
函数,我们需要注意的是在调用时传的几个参数
listeners
:事件对象oldListeners
:更新时传的旧事件对象- add 调用
vm.$on
:向上边创建的事件对象vm._events
中添加事件的函数
function add (event, fn) {
target.$on(event, fn)
}
- remove调用
vm.$off
:对应vm.$on
移除事件的函数
function remove (event, fn) {
target.$off(event, fn)
}
- createOnceHandler:处理带有
.once
修饰符的函数(调用一次后就执行$off
)
function createOnceHandler (event, fn) {
const _target = target
return function onceHandler () {
const res = fn.apply(null, arguments)
if (res !== null) {
_target.$off(event, onceHandler)
}
}
}
- vm:vm…
updateComponentListeners
函数源码
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
updateListeners
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]
old = oldOn[name]
event = normalizeEvent(name) // 处理修饰符
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else 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)
} else if (cur !== old) { // 更新函数调用器
old.fns = cur
on[name] = old
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name) // 处理修饰符
remove(event.name, oldOn[name], event.capture)
}
}
}
updateListeners
函数的代码可以分为两个for in
循环,并且做了三件事
- 处理修饰符(第一个循环)
- 创建/更新函数调用器(第一个循环)
- 组件更新时删除无用事件(第二个循环)
1. 处理修饰符
...
event = normalizeEvent(name) // 处理修饰符
...
const normalizeEvent = cached((name: string): {
name: string,
once: boolean,
capture: boolean,
passive: boolean,
handler?: Function,
params?: Array<any>
} => {
const passive = name.charAt(0) === '&'
name = passive ? name.slice(1) : name
const once = name.charAt(0) === '~' // Prefixed last, checked first
name = once ? name.slice(1) : name
const capture = name.charAt(0) === '!'
name = capture ? name.slice(1) : name
return {
name,
once,
capture,
passive
}
})
normalizeEvent
函数主要是用来处理修饰符,比如once
的修饰符在编译后中对应的就是“~”(@xxx.once="xxx"
),具体修饰符对应的前缀可以看官方文档中的修饰符介绍
执行完毕后会返回一个名叫event
的对象,包括name
和修饰符对应的布尔值
2. 创建/更新函数调用器
...
if (isUndef(cur)) {
报个错.log
} else 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)
} else if (cur !== old) { // 更新函数调用器
old.fns = cur
on[name] = old
}
这段代码可以粗略的分为组件创建时和组件更新时两个不同的执行时间解释
创建时
以文章最开头的例子来说
我们来看for in
循环的name
为aaaFun
的循环内,在组件创建时肯定没有老函数(oldOn.aaaFun
),所以会进入else if (isUndef(old))
,此时cur
的值为aaaFun(){console.log('函数')}
会进入if (isUndef(cur.fns))
去创建函数调用器
创建函数调用器 createFnInvoker
createFnInvoker
函数会返回一个调用器函数,这个调用器函数内部会判断是否是多个函数(fns
是Array
类型)并调用invokeWithErrorHandling
方法执行该函数
(invokeWithErrorHandling
函数就是一个通过apply|call
调用并抛出错误的通用函数,文章最底下会简略说明该函数)
/**
* 创建函数调用器
*/
export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {
function invoker () {
const fns = invoker.fns
if (Array.isArray(fns)) {
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`)
}
} else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`)
}
}
invoker.fns = fns
return invoker
}
然后if (isTrue(event.once))
判断once
去调用上一个函数传进来的createOnceHandler
(上边已经说过该函数,在这里就跳过了),最后再调用上个函数传进来的add
来将函数添加到_event
中,我们的目的就达到了
更新时
更新时的逻辑很简单,进入这个函数时,cur
的值都是父组件传进来的函数,所以只需要将老的函数的调用器中存储的函数(old.fns
)换成当前的cur
就可以了
old.fns = cur
on[name] = old
3. 组件更新时删除无用事件
组件更新时,如果有新传入的事件会和创建组件时走相同的逻辑,有变化的事件会走上边说的更新函数调用器,最后剩下无用的事件,在这里调用上个函数传入的remove
将他删除掉
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
附加:invokeWithErrorHandling
函数
invokeWithErrorHandling
函数就是一个通过apply|call
调用并抛出错误的通用函数,他的传参分别是【调用的函数,this,传参[Array],vm,报错信息】
export function invokeWithErrorHandling (
handler: Function, // function
context: any, // this
args: null | any[], // null|any
vm: any, // vm
info: string // info
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context) // 调用函数
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}