Getting Started with Vue 3.0 Custom Directives

Getting Started with Vue 3.0 Custom Directives

Tip: Before reading this article, it is recommended that you read the Vue 3 official documentation section on custom directives.

1. Custom instructions

1. Register global custom instructions

const app = Vue.createApp({})

// Register a global custom directive v-focus
app.directive('focus', {
  // Called when the bound element is mounted into the DOM mounted(el) {
    // Focus element el.focus()
  }
})

2. Use global custom instructions

<div id="app">
   <input v-focus />
</div>

3. Complete usage examples

<div id="app">
   <input v-focus />
</div>
<script>
   const { createApp } = Vue

      const app = Vue.createApp({}) // ①
   app.directive('focus', { // ②
      // Called when the bound element is mounted into the DOM mounted(el) {
       el.focus() // Focus element }
   })
   app.mount('#app') // ③
</script>

When the page is loaded, the input box element in the page will automatically get the focus. The code of this example is relatively simple and mainly includes three steps: creating an App object, registering global custom instructions, and mounting the application. The details of creating the App object will be introduced separately in subsequent articles. Below we will focus on analyzing the other two steps. First, let's analyze the process of registering global custom instructions.

2. The process of registering global custom instructions

In the above example, we use the directive method of the app object to register a global custom directive:

app.directive('focus', {
  // Called when the bound element is mounted into the DOM mounted(el) {
    el.focus() // Focus element }
})

Of course, in addition to registering global custom directives, we can also register local directives, because the component also accepts a directives option:

directives: {
  focus:
    mounted(el) {
      el.focus()
    }
  }
}

For the above example, we use the app.directive method defined in the runtime-core/src/apiCreateApp.ts file:

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    const context = createAppContext()
    let isMounted = false

    const app: App = (context.app = {
      // Omit some code _context: context,

            // Used to register or retrieve global directives.
      directive(name: string, directive?: Directive) {
        if (__DEV__) {
          validateDirectiveName(name)
        }
        if (!directive) {
          return context.directives[name] as any
        }
        if (__DEV__ && context.directives[name]) {
          warn(`Directive "${name}" has already been registered in target app.`)
        }
        context.directives[name] = directive
        return app
      },

    return app
  }
}

By observing the above code, we can know that the directive method supports the following two parameters:

  • name: indicates the name of the instruction;
  • directive (optional): indicates the definition of a directive.

The name parameter is relatively simple, so we focus on analyzing the directive parameter, which is of the Directive type:

// packages/runtime-core/src/directives.ts
export type Directive<T = any, V = any> =
  | ObjectDirective<T, V>
  | FunctionDirective<T, V>

From the above, we can see that the Directive type belongs to a union type, so we need to continue analyzing the ObjectDirective and FunctionDirective types. Here we first look at the definition of the ObjectDirective type:

// packages/runtime-core/src/directives.ts
export interface ObjectDirective<T = any, V = any> {
  created?: DirectiveHook<T, null, V>
  beforeMount?: DirectiveHook<T, null, V>
  mounted?: DirectiveHook<T, null, V>
  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
  updated?: DirectiveHook<T, VNode<any, T>, V>
  beforeUnmount?: DirectiveHook<T, null, V>
  unmounted?: DirectiveHook<T, null, V>
  getSSRProps?: SSRDirectiveHook
}

This type defines an object-type directive, where each property on the object represents a hook on the directive's lifecycle. The FunctionDirective type represents a function-type directive:

// packages/runtime-core/src/directives.ts
export type FunctionDirective<T = any, V = any> = DirectiveHook<T, any, V>

                              export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (
  el: T,
  binding: DirectiveBinding<V>,
  vnode: VNode<any, T>,
  prevVNode: Prev
) => void

After introducing the Directive type, let's review the previous example, I believe it will be much clearer to you:

app.directive('focus', {
  // Triggered when the bound element is mounted into the DOM mounted(el) {
    el.focus() // Focus element }
})

For the above example, when we call the app.directive method to register a custom focus directive, the following logic will be executed:

directive(name: string, directive?: Directive) {
  if (__DEV__) { // Avoid conflicts between custom directive names and existing built-in directive names validateDirectiveName(name)
  }
  if (!directive) { // Get the directive object corresponding to name return context.directives[name] as any
  }
  if (__DEV__ && context.directives[name]) {
    warn(`Directive "${name}" has already been registered in target app.`)
  }
  context.directives[name] = directive // ​​Register global directive return app
}

When the focus directive is successfully registered, the directive will be saved in the directives property of the context object, as shown in the following figure:

