底层实现,组件emit功能

前言

Vue 父子组件间传值,最基础也是最经典的实现就是父到子使用 props 传值,子到父使用 emit 触发,这样数据都是从父组件传输过来的,保证了数据的单一流向。

上篇文章《组件props传值大揭秘》实现了父组件到子组件的 props 功能,本篇文章来实现子组件到父组件到 emit 功能。

新建测试项目,App.js代码如下:

js 复制代码
import { h } from "../../lib/zwd-mini-vue.esm.js";
import { Foo } from "./Foo.js";

export const App = {
  render() {
    return h("div", {}, [
      h(Foo, {
        onAdd: () => {
          console.log("onAdd");
        }
      }),
    ]);
  },
  setup() {
    return {};
  },
};

Foo.js代码如下:

js 复制代码
import { h } from "../../lib/zwd-mini-vue.esm.js";

export const Foo = {
  setup(props, { emit }) {
    return {
      handleClick() {
        console.log("click");
        emit("add");
      },
    };
  },
  render() {
    const foo = h("div", {}, "foo");
    const btn = h(
      "button",
      {
        onClick: this.handleClick,
      },
      "button"
    );
    return h("div", {}, [foo, btn]);
  },
};

以上测试代码中,新建一个父组件App,它内部引用子组件Foo,子组件中按钮点击在控制台输出clicksetup中第二个参数对象中emit方法可以对外抛出事件add。父组件在引用子组件时,调用onAdd方法可以触发子组件的事件,并在控制台输出onAdd

简单总结一下,需要实现的功能点:

  1. setup接收第 2 个参数对象,其中有 emit 方法
  2. emit方法内自定义事件名,需要首字母大写再加上on,然后执行其函数

实现

因为emit参数是在setup方法中,实现的焦点就可以定位到setup函数调用,在 component.ts 中 setupStatefulComponent方法里,setup方法执行时,给第 2 个参数对象。

ts 复制代码
function setupStatefulComponent(instance) {
  const Component = instance.type;

  instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers);

  const { setup } = Component;
  if (setup) {
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
    });
    handleSetupResult(instance, setupResult);
  }
}

emit方法挂载在实例instance上,那在初始化时就需要给emit赋值,也就是需要从子组件中获取到emit内部的自定义方法名add

ts 复制代码
export function createComponentInstance(vnode) {
  const component = {
    vnode,
    type: vnode.type,
    setupState: {},
    props: {},
    emit: () => {},
  };

  component.emit = emit.bind(null, vnode) as any;

  return component;
}

emit方法单独抽离出一个文件 componentEmit.ts。

这里需要注意一个点,在用户使用emit方法时,只需要传入一个自定义的事件名,但是在内部实现时,当拿到这个自定义事件名event,还需要父组件的props对象,才能根据这个event找到对应的props中的key,获取到相应的函数。

这就需要内部实现时默认传入props对象,这里的技巧是利用bind,第 1 个参数this不存在即为null,还可以传入一系列参数,则第 2 个参数传入虚拟节点vnode,即可从中解构出props

componentEmit.ts 文件代码如下:

ts 复制代码
export function emit(instance, event) {
  const { props } = instance;
  
  const capitalize = (str: string) => {
    return str.charAt(0).toUpperCase() + str.slice(1);
  };
  const toHandlerKey = (str) => {
    return str ? `on${capitalize(str)}` : "";
  };

  const handlerName = toHandlerKey(event);
  const handler = props[handlerName];
  handler && handler();
}

以上代码,第 1 个参数instance就是通过bind绑定传入的参数vnode,第 2 个参数是用户自定义的事件名。按照测试项目的代码例子,这里传入的event就是add,只需要将add转换成onAdd,并获取到onAdd对应的函数,执行这个函数即可。

用户传入的自定义事件有add这样的命令方式,也可能存在add-foo这样的命令方法。

修改测试项目代码,Foo.js代码如下:

js 复制代码
export const Foo = {
  setup(props, { emit }) {
    return {
      handleClick() {
        console.log("click");
        emit("add");
        emit("add-foo");
      },
    };
  },
  render() {
    const foo = h("div", {}, "foo");
    const btn = h(
      "button",
      {
        onClick: this.handleClick,
      },
      "button"
    );
    return h("div", {}, [foo, btn]);
  },
};

App.js代码如下:

js 复制代码
export const App = {
  render() {
    return h("div", {}, [
      h(Foo, {
        onAdd: () => {
          console.log("onAdd");
        },
        onAddFoo: () => {
          console.log("onAddFoo");
        },
      }),
    ]);
  },
  setup() {
    return {};
  },
};

针对于不同的事件名规则,最终都需要转换成首字母大写的驼峰命名规则,再加上个on。那需要修改的地方只有emit方法内部,对于event的改造。

ts 复制代码
export function emit(instance, event) {
  const { props } = instance;

  const camelCase = (str) => {
    return str.replace(/-(\w)/g, (_, c: string) => {
      return c ? c.toUpperCase() : "";
    });
  };
  
  const capitalize = (str: string) => {
    return str.charAt(0).toUpperCase() + str.slice(1);
  };
  const toHandlerKey = (str) => {
    return str ? `on${capitalize(str)}` : "";
  };

  const handlerName = toHandlerKey(camelCase(event));
  const handler = props[handlerName];
  handler && handler();
}

以上代码中,camelCase方法就是针对add-foo这类命令规则的格式化,首先是改写成驼峰命名addFoo,然后调用之前的逻辑,改成首字母大写,再拼接on

camelCase方法中采用正则表达式,获取到add-foo中的-f进行替换。这里回顾一下replace的使用,尤其是第 2 个参数是个函数的情况。

replace 方法的第 2 个参数可以是一个函数。当第 2 个参数是函数时,replace 方法会在匹配到的每个子串上调用该函数,并将匹配到的子串作为第一个参数传递给函数。可以根据需要在函数内部对匹配到的子串进行处理,并返回替换后的结果。

格式通常如下:

js 复制代码
function replacer(match, p1, p2, ..., offset, string) {
  // 处理匹配到的子串并返回替换后的结果
}

其中的参数说明如下:

  • match:匹配到的子串。
  • p1, p2, ...:如果正则表达式使用了分组捕获,这些参数表示从左到右的分组捕获结果。例如,如果正则表达式是 /(\w+)\s(\w+)/,并且应用于字符串 "Hello World",那么 p1 将是 "Hello"p2 将是 "World"
  • offset:匹配到的子串在原字符串中的偏移量。
  • string:原始字符串。

函数可以返回用于替换的字符串。在函数中,可以根据需要对匹配到的子串进行处理,也可以使用其他逻辑来动态生成替换后的字符串。

以下是一个示例,将字符串中的数字加倍:

js 复制代码
const str = 'I have 3 apples and 5 oranges.';
const result = str.replace(/\d+/g, function(match) {
  return match * 2;
});

console.log(result);
// 输出:I have 6 apples and 10 oranges.

在上述示例代码中,正则表达式 /(\d+)/g 匹配到字符串中的数字,然后通过传递的函数将每个匹配到的数字乘以 2,最终得到替换后的字符串。

最后验证结果,

相关推荐
DT——3 小时前
Vite项目中eslint的简单配置
前端·javascript·代码规范
学习ing小白5 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
一只小阿乐5 小时前
前端web端项目运行的时候没有ip访问地址
vue.js·vue·vue3·web端
计算机学姐5 小时前
基于python+django+vue的旅游网站系统
开发语言·vue.js·python·mysql·django·旅游·web3.py
真的很上进6 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er6 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063716 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl6 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码6 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347546 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js