Vue通过$emit实现父子组件的通讯原理
父子组件通讯的方法之一是使用$emit。 该方法实现步骤:在父组件内,对子组件的占位符标签上绑定一个自定义事件回调在子组件内,调用$emit首先了解Vue.prototype.$emit的定义它的作用是循环执行当前 vm (组件实例)的 _events 属性内某个 event (事件名)对应的事件回调列表。也就是触发事件。Vue.prototype.$emit定义在src/core/instanc
父子组件通讯的方法之一是使用$emit
。 该方法实现步骤:
- 在父组件内,对子组件的占位符标签上绑定一个自定义事件回调
- 在子组件内,调用
$emit
首先了解Vue.prototype.$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
方法时,可以在回调方法中访问到父组件,也就形成父子组件的通讯。
更多推荐
所有评论(0)