The new editor for Nuggets is out (666), and diggers are ready to try it.

For those interested in Vue 3 bidirectional binding, read this article about how Vue 3 bidirectional binding works. In this article, We will take you step by step to explore what happened after $emit with a simple example. By the end of this article, you will understand the principles behind Vue 3 custom events.

<div id="app"></div>
<script>
   const app = Vue.createApp({
     template: '<welcome-button v-on:welcome="sayHi"></welcome-button>'.methods: {
       sayHi() {
         console.log('Hello, I'm Po Brother! ');
       }
     }
    })

   app.component('welcome-button', {
     emits: ['welcome'].template: ` < button v - on: click = "$emit (' welcome ')" > welcome to < / button > `
    })
    app.mount("#app")
</script>
Copy the code

In the example above, we create the app object with the vue. createApp method and then register the global component, the Welcome-button component, with the Component method on the object. When we define the component, we define custom events on the component through the emits property. Of course, when the user clicks the welcome button, the welcome event is emitted, the sayHi method is called, and the console prints hello, I’m Po! .

Pay attention to “the road of full stack Repair fairy” read 4 free e-books of Po Ge original (total download 30,000 +) and 9 advanced series of Vue 3 tutorials.

Although this example is relatively simple, there are two problems:

  • $emitWhere do methods come from?
  • What is the process of custom events?

Let’s take a closer look at the mechanics behind custom events around these questions, starting with the first one.

Where does the $emit method come from?

Using Chrome Developer Tools, we add a breakpoint inside the sayHi method and click the welcome button. The function call stack looks like this:

In the call stack on the right side of the figure above, we find an EMIT method in the componentEmits. Ts file. In the template, however, we use the $emit method. To clarify this, let’s look at the onClick method:

Our $emit method comes from the _ctx object. What is that object? Again, using breakpoints we can see the internal structure of the _ctx object:

Obviously the _ctx object is a Proxy object. If you don’t know about Proxy objects, you can read the article on Proxy that you don’t know about. When accessing the $emit property of the _CTx object, we will enter the GET trap, so let’s analyze the GET trap:

With the [[FunctionLocation]] attribute, we find the definition of the GET trap as follows:

// packages/runtime-core/src/componentPublicInstance.ts
export const RuntimeCompiledPublicInstanceProxyHandlers = extend(
  {},
  PublicInstanceProxyHandlers,
  {
    get(target: ComponentRenderContext, key: string) {
      // fast path for unscopables when using `with` block
      if ((key as any) = = =Symbol.unscopables) {
        return
      }
      returnPublicInstanceProxyHandlers.get! (target, key, target) },has(_: ComponentRenderContext, key: string) {
      const has = key[0]! = ='_' && !isGloballyWhitelisted(key)
      // Omit some code
      return has
    }
  }
)
Copy the code

Observe the above code, the capture device in the get inside will continue to call PublicInstanceProxyHandlers object the get method to obtain the key corresponding to the value. PublicInstanceProxyHandlers due to the internal code is relatively complex, here we only analysis related to sample code:

// packages/runtime-core/src/componentPublicInstance.ts
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const { ctx, setupState, data, props, accessCache, type, appContext } = instance
   
    // omit most of the content
    const publicGetter = publicPropertiesMap[key]
    // public $xxx properties
    if (publicGetter) {
      if (key === '$attrs') {
        track(instance, TrackOpTypes.GET, key)
        __DEV__ && markAttrsAccessed()
      }
      return publicGetter(instance)
    },
    // omit the set and HAS traps
}
Copy the code

In the code above, we see the publicPropertiesMap object, the object is defined in componentPublicInstance. Ts file:

// packages/runtime-core/src/componentPublicInstance.ts
const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), {
  $: i= > i,
  $el: i= > i.vnode.el,
  $data: i= > i.data,
  $props: i= > (__DEV__ ? shallowReadonly(i.props) : i.props),
  $attrs: i= > (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
  $slots: i= > (__DEV__ ? shallowReadonly(i.slots) : i.slots),
  $refs: i= > (__DEV__ ? shallowReadonly(i.refs) : i.refs),
  $parent: i= > getPublicInstance(i.parent),
  $root: i= > getPublicInstance(i.root),
  $emit: i= > i.emit,
  $options: i= > (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
  $forceUpdate: i= > () = > queueJob(i.update),
  $nextTick: i= >nextTick.bind(i.proxy!) .$watch: i= > (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
} as PublicPropertiesMap)
Copy the code

In the publicPropertiesMap object, we found the $emit property with a value of $emit: I => I.E MIT, that is, $emit points to the emit property of the parameter I object. Let’s look at what the target object looks like when the $emit attribute is obtained:

The target object has an _ attribute. The value of this attribute is an object that contains vNode, type, and parent attributes. So we assume that the value of the _ attribute is the component instance. To verify this, using Chrome developer tools, we can easily analyze which functions are called during component mounting:

In the figure above, we see that the createComponentInstance function is called during the component mount phase. As the name implies, this function is used to create a component instance, which is implemented as follows:

// packages/runtime-core/src/component.ts
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  const type = vnode.type as ConcreteComponent
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    appContext,
    // omit most of the attributes
    emit: null as any.emitted: null,}if (__DEV__) { // Development mode
    instance.ctx = createRenderContext(instance)
  } else { // Production mode
    instance.ctx = { _: instance }
  }
  instance.root = parent ? parent.root : instance
  instance.emit = emit.bind(null, instance)

  return instance
}
Copy the code

In the above code, in addition to finding the instance object, we also see the instance.emit = emit. Bind (null, instance) statement. This is where we find out where the $emit method comes from. With the first problem out of the way, let’s look at the flow of custom events.

Second, what is the process of custom events?

Find out why the sayHi method is automatically called after you click the welcome button to send out the Welcome event. We must emit function analysis of the internal processing logic, the function is defined in the runtime – core/SRC/componentEmits. T file:

// packages/runtime-core/src/componentEmits.ts
export function emit(
  instance: ComponentInternalInstance,
  event: string. rawArgs:any[]
) {
  const props = instance.vnode.props || EMPTY_OBJ
	// Omit most of the code
  let args = rawArgs

  // convert handler name to camelCase. See issue #2249
  let handlerName = toHandlerKey(camelize(event))
  let handler = props[handlerName]

  if (handler) {
    callWithAsyncErrorHandling(
      handler,
      instance,
      ErrorCodes.COMPONENT_EVENT_HANDLER,
      args
    )
  }
}
Copy the code

In fact, v-Model update: XXX event processing is also involved in the EMIT function. There will be a separate article about the internal principle of v-model instruction. Here we examine only the processing logic relevant to the current example.

In the EMIT function, the toHandlerKey function is used to convert the event name to a humped handlerName:

// packages/shared/src/index.ts
export const toHandlerKey = cacheStringFunction(
  (str: string) = > (str ? `on${capitalize(str)}` : ` `))Copy the code

After the handlerName is retrieved, the handler object corresponding to the handlerName is retrieved from the Props object. If the handler object exists, will call callWithAsyncErrorHandling function, to perform the current custom events corresponds to the event handler. CallWithAsyncErrorHandling function are defined as follows:

// packages/runtime-core/src/errorHandling.ts
export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null.type: ErrorTypes, args? : unknown[]) :any[] {
  if (isFunction(fn)) {
    const res = callWithErrorHandling(fn, instance, type, args)
    if (res && isPromise(res)) {
      res.catch(err= > {
        handleError(err, instance, type)})}return res
  }

  // Handle multiple event handlers
  const values = []
  for (let i = 0; i < fn.length; i++) {
    values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
  }
  return values
}
Copy the code

Code by above knowable, if fn parameter is a function object, will continue to call callWithErrorHandling function within callWithAsyncErrorHandling function to eventually implement the event handler:

// packages/runtime-core/src/errorHandling.ts
export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null.type: ErrorTypes, args? : unknown[]) {
  let res
  try{ res = args ? fn(... args) : fn() }catch (err) {
    handleError(err, instance, type)}return res
}
Copy the code

Inside the callWithErrorHandling function, try catch statements are used to catch exceptions and handle them. If a Promise object is returned after the FN event handler is called, the catch method on the Promise object is used to handle the exception. With that in mind, take a look at the function call stack you’ve seen before, and you’ll be familiar with it.

Now we have the answers to both of these questions. To get a better handle on custom events, We’ll use Vue 3 Template Explorer, an online tool, to analyze the results of Template compilation in our example:

App Component Template

<welcome-button v-on:welcome="sayHi"></welcome-button>

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue
    const _component_welcome_button = _resolveComponent("welcome-button")

    return (_openBlock(), _createBlock(_component_welcome_button,
     { onWelcome: sayHi }, null.8 /* PROPS */["onWelcome"))}}Copy the code

