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