底层实现,组件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,最终得到替换后的字符串。

最后验证结果,

相关推荐
全栈练习生几秒前
ESModule的工作原理是什么
前端
爱看书的小沐9 分钟前
【小沐学WebGIS】基于Three.JS绘制二三维地图地球晨昏效果(WebGL / vue / react )
javascript·vue.js·gis·webgl·three.js·opengl·晨昏线
疯狂的沙粒18 分钟前
Vue 前端大屏做多端屏幕适配时,如何让其自动适配多种不同尺寸的屏幕?
前端·javascript·vue.js
范小多22 分钟前
24小时学会Python Visual code +Python Playwright通过谷歌浏览器取控件元素(连载、十一)
服务器·前端·python
ooolmf23 分钟前
matlab2024读取温度01
java·前端·javascript
打工人小夏24 分钟前
前端vue3项目使用nprogress动画组件,实现页面加载动画
前端
一颗宁檬不酸26 分钟前
前端农业商城中产品产地溯源功能的实现
前端
李少兄33 分钟前
深入理解前端中的透视(Perspective)
前端·css
江公望43 分钟前
HTML5 History 模式 5分钟讲清楚
前端·html·html5
云和数据.ChenGuang1 小时前
Zabbix Web 界面安装时**无法自动创建配置文件 `zabbix.conf.php`** 的问题
前端·zabbix·运维技术·数据库运维工程师·运维教程