Welcome -button component template

<button v-on:click="$emit('welcome')"> welcome to < / button >const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock,
      createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock("button", {
      onClick: $event => ($emit('welcome'))},"Welcome".8 /* PROPS */["onClick"))}}Copy the code

Looking at the above results, we know that events bound by V-ON: are converted to properties that start with ON, such as onWelcome and onClick. Why convert to this form? This is because the event name is converted inside the emit function via the toHandlerKey and Camelize functions:

// packages/runtime-core/src/componentEmits.ts
export function emit(
  instance: ComponentInternalInstance,
  event: string. rawArgs:any[]
) {
	// Omit most of the code
  // convert handler name to camelCase. See issue #2249
  let handlerName = toHandlerKey(camelize(event))
  let handler = props[handlerName]
}
Copy the code

To clarify the conversion rules, let’s first look at the camelize function:

// packages/shared/src/index.ts
const camelizeRE = /-(\w)/g

export const camelize = cacheStringFunction(
  (str: string) :string= > {
    return str.replace(camelizeRE, (_, c) = > (c ? c.toUpperCase() : ' '))})Copy the code

Looking at the above code, we can see that the camelize function is used to convert kebab-case event names to camelCase event names. For example, the event name “test-event” will be converted to “testEvent” after being processed by the Camelize function. The result of this transformation is further processed by the toHandlerKey function, which is defined in shared/ SRC /index.ts:

// packages/shared/src/index.ts
export const toHandlerKey = cacheStringFunction(
  (str: string) = > (str ? `on${capitalize(str)}` : ` `))export const capitalize = cacheStringFunction(
  (str: string) = > str.charAt(0).toUpperCase() + str.slice(1))Copy the code

The previously used “testEvent” event name is processed by the toHandlerKey function and will eventually be converted to the form “onTestEvent”. To get a better idea of the legal form of an event listener, let’s look at a test case in the Run-time Core module:

// packages/runtime-core/__tests__/componentEmits.spec.ts
test('isEmitListener'.() = > {
  const options = {
    click: null.'test-event': null.fooBar: null.FooBaz: null
  }
  expect(isEmitListener(options, 'onClick')).toBe(true)
  expect(isEmitListener(options, 'onclick')).toBe(false)
  expect(isEmitListener(options, 'onBlick')).toBe(false)
  // .once listeners
  expect(isEmitListener(options, 'onClickOnce')).toBe(true)
  expect(isEmitListener(options, 'onclickOnce')).toBe(false)
  // kebab-case option
  expect(isEmitListener(options, 'onTestEvent')).toBe(true)
  // camelCase option
  expect(isEmitListener(options, 'onFooBar')).toBe(true)
  // PascalCase option
  expect(isEmitListener(options, 'onFooBaz')).toBe(true)})Copy the code

Now that we know about the legal form of event listeners, let’s look at the cacheStringFunction function:

// packages/shared/src/index.ts
const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
  const cache: Record<string, string> = Object.create(null)
  return ((str: string) => {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }) as any
}
Copy the code

The above code is also relatively simple; the cacheStringFunction function is used for caching.

Pay attention to “the road of full stack Repair fairy” read 4 free e-books of Po Ge original (total download 30,000 +) and 9 advanced series of Vue 3 tutorials.

Third, Po Ge has something to say

3.1 How do I bind events in rendering functions?

In the previous example, we did event binding with the V-ON directive, so how do we bind events in the rendering function?

<div id="app"></div>
<script>
  const { createApp, defineComponent, h } = Vue
  
  const Foo = defineComponent({
    emits: ["foo"].render() { return h("h3"."Vue 3 Custom Events")},
    created() {
      this.$emit('foo'); }});const onFoo = () = > {
    console.log("foo be called")};const Comp = () = > h(Foo, { onFoo })
  const app = createApp(Comp);
  app.mount("#app")
</script>
Copy the code

In the above example, we defined the Foo component through the defineComponent global API, and then created the functional component Comp through the H function. When we created the Comp component, we implemented the binding action of the custom event by setting the onFoo attribute.

3.2 How do I execute event handlers only once?

Set in the template
<welcome-button v-on:welcome.once="sayHi"></welcome-button>

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue
    const _component_welcome_button = _resolveComponent("welcome-button")

    return (_openBlock(), _createBlock(_component_welcome_button, 
      { onWelcomeOnce: sayHi }, null.8 /* PROPS */["onWelcomeOnce"))}}Copy the code