As the name implies, context is the context object representing the application, so how is this object created? In fact, this object is created by the createAppContext function:

const context = createAppContext()

The createAppContext function is defined in the runtime-core/src/apiCreateApp.ts file:

// packages/runtime-core/src/apiCreateApp.ts
export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      isCustomElement: NO,
      errorHandler: undefined,
      warnHandler: undefined
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}

Seeing this, do you think that the internal processing logic of registering global custom instructions is actually quite simple? So when will the registered focus command be called? To answer this question, we need to analyze another step - application mounting.

3. Application Mounting Process

In order to understand the application mounting process more intuitively, Abaoge used Chrome developer tools to record the main process of application mounting:

From the above picture, we can know the main process during application mounting. In addition, we also found a function resolveDirective related to instructions from the figure. Obviously, this function is used to parse the instruction, and this function will be called in the render method. In the source code, we found the definition of the function:

// packages/runtime-core/src/helpers/resolveAssets.ts
export function resolveDirective(name: string): Directive | undefined {
  return resolveAsset(DIRECTIVES, name)
}

Inside the resolveDirective function, the resolveAsset function will continue to be called to perform specific resolution operations. Before analyzing the specific implementation of the resolveAsset function, let's add a breakpoint inside the resolveDirective function to take a look at the "beauty" of the render method:

In the image above, we see the _resolveDirective("focus") function call associated with the focus directive. We already know that the resolveAsset function will continue to be called inside the resolveDirective function. The specific implementation of this function is as follows:

// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(
  type: typeof COMPONENTS | typeof DIRECTIVES,
  name: string,
  warnMissing = true
) {
  const instance = currentRenderingInstance || currentInstance
  if (instance) {
    const Component = instance.type
    // Omit the processing logic of the parsing component const res =
      // Local registration resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
      // Global registration resolve(instance.appContext[type], name)
    return res
  } else if (__DEV__) {
    warn(
      `resolve${capitalize(type.slice(0, -1))} ` +
        `can only be used in render() or setup().`
    )
  }
}

Because the global registration method is used when registering the focus directive, the parsing process will execute the resolve(instance.appContext[type], name) statement, where the resolve method is defined as follows:

function resolve(registry: Record<string, any> | undefined, name: string) {
  return (
    registry &&
    (registry[name] ||
      registry[camelize(name)] ||
      registry[capitalize(camelize(name))])
  )
}

After analyzing the above processing flow, we can know that when parsing globally registered instructions, the registered instruction object will be obtained from the application context object through the resolve function. After getting the _directive_focus directive object, the render method will continue to call the _withDirectives function to add the directive to the VNode object. This function is defined in the runtime-core/src/directives.ts file:

// packages/runtime-core/src/directives.ts
export function withDirectives<T extends VNode>(
  vnode: T,
  directives: DirectiveArguments
): T {
  const internalInstance = currentRenderingInstance // Get the currently rendered instance const instance = internalInstance.proxy
  const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
  for (let i = 0; i < directives.length; i++) {
    let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
    // Trigger the same behavior when mounted and updated, regardless of other hook functions if (isFunction(dir)) { // Processing function type instructions dir = {
        mounted: dir,
        updated:dir
      } as ObjectDirective
    }
    bindings.push({
      dir,
      instance,
      value,
      oldValue: void 0,
      arg,
      modifiers
    })
  }
  return vnode
}

Because multiple directives may be applied to a node, the withDirectives function defines a dirs property on the VNode object and the value of this property is an array. For the previous example, after calling the withDirectives function, a dirs property will be added to the VNode object, as shown in the following figure:

Through the above analysis, we already know that in the render method of the component, we will register the directive on the corresponding VNode object through the withDirectives function. So when will the hook defined on the focus directive be called? Before continuing the analysis, let's first introduce the hook functions supported by the instruction object.

A directive definition object can provide the following hook functions (all optional):

  • created: Called before the bound element's properties or event listeners are applied.
  • beforeMount: Called when the directive is first bound to an element and before the parent component is mounted.
  • mounted: called after the parent component of the bound element is mounted.
  • beforeUpdate: Called before the VNode containing the component is updated.
  • updated: Called after the containing component's VNode and its subcomponents' VNodes are updated.
  • beforeUnmount: Called before the parent component of the bound element is unmounted.
  • unmounted: Called only once when the directive is unbound from an element and the parent component has been unmounted.

After introducing these hook functions, let's review the ObjectDirective type introduced earlier:

