最近開了一個讀者回饋表單郵箱,無論是對文章的感想或是對部落格的感想,有什麼想回饋的都可以發郵箱跟我說:i_kkkp@163.com

Vuejs 组件事件与 emit 的实现

前言

想把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 的注入和事件名称的约定转换。

  1. setupContext 的注入: 在 Vue 的组件中,setup 函数接收两个参数:propscontextcontext 中包含了一系列属性和方法,其中之一就是 emit 函数。Vue 3 的组件中,setup 函数返回的对象中,可以将 emit 函数添加到 setupContext 中,以便用户在组件内通过 setupContext.emit 访问。

    下面是一个简单的例子,展示了如何在 setup 函数中添加 emitsetupContext

    setup(props, context) {
      // 添加 emit 到 setupContext
      context.emit = emit;
    
      // 其他 setup 逻辑
      // ...
    
      // 返回 setup 返回的对象
      return {};
    }

    这样,在组件内部,用户就可以通过 setupContext.emit 来调用 emit 函数了。

  2. 事件名称的约定转换: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 中正确地找到对应的事件处理函数。

React入门-第一篇 Vuejs 组件的实现原理

評論