Embedding of functions is a common requirement, previously done by parsing inserts through Babel or manually adding

In a Babel group the other day someone asked: How do I insert a function for every click event in Vue?

Add additional functions by modifying the source code

If the company has its own modified Vue framework, it is quite convenient. The source version is: 3.2.20, located at runtime – dom/SRC/modules/events. Ts:

export function patchEvent(el: Element & { _vei? : Record<string, Invoker | undefined> },
  rawName: string,
  prevValue: EventValue | null,
  nextValue: EventValue | null,
  instance: ComponentInternalInstance | null = null
) {
  const invokers = el._vei || (el._vei = {})
  const existingInvoker = invokers[rawName]
  
  //TODO:Add extra functions

  if (nextValue && existingInvoker) {
    // Update the listener function
    existingInvoker.value = nextValue
  } else {
    const [name, options] = parseName(rawName)
    if (nextValue) {
      // Add a listener
      const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
      addEventListener(el, name, invoker, options)
    } else if (existingInvoker) {
      // Remove the listener
      removeEventListener(el, name, existingInvoker, options)
      invokers[rawName] = undefined}}}Copy the code

In the patchEvent method, createInvoker encapsulates a layer of listener functions to perform additional tasks, which are then added to the element’s event callback queue:

function createInvoker(
  initialValue: EventValue,
  instance: ComponentInternalInstance | null
) {
  const invoker: Invoker = (e: Event) = > {
    const timeStamp = e.timeStamp || _getNow()

    if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
      callWithAsyncErrorHandling(
        patchStopImmediatePropagation(e, invoker.value),
        instance,
        ErrorCodes.NATIVE_EVENT_HANDLER,
        [e]
      )
    }
  }
  invoker.value = initialValue
  invoker.attached = getNow()
  return invoker
}
Copy the code

PatchStopImmediatePropagation role is: in the invoker. The value is an array of array, generate new monitoring function check e. _stopped decided whether to perform monitoring functions

Queue callWithAsyncErrorHandling is used to perform functions, the parameters of the function of the fourth parameter for incoming deconstruction

Realize the function

function createInvokerValueWithSecretFunction (rawName: string, v: EventValue | null) {
  if(! v)return v

  const targetName = 'click'

  const [name] = parseName(rawName)
  const newValue: EventValue = isArray(v) ? v : [v]

  if (name === targetName) {
    newValue.unshift(insertFunction)
  }

  return newValue

  function insertFunction (e: Event) {
    console.log('Hello Click')}}/** * completes the previous TODO * - //TODO:Add additional functions * + nextValue = createInvokerValueWithSecretFunction (rawName, nextValue) * /
Copy the code

Additional functions are added by way of a Vue plug-in

The purpose is to add additional conversions to the render function, changing @click=”[insertFn, fn]” to @click=”[insertFn, fn]”. The custom renderer component does not support the addition of additional functions because it is converted when the render function is generated

Add additional compiler conversion functions to modify the instance configuration directly. Consider whether to write it as a separate Vue plug-in or as a Babel plug-in

Run-time core/ SRC /component.ts:

export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean, skipOptions? :boolean
) {
  / *... * /
  if(! instance.render) {if(! isSSR && compile && ! Component.render) {/ *... * /
      if (template) {
        / *... * /
        const { isCustomElement, compilerOptions } = instance.appContext.config
        const { delimiters, compilerOptions: componentCompilerOptions } =
          Component
        const finalCompilerOptions: CompilerOptions = extend(
          extend(
            {
              isCustomElement,
              delimiters
            },
            compilerOptions
          ),
          componentCompilerOptions
        )
        / *... * /
        Component.render = compile(template, finalCompilerOptions)
      }
    }
    / *... * /
  }
  
  / *... * /
}
Copy the code

In front of the generated rendering functions, from the instance. The appContext. Config the Vue instance in the global context of configuration for compiling compilerOptions configuration object, from the current component configuration object access to compile compilerOptions configuration object

Located at runtime – core/SRC/compile. Ts:

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
) :CodegenResult {
  / *... * /
  const ast = isString(template) ? baseParse(template, options) : template
  / *... * /
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms].directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms)}))return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}
Copy the code

After generating the abstract syntax tree, transform at the source level, and you can see that the options passed in by the user are merged: Options. NodeTransforms and options. DirectiveTransforms, the difference between the two is that the former is for all the abstract syntax tree of each node, which is only for the built-in commands

Realize the function

// It can be modified by itself
type Options = {
  event: stringfn? :(e: Event) = > void
}
function nodeTransformPluginForInsertFunction (app:App, options: Options = { event: 'click' }) {
  const { event, fn } = options

  if (typeoffn ! = ='undefined' && typeoffn ! = ='function') {
    console.warn(/ * warning * /)
    return
  }

  // globally add unified functions for rendering function fetching
  const globalInsertFnName = '$__insertFunction__'
  app.config.globalProperties[globalInsertFnName] = fn || defaultInsertFunction

  const transformEventOfElement: NodeTransform = (node, context) = > {
    if (node.type === NodeTypes.ELEMENT / * 1 * / && node.props && node.props.length > 0) {
      for (let i = 0; i < node.props.length; i++) {
        const prop = node.props[i]

        if (
          / * * / instruction
          prop.type === NodeTypes.DIRECTIVE / * * / 7 && prop.name === 'on'
          /* Qualified instruction */
          && prop.arg && prop.arg.type === NodeTypes.SIMPLE_EXPRESSION / * * / 4 && prop.arg.content === event
          /* Make sure exp exists */
          && prop.exp && prop.exp.type === NodeTypes.SIMPLE_EXPRESSION / * * / 4 && prop.exp.content.trim()
        ) {
          let trimmedContent = prop.exp.content.trim()

          / / will be similar to ` @ click = "fn" ` modified to ` @ click = "[insertFn, fn] `"
          // There are other cases to consider, which are dealt with briefly here
          if (trimmedContent[0= = ='[') {
            trimmedContent = ` [${globalInsertFnName}.${trimmedContent.substr(1)}`
          } else {
            trimmedContent = ` [${globalInsertFnName}.${trimmedContent}] `
          }

          prop.exp.content = trimmedContent
        }

      }
    }
  }

  // Add the compiler conversion function
  const nodeTransforms: NodeTransform[] =
    (app.config.compilerOptions as any).nodeTransforms|| ((app.config.compilerOptions as any).nodeTransforms = [])
  nodeTransforms.push(transformEventOfElement)

  function defaultInsertFunction (e: Event) {}}Copy the code

There are many details that can be refined to make your own functions more robust by referring to the built-in conversion functions

Add extra functions manually

💪 where to write where