In Vue projects, we often come across built-in directives such as V-if, V-show, V-for, or V-Model, which provide us with different capabilities. In addition to using these built-in directives, Vue also allows you to register custom directives. Next, We’ll take a step closer to unlocking the secrets behind custom directives using the examples used in the custom directives section of Vue 3 official documentation.
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.
1. Custom instructions
1. Register global custom directives
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 elements
el.focus()
}
})
Copy the code
2. Use global custom directives
<div id="app">
<input v-focus />
</div>
Copy the code
3. Complete usage examples
<div id="app">
<input v-focus />
</div>
<script>
const { createApp } = Vue
const app = Vue.createApp({}) / / 1.
app.directive('focus', { / / 2.
// called when the bound element is mounted into the DOM
mounted(el) {
el.focus() // Focus elements
}
})
app.mount('#app') / / 3.
</script>
Copy the code
When the page is loaded, the input box element in the page automatically gets focus. The code for this example is relatively simple and consists of three steps: creating App objects, registering global custom directives, and mounting the application. The details of creating App objects will be covered separately in a future article, but we will focus on the other two steps below. First let’s analyze the process of registering a global custom instruction.
Second, the process of registering global custom instructions
In the example above, 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 elements}})Copy the code
Of course, in addition to registering global custom directives, we can also register local directives because the component also accepts a caching option:
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
Copy the code
For the example above, the app.directive method we use is defined in the run-time 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
}
}
Copy the code
Looking at the code above, we can see that the directive method supports two parameters:
- Name: indicates the command name.
- Directive (Optional) : Indicates the definition of a directive.
The name parameter is relatively simple, so we focus on the directive parameter, which is of type directive:
// packages/runtime-core/src/directives.ts
export type Directive<T = any, V = any> =
| ObjectDirective<T, V>
| FunctionDirective<T, V>
Copy the code
It can be seen from the above that the type of Directive belongs to the union type, so we need to continue to analyze the types of ObjectDirective and FunctionDirective. The ObjectDirective type is defined as follows:
// packages/runtime-core/src/directives.ts
export interfaceObjectDirective<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 }Copy the code
This type defines an instruction for an object type, and each property on the object represents a hook on the instruction’s lifecycle. The FunctionDirective type represents a directive of the function type:
// 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
Copy the code
After introducing the Directive type, review the previous example and you’ll be much clearer:
app.directive('focus', {
Emitted when the bound element is mounted into the DOM
mounted(el) {
el.focus() // Focus elements}})Copy the code
For the example above, when we call the app.directive method to register a custom focus directive, the following logic is executed:
directive(name: string, directive? : Directive) {
if (__DEV__) { // Avoid custom directive names that conflict with existing built-in directive names
validateDirectiveName(name)
}
if(! directive) {// Get the instruction 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 the global directive
return app
}
Copy the code
When the Focus directive is registered successfully, it is stored in the Directives property of the Context object, as shown in the figure below:
As the name implies, context is the context object that represents the application, so how is this object created? The createAppContext function is used to create this object:
const context = createAppContext()
Copy the code
The createAppContext function is defined in runtime-core/ SRC/apicreateapp. ts:
// 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)}}Copy the code
Registering internal processing logic for global custom directives is actually quite simple. When will the registered focus directive be called? To answer this question, we need to analyze another step — application mount.
Apply the process of mounting
In order to have a more intuitive understanding of the application mounting process, Po Ge used Chrome developer Tool to record the main application mounting process:
From the above figure, we can see the main process that goes through during the application mount. In addition, we can also find a function related to instruction, resolveDirective, from the figure. This function is obviously used to parse instructions and is called in the Render method. In the source code, we find the definition of this function:
// packages/runtime-core/src/helpers/resolveAssets.ts
const DIRECTIVES = 'directives'
export function resolveDirective(name: string) :Directive | undefined {
return resolveAsset(DIRECTIVES, name)
}
Copy the code
From the above code, we can see that inside the resolveDirective function, the resolveAsset function will continue to be called to perform specific resolution operations. Before we look at the implementation of the resolveAsset function, let’s add a breakpoint inside the resolveDirective function to see what the render method looks like:
In the figure above, we see the _resolveDirective(“focus”) function call associated with the focus directive. We already know that the resolveAsset function will be called inside the resolveDirective function, which is implemented 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 parsing component's processing logic
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().`)}}Copy the code
Resolve (instance. AppContext [type], name); resolve(instance. AppContext [type], name);
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolve(registry: Record<string.any> | undefined, name: string) {
return (
registry &&
(registry[name] ||
registry[camelize(name)] ||
registry[capitalize(camelize(name))])
)
}
Copy the code
The resolve function is used to retrieve the registered directive object from the application context object when resolving globally registered directives. After obtaining the _directive_FOCUS directive object, the _withDirectives function continues to be called internally by the render method to add the directive to the VNode object, This function is defined in the Run-time core/ SRC/cache. 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]
// When mounted and updated, fires the same behavior regardless of other hook functions
if (isFunction(dir)) { // Handle function type instructions
dir = {
mounted: dir,
updated: dir
} as ObjectDirective
}
bindings.push({
dir,
instance,
value,
oldValue: void 0,
arg,
modifiers
})
}
return vnode
}
Copy the code
Since more than one directive may be applied to a node, the withDirectives function defines a DIRS property on the VNode object and the value of the dirS property is an array. For the previous example, after the withDirectives function is called, a dirS property is added to the VNode object, as shown in the following figure:
From the above analysis we already know that in the Render method of the component we register the directives on the corresponding VNode object using the withDirectives function. When are the hooks defined in the focus directive called? Before continuing, let’s look at the hook functions supported by the instruction object.
An instruction definition object can provide the following hook functions (all optional) :
-
Created: called before an attribute or event listener of a bound element is applied.
-
BeforeMount: Called when the directive is first bound to an element and before the parent component is mounted.
-
Mounted: Is invoked after the parent component of the bound element is mounted.
-
BeforeUpdate: Called before updating a VNode that contains components.
-
Updated: Called after the VNode that contains the component and its children are updated.
-
BeforeUnmount: Called before unmounting the parent component of a bound element.
-
Unmounted: This command is invoked only once when it is unbound from an element and the parent component is unmounted.
With these hook functions in place, let’s review the ObjectDirective type we introduced earlier:
// packages/runtime-core/src/directives.ts
export interfaceObjectDirective<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 }Copy the code
Ok, let’s analyze when the hook defined on the focus directive is called. Mounted focus: / / add a breakpoint to focus mounted:
In the call stack on the right, we see the invokeDirectiveHook function, which is clearly used to invoke the hooks registered on the instruction. For the length of consideration, the specific details of the Po brother will not continue to introduce, interested partners can be their own breakpoint debugging.
Four, Po Ge has something to say
4.1 What are the built-in commands of Vue 3?
In the introduction to registering global custom directives, we saw a validateDirectiveName function that validates the name of a custom directive to avoid conflicts with existing built-in directive names.
// 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)
}
}
Copy the code
Inside the validateDirectiveName function, 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'
)
Copy the code
The makeMap function in the above code generates a map Object (object.create (null)) and returns a function that checks whether a key exists in the map Object. In addition, the above code gives us a good idea of what the built-in instructions are for us in Vue 3.
4.2 How many types of instructions are there?
In Vue 3, directives are divided into ObjectDirective and FunctionDirective:
// packages/runtime-core/src/directives.ts
export type Directive<T = any, V = any> =
| ObjectDirective<T, V>
| FunctionDirective<T, V>
Copy the code
ObjectDirective
export interfaceObjectDirective<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 }Copy the code
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
Copy the code
If you want to trigger the same behavior when mounted and updated, regardless of the other hook functions. You can do this by passing the callback to the command:
app.directive('pin'.(el, binding) = > {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
})
Copy the code
4.3 What is the difference between registering global directives and local directives?
Register global directive
app.directive('focus', {
// called when the bound element is mounted into the DOM
mounted(el) {
el.focus() // Focus elements}});Copy the code
Register local directives
const Component = defineComponent({
directives: {
focus: {
mounted(el) {
el.focus()
}
}
},
render() {
const { directives } = this.$options;
return [withDirectives(h('input'), [[directives.focus, ]])]
}
});
Copy the code
Directives that resolve global and local registrations
// 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 parsing component's processing logic
const res =
// Local registration
resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
// Global registration
resolve(instance.appContext[type], name)
return res
}
}
Copy the code
4.4 What are the differences between the rendering functions generated by built-in instructions and custom instructions?
To see the difference between rendering functions generated by built-in and custom commands, use the V-if, V-show, and V-focus custom commands as examples. Then use Vue 3 Template Explorer, an online tool, to compile and generate rendering functions:
V-if built-in instruction
<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)}}Copy the code
For the V-if directive, which will pass after compilation? : ternary operator to dynamically create nodes.
V-show built-in command
<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]
])
}
}
Copy the code
The vShow instructions in the above example is defined in the packages/runtime – dom/SRC/directives/vShow ts file, the instructions belong to ObjectDirective type, This directive internally defines the four hooks beforeMount, Mounted, updated, and beforeUnmount.
V-focus custom command
<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]
])
}
}
Copy the code
By comparing the render functions generated by the V-Focus and V-show directives, we know that both the V-focus custom and v-show built-in directives register their directives with the withDirectives function to a VNode object. Compared with the built-in instruction, the custom instruction will have more instruction parsing process.
Moreover, if both v-show and V-focus directives are applied on the input element, the _withDirectives function will be called using a 2-D array:
<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]
])
}
}
Copy the code
4.5 How do I apply instructions to render functions?
In addition to applying directives in the template, using the withDirectives function described earlier, we can easily apply the specified directives in the render function:
<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 Po Po.'), [[vShow, this.value]])]
}
});
const app = Vue.createApp(Component)
app.mount('#app')
</script>
Copy the code
This article mainly introduces how to customize instructions in Vue 3, how to register global and local instructions. In order to enable you to more in-depth grasp of the relevant knowledge of custom instructions, Po brother also analyzed the registration and application process of instructions from the perspective of source code. In the following articles, Po ge will introduce some special instructions, of course, will also focus on the principle of bidirectional binding, you should not miss it.
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. If you want to learn Vue 3.0 together, you can add Semlinker to wechat.
5. Reference resources
- Vue 3 官网 – custom commands
- Vue 3 official website – Application API