实现mini-vue -- runtime-core模块(四)实现注册事件功能

1,784 阅读1分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

本节我们会来实现一下事件注册的功能,也就是平时我们在vuetemplate中会用到的v-on编译成渲染函数后的结果,比如v-on:click="xxx"则会被编译成vnode的一个prop -- onClick,而我们要实现的就是这个onXxx的功能,当xxx事件发生的时候,就会执行onXxx回调

1. 案例场景

根据vue3的官方文档对h函数的介绍,当props中有onXxx的方法时,会作为事件监听器,为相应的DOM添加监听事件,我们现在就是要去实现这个功能

首先修改一下hello world的案例:

export const App = {
  // 由于还没有实现模板编译的功能 因此先用 render 函数来替代
  render() {
    window.self = this;
    return h(
      'div',
      {
        class: ['cyan', 'success'],
        onClick() {
          console.log('click...');
        },
      },
      [
        h('p', { class: 'cyan' }, 'hi '),
        h('p', { class: 'darkcyan' }, 'plasticine '),
        h('p', { class: 'darkviolet' }, 'mini-vue!'),
        h('p', { class: 'darkcyan' }, `setupState msg: ${this.msg}`),
      ]
    );
  },
  setup() {
    // Composition API
    return {
      msg: 'plasticine-mini-vue',
    };
  },
};

2. 处理props时对事件监听属性做特殊处理

给根组件的DOM结点添加了一个onClick方法,作为click事件的回调,那么我们就应该到具体操作DOM的地方添加事件监听器,也就是要到mountElement函数中操作

// src/runtime-core/renderer.ts
function mountElement(vnode: any, container: any) {
  // 将 DOM 对象挂载到 vnode 上 从而让组件实例能够访问到
  const el = (vnode.el = document.createElement(vnode.type));
  const { children, shapeFlag } = vnode;

  // children
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    el.textContent = children;
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(children, el);
  }

  // props
  const { props } = vnode;
  for (const [key, value] of Object.entries(props)) {
-   el.setAttribute(key, value);
+   // 处理事件监听
+   if (key === 'onClick') {
+     el.addEventListener('click', value);
+   } else {
+     el.setAttribute(key, value);
+   }
  }

  container.append(el);
}

theme toggle.gif 这样就实现对点击事件的事件监听器注册功能啦!

3. 让事件处理更加通用

目前虽然能够处理click事件,但是还不够通用,我们应当让它能够处理任意事件,只要用户遵循在props设置onXxx命名格式的方法这一约定,就能够监听到相应的事件

既然有命名规范了,那就好办了,只需要遍历到符合事件监听器命名规范的属性时,就给它添加相应的事件监听器就可以了

function mountElement(vnode: any, container: any) {
  // 将 DOM 对象挂载到 vnode 上 从而让组件实例能够访问到
  const el = (vnode.el = document.createElement(vnode.type));
  const { children, shapeFlag } = vnode;

  // children
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    el.textContent = children;
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(children, el);
  }

  // props
  const { props } = vnode;
  for (const [key, value] of Object.entries(props)) {
+   const isOn = (key: string) => /^on[A-Z]/.test(key);
    // 处理事件监听
-   if (key === 'onClick') {
-     el.addEventListener('click', value);
-   } else {
-     el.setAttribute(key, value);
-   }
+   if (isOn(key)) {
+     el.addEventListener(key.slice(2).toLowerCase(), value);
+   } else {
+     el.setAttribute(key, value);
+   }
  }

  container.append(el);
}

现在就能够处理任何原生DOM事件了

4. 测试其他事件的监听

我们可以再添加一个事件监听回调测试一下

export const App = {
  // 由于还没有实现模板编译的功能 因此先用 render 函数来替代
  render() {
    window.self = this;
    return h(
      'div',
      {
        class: ['cyan', 'success'],
        onClick() {
          console.log('click...');
        },
        onMouseMove() {
          console.log('mouse moving...');
        },
      },
      [
        h('p', { class: 'cyan' }, 'hi '),
        h('p', { class: 'darkcyan' }, 'plasticine '),
        h('p', { class: 'darkviolet' }, 'mini-vue!'),
        h('p', { class: 'darkcyan' }, `setupState msg: ${this.msg}`),
      ]
    );
  },
  setup() {
    // Composition API
    return {
      msg: 'plasticine-mini-vue',
    };
  },
};

监听鼠标移动.gif