Vue directives are JS objects that perform low-level operations on ordinary DOM elements. They are attached to Element VNode objects and are called during some Element VNode lifecycle to manipulate Element VNode’s underlying DOM elements.
Instructions to register
Instruction registration refers to placing the JS code corresponding to an instruction in some places, where it can be searched when needed.
Global registration
- Global registration is called
App. directive(' directive name ', {directive code})
To implement the
app.directive('pin', (el, binding) => {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
})
Copy the code
- The logic of global registration is to mount the instruction name and corresponding instruction code globally
context
thedirectives
On the object
<! -- apiCreateApp.js --> directive(name: string, directive? Directives [name] = Directive return app} : directives) {// Mount to the global 'context'. Cache [name] = Directive return app}Copy the code
In-component registration
- In-component registration is an option to add directives to the component
directives: {
pin: (el, binding) => {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
}
Copy the code
- The logic of component registration is to mount the directive name and corresponding directive code on the component instance object
directives
on
<! -- component.ts --> export function applyOptions(instance: ComponentInternalInstance) {/ / mounted in the component instance object ` directives on ` instance. The directives = directives}Copy the code
Instructions for
The timing of the search
The developer is using the commands in the template, so it is necessary to search for them before rendering the template.
function render(_ctx, _cache) { with (_ctx) { const { openBlock: _openBlock, createElementBlock: _createElementBlock} = _Vue return (_openBlock(), _createElementBlock("h4", null, "instruction demo "))}}Copy the code
Using the template
instruction demo
render function as follows:
function render(_ctx, _cache) {
with (_ctx) {
const { createTextVNode: _createTextVNode, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
const _directive_pin = _resolveDirective("pin")
return _withDirectives((_openBlock(), _createElementBlock("h4", null, _hoisted_2, 512 /* NEED_PATCH */)), [
[_directive_pin, pinPadding, direction]
])
}
}
Copy the code
Templates that use directives need to search for the corresponding directives and bind them to vNodes
Command search logic
- The logic of the command search is to start with the component instance
instance
thedirectives
If you don’t find it, try againappContext
thedirectives
On looking for
export function resolveDirective(name: string): Directive | undefined {
return resolveAsset(DIRECTIVES, name)
}
function resolveAsset(
type: AssetTypes,
name: string,
warnMissing = true,
maybeSelfReference = false
) {
const res =
// local registration
// check instance[type] first which is resolved for options API
resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
// global registration
resolve(instance.appContext[type], name)
return res
}
Copy the code
The directive binds VNode
export function withDirectives<T extends VNode>(
vnode: T,
directives: DirectiveArguments
): T {
const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
if (isFunction(dir)) {
dir = {
mounted: dir,
updated: dir
} as ObjectDirective
}
bindings.push({
dir,
instance,
value,
oldValue: void 0,
arg,
modifiers
})
}
return vnode
}
Copy the code
Mount each instruction dir and some other parameters on the VNode dirs. Other parameters are: the instance component instance, the new value of the value directive (20 in this case), the oldValue of the oldValue directive (0 in this case), and the argument to the arg directive (right in this case)
The instruction calls
Instruction invocation refers to when the instruction code is executed. We initially mentioned that directives are JS objects that perform low-level operations on ordinary DOM elements, so the logic for directives should be handled in Element VNode.
const mountElement = ( vnode: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { // 1 if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'created') } // 2 if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') } // 3 queuePostRenderEffect(() => { vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) needCallTransitionHooks && transition! .enter(el) dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') }, parentSuspense) }Copy the code
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!)
// 1
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
// 2
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
Copy the code
const unmount: UnmountFn = ( vnode, parentComponent, parentSuspense, doRemove = false, optimized = false ) => { const { type, props, ref, children, dynamicChildren, shapeFlag, patchFlag, dirs } = vnode // unset ref if (ref ! = null) { setRef(ref, null, parentSuspense, vnode, true) } if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { ; (parentComponent! .ctx as KeepAliveContext).deactivate(vnode) return } const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs let vnodeHook: VNodeHook | undefined | null if ((vnodeHook = props && props.onVnodeBeforeUnmount)) { invokeVNodeHook(vnodeHook, parentComponent, vnode) } if (shapeFlag & ShapeFlags.COMPONENT) { unmountComponent(vnode.component! , parentSuspense, doRemove) } else { if (shouldInvokeDirs) { invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount') } queuePostRenderEffect(() => { vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) shouldInvokeDirs && invokeDirectiveHook(vnode, null, parentComponent, 'unmounted') }, parentSuspense) }Copy the code
- Mounting elementsVNodeIs calledThe directive
created
.beforeMount
andmounted
Hook function;- Updating elementsVNodeIs calledThe directive
beforeUpdate
.updated
Hook function;- Unloading elementsVNodeIs calledThe directive
beforeUnmount
.unmounted
Hook function;
Thinking about instructions
Use directives on components
export function renderComponentRoot( instance: ComponentInternalInstance ): VNode { const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx, inheritAttrs } = instance // inherit directives if (vnode.dirs) { if (__DEV__ && ! isElementRoot(root)) { warn( `Runtime directive used on component with non-element root node. ` + `The directives will not function as intended.` ) } root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs } }Copy the code
When a component renders the root VNode of the subtree VNode, the component’s instruction dirs is added to the root VNode’s dirs. So directives that apply to components are equivalent to directives that apply to the root node element VNode.
Some usage scenarios on components
I think some of the most useful commands are:
- V-lazyload: Lazy loading of images
- V-loading: Implementation plus a loading animation
- V-permission: Permission control that hides DOM elements without permission
- V-debounce: Input anti-shock, especially as requested by the search box