Vue通过$emit实现父子组件的通讯原理

微信扫一扫,分享到朋友圈

Vue通过$emit实现父子组件的通讯原理

它的作用是循环执行当前 vm (组件实例)的 _events 属性内某个 event (事件名)对应的事件回调列表。也就是触发事件。

Vue.prototype.$emit
定义在 src/core/instance/events.js
中。

Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
...
// 获取 _events 属性内某个 event (事件名)对应的事件回调列表
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
// 循环执行回调
for (let i = 0, l = cbs.length; i < l; i++) {
...
cbs[i].apply(vm, args)
...
}
}
return vm
}
复制代码

而 vm 的 _events 属性内的对 event 的回调方法收集全部是通过 Vue.prototype.$on
方法收集的。即事件监听。

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
...
// 对 event 的回调方法进行收集
(vm._events[event] || (vm._events[event] = [])).push(fn)
...
return vm
}
复制代码

通过 $emit
实现父子组件通讯的第一个步骤:在父组件内,对子组件的占位符标签上绑定一个自定义事件回调。这个绑定动作最终将通过子组件的 $on
方法将回调进行收集。

在父组件内,对子组件的占位符标签上绑定一个自定义事件回调,怎么被子组件收集。

例子:

父组件:

import Child from './Child.js'
export default {
name: 'Parent',
template: `<div class="parent-component">
<Child v-on:custom_event="handleCustomEvent"></Child>
</div>`,
methods: {
handleCustomEvent() {
console.log('this.$options.name:', this.$options.name)
}
},
components: {
Child
}
}
复制代码

子组件

export default {
name: 'Child',
template: `<div class="child-component" v-on:click="handleClick">click me!</div>`,
methods: {
test() {
console.log('test')
},
handleClick() {
this.$emit('custom_event')
}
}
}
复制代码

以下将以上两个组件为例,讲解通过$emit实现父子组件的通讯原理

parse (解析)父组件模版

模版解析过程就是AST (虚拟树)的生成过程、是通过各种正则表达式来匹配到节点的各个部分并处理。

匹配属性

<Child v-on:custom_event="handleCustomEvent"></Child>
复制代码

子节点占位符标签的属性 v-on:custom_event="handleCustomEvent"
被使用以下正则表达式被匹配到:

const attribute = /^s*([^s"'<>/=]+)(?:s*(=)s*(?:"([^"]*)"+|'([^']*)'+|([^s"'=<>`]+)))?/
复制代码

正则表达式=前面的作为name值,=后面作为value值,组成一个对象保存在节点的attrs队列中。
匹配结果:

{
name: 'v-on:custom_event',
value: 'handleCustomEvent'
}
复制代码

处理属性

节点处理过程中,attrs会被循环遍历,通过不同的正则匹配对属性name进行匹配分类,对不同类别的属性做不同的处理。

例子中占位符子节点的属性会被 const onRE = /^@|^v-on:/
这个正则被匹配到,属性被处理添加到占位符子节点的events属性内。

{
tag: 'Child'
events: {
'custom_event': {value: 'handleCustomEvent'}
}
...
}
复制代码

父组件渲染函数的生成

组件模版解析成虚拟树后再被生成代码字符串。

节点的events属性会以字符串的形式被添加到一个data的属性on中:

export function genHandlers (
events: ASTElementHandlers,
isNative: boolean,
warn: Function
): string {
let res = isNative ? 'nativeOn:{' : 'on:{'
// genHandler会对事件回调做一些处理
for (const name in events) {
res += `"${name}":${genHandler(name, events[name])},`
}
return res.slice(0, -1) + '}'
}
function genHandler (
name: string,
handler: ASTElementHandler | Array<ASTElementHandler>
): string {
...
// 把handler.value组装成执行命令的字符串
const handlerCode = isMethodPath
? handler.value + '($event)'
: isFunctionExpression
? `(${handler.value})($event)`
: handler.value
return `function($event){${code}${handlerCode}}`
...
}
复制代码

而data会成为创建虚拟节点函数的参数。
例子中父组件模版最后被编译成字符串

"with(this){return _c('div',{staticClass:"parent-component"},[_c('Child',{on:{"custom_event":handleCustomEvent}})],1)}"
复制代码

其中方法 _c
是 Vnode (虚拟节点)生成方法。 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

生成Vnode虚拟树

createElement方法定义在 src/core/vdom/create-element.js
。该方法实质上是调用了_createElement。

export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode {
...
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
}
...
if (isDef(vnode)) {
if (ns) applyNS(vnode, ns)
return vnode
} else {
return createEmptyVNode()
}
}
复制代码

例子中的 _c('Child',{on:{"custom_event":handleCustomEvent}})
这段代码最终会执行上面的 vnode = createComponent(Ctor, data, context, children, tag)
其中data就是 {on:{"custom_event":handleCustomEvent}}

export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | void {
...
// data.on的数据将作为实例化一个Vnode的componentOptions的listeners参数
const listeners = data.on
...
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
复制代码

实例化子组件

patch过程就是将Vnode转化成真实节点,当转化过程中遇到组件子节点时会递归得实例化子组件,子组件生成Vnode Tree,Vnode Tree经过patch生成正式的节点树,然后返回上一级。

在实例化子组件前,Vnode的数据被重写整合成options,作为实例化子组件的参数。其中listeners成为options._parentListeners。

在实例化子组件时,执行 initEvents
方法,将所有的options._parentListeners添加到子组件的实例上

function add (event, fn, once) {
// target是当前组件实例
if (once) {
target.$once(event, fn)
} else {
target.$on(event, fn)
}
}
复制代码

终于在父组件模版中的 v-on:custom_event="handleCustomEvent"
历经千山万水通过子组件实例的$on方法添加到子组件实例的_events中

事件回调中访问父组件

组件在实例化过程中 initState
(初始化状态),在初始化状态时会执行 initMethods
initMethods
这个方法的工作就是通过bind方法,使得 methods 中的方法 this 指向当前 vm (实例)。

function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
...
// bind方法
vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
}
}
复制代码
export function bind (fn: Function, ctx: Object): Function {
function boundFn (a) {
const l: number = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
// record original fn length
boundFn._length = fn.length
return boundFn
}
复制代码

父组件在初始化过程中,已经将 handleCustomEvent
this
绑定父组件, 所以当子组件通过触发 custom_event
方法时,可以在回调方法中访问到父组件,也就形成父子组件的通讯。

Java 各个GC的比较和选择

上一篇

高盛:瑞幸咖啡股东发生违约 7635万股ADS强制出售

下一篇

你也可能喜欢

Vue通过$emit实现父子组件的通讯原理

长按储存图像,分享给朋友