// packages/runtime-core/src/directives.ts
export interface ObjectDirective<T = any, V = any> {
  created?: DirectiveHook<T, null, V>
  beforeMount?: DirectiveHook<T, null, V>
  mounted?: DirectiveHook<T, null, V>
  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
  updated?: DirectiveHook<T, VNode<any, T>, V>
  beforeUnmount?: DirectiveHook<T, null, V>
  unmounted?: DirectiveHook<T, null, V>
  getSSRProps?: SSRDirectiveHook

OK, let's analyze when the hook defined on the focus directive is called. Similarly, Abaoge adds a breakpoint in the mounted method of the focus command:

In the call stack on the right side of the figure, we see the invokeDirectiveHook function. It is obvious that the function is to call the registered hook on the instruction. Due to space considerations, I will not go into the specific details. Interested friends can debug it by themselves.

4. Brother Abao has something to say

4.1 What are the built-in directives of Vue 3?

In the process of introducing the registration of global custom instructions, we saw a validateDirectiveName function, which is used to validate the name of the custom instruction to avoid conflicts between the custom instruction name and the existing built-in instruction name.

// packages/runtime-core/src/directives.ts
export function validateDirectiveName(name: string) {
  if (isBuiltInDirective(name)) {
    warn('Do not use built-in directive ids as custom directive id: ' + name)
  }
}

Inside the validateDirectiveName function, the isBuiltInDirective(name) statement is used to determine whether it is a built-in directive:

const isBuiltInDirective = /*#__PURE__*/ makeMap(
  'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text'
)

The makeMap function in the above code is used to generate a map object (Object.create(null)) and return a function to detect whether a key exists in the map object. In addition, through the above code, we can clearly understand what built-in instructions Vue 3 provides us.

4.2 How many types of instructions are there?

In Vue 3, directives are divided into two types: ObjectDirective and FunctionDirective:

// packages/runtime-core/src/directives.ts
export type Directive<T = any, V = any> =
  | ObjectDirective<T, V>
  | FunctionDirective<T, V>

ObjectDirective

export interface ObjectDirective<T = any, V = any> {
  created?: DirectiveHook<T, null, V>
  beforeMount?: DirectiveHook<T, null, V>
  mounted?: DirectiveHook<T, null, V>
  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
  updated?: DirectiveHook<T, VNode<any, T>, V>
  beforeUnmount?: DirectiveHook<T, null, V>
  unmounted?: DirectiveHook<T, null, V>
  getSSRProps?: SSRDirectiveHook
}

FunctionDirective

export type FunctionDirective<T = any, V = any> = DirectiveHook<T, any, V>

                              export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (
  el: T,
  binding: DirectiveBinding<V>,
  vnode: VNode<any, T>,
  prevVNode: Prev
) => void

If you want to trigger the same behavior when mounted and updated, and don't care about other hook functions. Then you can do it by passing a callback function to the directive

app.directive('pin', (el, binding) => {
  el.style.position = 'fixed'
  const s = binding.arg || 'top'
  el.style[s] = binding.value + 'px'
})

4.3 What is the difference between registering global instructions and local instructions?

Registering global directives

app.directive('focus', {
  // Called when the bound element is mounted into the DOM mounted(el) {
    el.focus() // Focus element }
});

Registering Local Directives

const Component = defineComponent({
  directives: {
    focus:
      mounted(el) {
        el.focus()
      }
    }
  },
  render() {
    const { directives } = this.$options;
    return [withDirectives(h('input'), [[directives.focus, ]])]
  }
});

Parsing global and local registration instructions

// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(
  type: typeof COMPONENTS | typeof DIRECTIVES,
  name: string,
  warnMissing = true
) {
  const instance = currentRenderingInstance || currentInstance
  if (instance) {
    const Component = instance.type
    // Omit the processing logic of the parsing component const res =
      // Local registration resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
      // Global registration resolve(instance.appContext[type], name)
    return res
  }
}

4.4 What is the difference between the rendering functions generated by built-in instructions and custom instructions?

To understand the difference between the rendering functions generated by built-in instructions and custom instructions, Abaoge takes the v-if, v-show built-in instructions and the v-focus custom instruction as examples, and then uses the Vue 3 Template Explorer online tool to compile and generate rendering functions:

v-if built-in directive

<input v-if="isShow" />

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock,
       createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue

    return isShow
      ? (_openBlock(), _createBlock("input", { key: 0 }))
      : _createCommentVNode("v-if", true)
  }
}

For the v-if instruction, after compilation, the ?: ternary operator is used to implement the function of dynamically creating nodes.

v-show built-in directive

<input v-show="isShow" />

  const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives,
       openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return _withDirectives((_openBlock(), _createBlock("input", null, null, 512 /* NEED_PATCH */)), [
      [_vShow, isShow]
    ])
  }
}

