前言
想把Vuejs 组件事件与 emit 的实现讲清楚,我先要把组件事件和emit的应用讲清楚,这样大家会更清楚的理解组件事件和emit的实现。
对于组件事件,我们其实之前就已经提到过了浏览器的事件机制:
浏览器事件回顾
浏览器事件是指在 Web 页面中发生的各种交互和状态变化的信号。这些事件可以由用户的操作、浏览器的状态变化或其他一些因素触发。以下是一些常见的 DOM 事件和它们的简介:
鼠标事件:
- click: 当鼠标点击一个元素时触发。对于触摸屏设备,也会在触摸屏上进行点击时触发。
- contextmenu: 当鼠标右键点击一个元素时触发。
- mouseover / mouseout: 当鼠标指针移入或离开一个元素时触发。
- mousedown / mouseup: 当在元素上按下或释放鼠标按钮时触发。
- mousemove: 当鼠标移动时触发。
键盘事件:
- keydown 和 keyup: 当按下和松开一个按键时触发。
表单(form)元素事件:
- submit: 当访问者提交了一个
<form>
时触发。 - focus: 当访问者聚焦于一个元素时触发,例如聚焦于一个
<input>
。
Document 事件:
- DOMContentLoaded: 当 HTML 的加载和处理均完成,DOM 被完全构建完成时触发。
CSS 事件:
- transitionend: 当一个 CSS 动画完成时触发。
这些事件可以通过 JavaScript 中的事件监听器来捕获和处理。例如,通过给元素添加事件监听器,可以在特定事件发生时执行相应的代码。事件是 Web 开发中交互和响应的基础。
那我们所说的组件事件和浏览器事件有什么区别呢?
对于事件这一个定义,我们可以这样理解:事件是指在特定的时间点上发生的事情,在某个时间点产生了一个信号,而信号可以被接收处理,这就是事件。
下面我们所说的组件事件,其实相对于浏览器事件来说更为宏观,它是指在Vue组件中,组件之间的通信方式,而这种通信方式是通过事件来实现的,这里的事件就是我们所说的组件事件。
Vue 组件之间通过事件进行通信的主要机制是通过 emit 和 on 方法。当子组件需要向父组件通信时,它会触发一个事件,并通过 emit 方法将事件发送出去;而父组件则通过在模板中使用 v-on 指令监听这个事件,从而执行相应的逻辑。
<!-- 子组件 ChildComponent.vue -->
<template>
<button @click="sendMessage">点击通知父组件</button>
</template>
<script>
export default {
methods: {
sendMessage() {
// 使用 emit 发送自定义事件
this.$emit('child-event', 'Hello from child!');
}
}
}
</script>
<!-- 父组件 ParentComponent.vue -->
<template>
<div>
<child-component @child-event="handleChildEvent"></child-component>
<p>从子组件接收的消息: {{ messageFromChild }}</p>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
messageFromChild: ''
};
},
methods: {
handleChildEvent(message) {
// 使用 v-on 监听子组件的事件
this.messageFromChild = message;
}
}
}
</script>
Vue 的事件机制是通过发布/订阅模式实现的。$emit 用于发布(触发)事件,而 v-on 则用于订阅(监听)事件。这使得不同组件之间可以相互通信,实现了组件之间的解耦。
首先,我们会注意到,子组件通过 this.$emit(‘child-event’, ‘Hello from child!’) 发送了一个自定义事件 child-event,而父组件则通过 @child-event=”handleChildEvent” 监听了这个事件。 这里的 child-event 就是我们自定义的事件名,而 handleChildEvent 则是父组件中定义的一个方法,用于处理子组件发送过来的消息。
当使用一个自定义的MyComponent组件时,我们可以监听由 emit 函数发射的自定义事件
可以看到,自定义事件 change 被编译成名为 onChange 的属性,并存储在 props 数据对象中。这实际上是一种约定。作为框架设计者,也可以按照自己期望的方式来设计事件的编译结果。在具体的实现上,发射自定义事件的本质就是根据事件名称去props 数据对象中寻找对应的事件处理函数并执行,如下面的代码所示:
emit的实现
// MyComponent 组件定义
const MyComponent = {
name: 'MyComponent',
setup(props, { emit }) {
// 发射 change 事件,并传递给事件处理函数两个参数
emit('change', 1, 2);
return () => {
// 组件的渲染逻辑
return /* ... */;
};
}
};
上面这一段代码会被编译成一个节点 CompNode,它的类型为 MyComponent 组件,同时会将 emit 函数传递给组件的 setup 函数,如下所示:
// 定义一个 Vue 组件节点 CompNode
const CompNode = {
// 指定节点的类型为 MyComponent 组件
type: MyComponent,
// 为组件传递的 props 对象
props: {
// 在 MyComponent 组件中,会有一个 onChange 的事件处理函数,其值为 handler
onChange: handler
}
};
可以看到,自定义事件 change 被编译成名为 onChange 的属性,并存储在 props 数据对象中。这实际上是一种约定。作为框架设计者,也可以按照自己期望的方式来设计事件的编译结果。
在具体的实现上,发射自定义事件的本质就是根据事件名称去props 数据对象中寻找对应的事件处理函数并执行,如下面的代码所示:
// 挂载组件
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const instance = {
state, // 状态
props: shallowReactive(props), // 响应式处理 props
isMounted: false,
subTree: null
};
// 定义 emit 函数,它接收两个参数
// event: 事件名称
// payload: 传递给事件处理函数的参数
function emit(event, ...payload) {
// 根据约定对事件名称进行处理,例如 change --> onChange
const eventName = `on${event[0].toUpperCase()}${event.slice(1)}`;
// 根据处理后的事件名称去 props 中寻找对应的事件处理函数
const handler = instance.props[eventName];
if (handler) {
// 调用事件处理函数并传递参数
handler(...payload);
} else {
console.error('事件不存在');
}
}
// 将 emit 函数添加到 setupContext 中
const setupContext = { attrs, emit };
// 省略部分代码
}
// 解析 props 数据的函数,对事件类型的 props 做特殊处理
function resolveProps(options, propsData) {
const props = {};
const attrs = {};
for (const key in propsData) {
// 以字符串 'on' 开头的 props 添加到 props 数据中,否则添加到 attrs 中
if (key in options || key.startsWith('on')) {
props[key] = propsData[key];
} else {
attrs[key] = propsData[key];
}
}
return [props, attrs];
}
emit
的实现原理主要涉及两个方面:setupContext
的注入和事件名称的约定转换。
setupContext 的注入: 在 Vue 的组件中,
setup
函数接收两个参数:props
和context
。context
中包含了一系列属性和方法,其中之一就是emit
函数。Vue 3 的组件中,setup
函数返回的对象中,可以将emit
函数添加到setupContext
中,以便用户在组件内通过setupContext.emit
访问。下面是一个简单的例子,展示了如何在
setup
函数中添加emit
到setupContext
:setup(props, context) { // 添加 emit 到 setupContext context.emit = emit; // 其他 setup 逻辑 // ... // 返回 setup 返回的对象 return {}; }
这样,在组件内部,用户就可以通过
setupContext.emit
来调用emit
函数了。事件名称的约定转换: 在
emit
函数内部,为了匹配组件模板中的事件处理函数,需要对事件名称进行约定转换。Vue 使用了一种约定,将事件名称转换为驼峰命名的形式。例如,change
事件会转换为onChange
。这样,用户在组件模板中监听事件时,可以使用驼峰命名的方式。下面是一个简单的例子,展示了事件名称的约定转换:
function emit(event, ...payload) { // 根据约定对事件名称进行处理,例如 change --> onChange const eventName = `on${event[0].toUpperCase()}${event.slice(1)}`; // 根据处理后的事件名称去 props 中寻找对应的事件处理函数 const handler = instance.props[eventName]; if (handler) { // 调用事件处理函数并传递参数 handler(...payload); } else { console.error('事件不存在'); } }
这里的
emit
函数会将事件名称转换为以 “on” 开头并使用驼峰命名的形式,例如change
转换为onChange
。然后,它会在instance.props
中查找对应的事件处理函数并执行。
需要注意的是,事件类型的 props 在 instance.props
中是找不到的,因此它们会存储在 attrs
中。为了解决这个问题,在解析 props 数据时,需要对事件类型的 props 做特殊处理,确保它们被正确添加到 props
中,而不是 attrs
。这样,emit
函数就能够在 instance.props
中正确地找到对应的事件处理函数。
評論