实现组件的 emit 功能

1. 什么是 emit?

export const Foo = {
  setup(props, { emit }) {
    // setup 第二个参数是 ctx,里面有一个参数是 emit
    const handleClick = () => {
      // emit 是一个函数,第一个参数是触发的事件
      emit('add')
    }
    return {
      handleClick,
    }
  },
  render() {
    return h(
      'button',
      {
        onClick: this.handleClick,
      },
      '点击我'
    )
  },
}
export default {
  render() {
    // 这里在写组件的时候,第二个参数就可以传入 on + emit's Event
    return h('div', {}, [h('p', {}, 'hello'), h(Foo, { onAdd: this.onAdd })])
  },
  setup() {
    function onAdd() {
      console.log('onAdd')
    }
    return {
      onAdd,
    }
  },
}

2. 实现 emit

首先,setup 的第二个参数是一个对象,传入 emit

// component.ts

// 在 setupStatefulComponent 时调用组件的 component
function setupStatefulComponent(instance) {
    // other code ...
     const setupResult = setup(shallowReadonly(instance.props), {
      // 传入的 emit 就可以直接使用 instance.emit
      emit: instance.emit,
    })
    handleSetupResult(instance, setupResult)
}

那么我们需要在初始化 emit 的时候去注册一下 emit

export function createComponentInstance(vnode) {
  // 这里返回一个 component 结构的数据
  const component = {
    vnode,
    type: vnode.type,
    setupState: {},
    props: {},
    emit: () => {},
  }
  component.emit = emit as any
  return component
}

将 emit 的逻辑单独抽离出来,创建 componentEmit.ts

// componentEmit.ts

// 第一个参数接收一个 event 的值
export function emit(event) {
  console.log('event', event)
}

而我们的触发 event 的值是从哪里来的呢?我们再回头看看上面的例子,其实第二个参数中就传入了 emit 的值,所以就可以理解为,我们要触发的事件,其实就在 props 中。但是问题来了,这个函数该如何获取到 instance 呢(因为 props 在 instance 中)?

export function createComponentInstance(vnode) {
  // 这里返回一个 component 结构的数据
  const component = {
    vnode,
    type: vnode.type,
    setupState: {},
    props: {},
    emit: () => {},
  }
  // 使用 bind 这个小技巧可以让 component 传入的 emit 参数中携带,同时不影响其他的参数
  component.emit = emit.bind(null, component) as any
  return component
}
export function emit(instance, event) {
  const { props } = instance
}

接下来,我们就可以从 props 中来获取到具体的 event 了。但是目前还是有一个问题的,我们 emit('add') 这里的都是全部小写的,而上层父组件监听的则是大写的 onAdd,同时加了一个 on,我们需要这样

export function emit(instance, event) {
  // 获取 props
  const { props } = instance
  const toUpperCase = (str: string) =>
    str.charAt(0).toUpperCase() + str.slice(1)
  // 将 event 第一个字母大写,同时加上 on
  const handler = props[`on${toUpperCase(event)}`]
  // 如果 props 中存在这个 handler,那么就触发这个 handler
  handler && handler()
}

现在我们就已经可以实现 emit 了。

3. 完善 emit

3.1 emit 可以传递参数

export const Foo = {
  setup(props, { emit }) {
    const handleClick = () => {
      // emit 可以传递多个参数
      emit('add', 1, 2)
    }
    return {
      handleClick,
    }
  },
}

会在父组件监听的事件上进行接收。这个也是非常简单的,

// 接收参数
export function emit(instance, event, ...params) {
  const { props } = instance
  const toUpperCase = (str: string) =>
    str.charAt(0).toUpperCase() + str.slice(1)
  const handler = props[`on${toUpperCase(event)}`]
  // 触发时传递参数
  handler && handler(...params)
}

3.2 事件名可以是短横线格式

// emit 的事件名称也可以是短横线连接的
emit('add-count', 1)
// 在监听的时候需要换成驼峰的格式
h(Foo, { onAdd: this.onAdd, onAddCount: this.onAddCount })

那么这个还如何解决呢?

// 我们在处理 event 的时候仅仅处理了全部是小写的情况
// 所以我们还需要处理一层,
export function emit(instance, event, ...params) {
  const { props } = instance
  // toUpperCase 名字可以改为 capitalize,表述的更加准确
  const toUpperCase = (str: string) =>
    str.charAt(0).toUpperCase() + str.slice(1)
  const handler = props[`on${toUpperCase(event)}`]
  // 触发时传递参数
  handler && handler(...params)
}

我们需要加一个 camelize 处理层,先将短横连接的转换为大写格式

export function emit(instance, event, ...params) {
  const { props } = instance
  // 在这里进行正则匹配,将 横杠和第一个字母 -> 不要横杠,第一个字母大写
  const camelize = (str: string) => {
    return str.replace(/-(\w)/, (_, str: string) => {
      return str.toUpperCase()
    })
  }
  const capitalize = (str: string) =>
    str ? str.charAt(0).toUpperCase() + str.slice(1) : ''
  // 在这里先处理横杠,在处理大小写
  const handler = props[`on${capitalize(camelize(event))}`]
  handler && handler(...params)
}

现在我们的 emit 就已经完善了