The vShow directive in the above example is defined in the packages/runtime-dom/src/directives/vShow.ts file. This directive is of the ObjectDirective type and defines four hooks: beforeMount, mounted, updated, and beforeUnmount.

v-focus custom directive

<input v-focus />

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { resolveDirective: _resolveDirective, createVNode: _createVNode,
       withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _directive_focus = _resolveDirective("focus")
    return _withDirectives((_openBlock(), _createBlock("input", null, null, 512 /* NEED_PATCH */)), [
      [_directive_focus]
    ])
  }
}

By comparing the rendering functions generated by the v-focus and v-show instructions, we can see that both the v-focus custom instruction and the v-show built-in instruction will register the instruction to the VNode object through the withDirectives function. Compared with built-in instructions, custom instructions have an additional instruction parsing process.

Additionally, if both the v-show and v-focus directives are applied on the input element, a two-dimensional array will be used when calling the _withDirectives function:

<input v-show="isShow" v-focus />

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { vShow: _vShow, resolveDirective: _resolveDirective, createVNode: _createVNode,
       withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _directive_focus = _resolveDirective("focus")
    return _withDirectives((_openBlock(), _createBlock("input", null, null, 512 /* NEED_PATCH */)), [
      [_vShow, isShow],
      [_directive_focus]
    ])
  }
}

4.5 How to apply directives in rendering functions?

In addition to applying directives in templates, we can easily apply specified directives in rendering functions using the withDirectives function introduced earlier:

<div id="app"></div>
<script>
   const { createApp, h, vShow, defineComponent, withDirectives } = Vue
   const Component = defineComponent({
     data() {
       return { value: true }
     },
     render() {
       return [withDirectives(h('div', 'I am Brother Abao'), [[vShow, this.value]])]
     }
   });
   const app = Vue.createApp(Component)
   app.mount('#app')
</script>

In this article, Brother Abao mainly introduces how to customize instructions and how to register global and local instructions in Vue 3. In order to enable everyone to have a deeper understanding of the relevant knowledge of custom instructions, Brother Abao analyzed the registration and application process of instructions from the perspective of source code.

In subsequent articles, Brother Abao will introduce some special instructions, and of course will focus on analyzing the principle of two-way binding. Don’t miss it if you are interested.

The above is the detailed introduction to the use of Vue 3.0 custom instructions. For more information on the use of Vue 3.0 custom instructions, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • Detailed explanation of Vue custom instructions and their use
  • How to build a drag and drop plugin using vue custom directives
  • Detailed explanation of custom instructions for Vue.js source code analysis
  • Vue custom v-has instruction to implement button permission judgment
  • Vue basic instructions example graphic explanation
  • Summary of Vue 3 custom directive development
  • Vue3.0 custom instructions (drectives) knowledge summary
  • 8 very practical Vue custom instructions
  • Detailed explanation of custom instructions in Vue
  • Analysis of the implementation principle of Vue instructions

<<:  .NETCore Docker implements containerization and private image repository management

>>:  Detailed explanation of how MySQL determines whether an InnoDB table is an independent tablespace or a shared tablespace

Recommend

Summary of common optimization operations of MySQL database (experience sharing)

Preface For a data-centric application, the quali...

Detailed explanation of the concept, principle and usage of MySQL triggers

This article uses examples to explain the concept...

Tomcat8 uses cronolog to split Catalina.Out logs

background If the catalina.out log file generated...

Usage of the target attribute of the html tag a

1: If you use the tag <a> to link to a page,...

Problems with index and FROM_UNIXTIME in mysql

Zero, Background I received a lot of alerts this ...

Tutorial on using Docker Compose to build Confluence

This article uses the "Attribution 4.0 Inter...

MySQL 5.7.17 installation and configuration method graphic tutorial under win7

I would like to share with you the graphic tutori...

Docker+nacos+seata1.3.0 installation and usage configuration tutorial

I spent a day on it before this. Although Seata i...

Use Grafana+Prometheus to monitor MySQL service performance

Prometheus (also called Prometheus) official webs...

Answers to several high-frequency MySQL interview questions

Preface: In interviews for various technical posi...

Mysql solution to improve the efficiency of copying large data tables

Preface This article mainly introduces the releva...

Html Select uses the selected attribute to set the default selection

Adding the attribute selected = "selected&quo...

How to handle spaces in CSS

1. Space rules Whitespace within HTML code is usu...

Solution to the problem that Docker container cannot be stopped or killed

Docker version 1.13.1 Problem Process A MySQL con...