In the code above, we use the once event modifier to execute the event handler only once. In addition to the once modifier, there are other modifiers such as:

<! -- Prevent the click event from propagating -->
<a @click.stop="doThis"></a>

<! Submit events no longer reload the page -->
<form @submit.prevent="onSubmit"></form>

<! -- modifiers can be concatenated -->
<a @click.stop.prevent="doThat"></a>

<! -- only modifiers -->
<form @submit.prevent></form>

<! Add event listener with event capture mode
<! Events triggered by an inner element are processed here before they are processed by the inner element.
<div @click.capture="doThis">.</div>

<! Trigger handler only if event.target is the current element itself -->
<! -- that is, events are not triggered from internal elements -->
<div @click.self="doThat">.</div>
Copy the code
Set in the render function
<div id="app"></div>
<script>
   const { createApp, defineComponent, h } = Vue
   const Foo = defineComponent({
     emits: ["foo"].render() { return h("h3"."Vue 3 Custom Events")},
     created() {
       this.$emit('foo');
       this.$emit('foo'); }});const onFoo = () = > {
     console.log("foo be called")};// Add Once after the event name to indicate that the event handler executes only Once
   const Comp = () = > h(Foo, { onFooOnce: onFoo })
   const app = createApp(Comp);
   app.mount("#app")
</script>
Copy the code

Both methods work because the directive V-on :welcome.once in the template, which is translated to onWelcomeOnce upon compilation, defines a rule for handling the once modifier in the emit function:

// packages/runtime-core/src/componentEmits.ts
export function emit(
  instance: ComponentInternalInstance,
  event: string. rawArgs:any[]
) {
  const props = instance.vnode.props || EMPTY_OBJ

  const onceHandler = props[handlerName + `Once`]
  if (onceHandler) {
    if(! instance.emitted) { ; (instance.emitted = {}as Record<string.boolean>)[handlerName] = true
    } else if (instance.emitted[handlerName]) {
      return
    }
    callWithAsyncErrorHandling(
      onceHandler,
      instance,
      ErrorCodes.COMPONENT_EVENT_HANDLER,
      args
    )
  }
}
Copy the code

3.3 How Can I Add Multiple Event Handlers

Set in the template
<div @click="foo(), bar()"/>
  
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock, 
      createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", {
      onClick: $event => (foo(), bar())
    }, null.8 /* PROPS */["onClick"))}}Copy the code
Set in the render function
<div id="app"></div>
<script>
   const { createApp, defineComponent, h } = Vue
   const Foo = defineComponent({
     emits: ["foo"].render() { return h("h3"."Vue 3 Custom Events")},
     created() {
       this.$emit('foo'); }});const onFoo = () = > {
     console.log("foo be called")};const onBar = () = > {
     console.log("bar be called")};const Comp = () = > h(Foo, { onFoo: [onFoo, onBar] })
   const app = createApp(Comp);
  app.mount("#app")
</script>
Copy the code

Is the cause of the above way will take effect, in the previous introduction callWithAsyncErrorHandling function contains more than one event handler processing logic:

// packages/runtime-core/src/errorHandling.ts
export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null.type: ErrorTypes, args? : unknown[]) :any[] {
  if (isFunction(fn)) {
   // Omit some code
  }

  const values = []
  for (let i = 0; i < fn.length; i++) {
    values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
  }
  return values
}
Copy the code

3.4 the Vue 3$emitWith the Vue 2$emitWhat’s the difference?

In Vue 2, the $emit method is a property on the vue. prototype object, while $emit on Vue 3 is a property on the component instance, instance.emit = emit. Bind (null, instance).

// src/core/instance/events.js
export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/

  // omit the definitions of $on, $once, and $off methods
  // Vue instance is an EventBus object
  Vue.prototype.$emit = function (event: string) :Component {
    const vm: Component = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments.1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}
Copy the code

This article introduces the secrets behind custom events in Vue 3. In order to enable you to have a deeper understanding of the knowledge of custom events, We analyze the source of $emit method and the process of custom events from the perspective of source code.

Vue 3.0 advanced series of articles is still updated, currently updated to the ninth, want to learn Vue 3.0 partners can add a Pokemon wechat – Semlinker.

Iv. Reference resources

  • Vue 3 official website – Event handling
  • Vue 3 official website – Custom events
  • Vue 3 官网 – global API