preface
Recently, in the process of business development, I found that the Vue function – custom instruction, which was not used much before, realized the abstract reuse of some element logic. Here it has carried on the simple analysis collation.
registered
There are two ways to register custom instructions:
- Global registration
Vue.directive('focus', {
// When the bound element is inserted into the DOM...
inserted: function (el) {
// Focus elements
el.focus()
}
})
Copy the code
Note that if no data is passed in as the second parameter of vue. directive, the registered directive is returned based on the directive name.
- Local registration for a component
directives: {
focus: {
// The definition of a directive
inserted: function (el) {
el.focus()
}
}
}
Copy the code
Once registered, you can use it by adding v-focus directly to the element.
<input v-focus>
Copy the code
Of course, in addition to the above format, you can also add some additional information to the directive:
v-name="data"
, pass the value to the instruction, where data can be data in the component, or methods.v-myon:click="clickHandle"
, pass parametersclick
Here you can go through[xx]
Format to dynamically pass parameters.v-myon:click.top.bar="clickHandle"
Pass the modifiertop
andbar
.
Hook function
An instruction definition object can provide the following hook functions:
- bind
Only called once, the first time a directive is bound to an element. One-time initialization Settings, such as style Settings, can be done here.
// html <div v-red></div> // js Vue.directive('red', { bind: (el, binding) => { el.style.background = 'red'; }});Copy the code
- inserted
Called when the bound element is inserted into the parent (the parent is guaranteed to exist, but not necessarily inserted into the document).
This typically performs operations related to JS behavior, such as adding listening events to elements:
// html <span v-down={ url: 'xx', name: 'xx'} /> // js Vue.directive('down', { inserted: (el, binding) = > {el. AddEventListener (' click '() = > {} / download/execution events); }});Copy the code
- update
Called when the VNode of the component is updated, but may occur before its child vNodes are updated, and can be fired multiple times.
The value of the instruction may or may not have changed, which can be determined by comparing the old and new VNodes.
- componentUpdated
Called after the VNode of the component where the directive resides and its child VNodes are all updated.
- bind
Only called once, when an instruction is unbound from an element.
Execution order
Hook functions are executed in the following order:
bind ==> inserted ==> updated ==> componentUpdated ==> bind
Function parameters
The following parameters are passed in when the hook function is called. For details, click here:
- El: The element bound by the directive that can be used to manipulate the DOM directly.
- Binding: An object that contains a lot of information about the directive.
- Name: indicates the command name
v-
Prefix. - Value: the binding value of the directive, for example
v-my-directive="1 + 1"
, the binding value is2
. - OldValue: The value preceding the instruction binding, only in
update
andcomponentUpdated
Hooks are available. Available regardless of whether the value changes. - Expression: command expression in the form of a string. For example,
v-my-directive="1 + 1"
Where, the expression is"1 + 1"
. - Arg: Optional parameter passed to the instruction. For example,
v-my-directive:foo
Where, the parameter is"foo"
. - Modifiers: An object that contains modifiers. For example,
v-my-directive:foo.bar
, the modifier object is{ bar: true }
.
- Name: indicates the command name
- Vnode: virtual node generated by Vue compilation
- OldVnode: the last virtual node
update
andcomponentUpdated
Hooks are available.
Vnode returns an object with the following properties:
- Tag, the name of the current node tag. Note that the text is also treated as a tag
vnode
And stored in thechildren
, and itstag
A value ofundefined
- Data, current node data (VNodeData type),
class
,id
All the HTML attributes are in theredata
In the - Children, current node idea node
- Text: indicates the text information of a node
- Elm, the real DOM node corresponding to the current node
- Context, the current node context, refers to the Vue instance
- Parent: Indicates the parent node of the current node
- ComponentOptions: component configuration items
Note that the Vue instance cannot be found using this keyword in the hook function, so you need to use vnode.context.
Function shorthand
If you only trigger the same behavior with bind and update, and don’t care about other hooks, you can use the function shorthand:
Vue.directive('color-swatch'.function (el, binding) {
el.style.backgroundColor = binding.value
})
Copy the code
The source code to learn
Note: The following source parses are based on version 2.6.12.
The initial object
InitGlobalAPI (Vue) is used to initialize global API methods in core instance/index.js, and initGlobalAPI(Vue) is used to initialize global API methods.
export function initGlobalAPI (Vue: GlobalAPI) {... Vue.options =Object.create(null)
ASSET_TYPES.forEach(type= > {
Vue.options[type + 's'] = Object.create(null)})... }Copy the code
ASSET_TYPES under shared/constants is an array [‘ Component ‘,’directive’,’filter’], where the initial directives are generated in the options to hold the custom DIRECTIVES for the Vue.
The global method
Further down the initGlobalAPI method, the initAssetRegisters(Vue) method is executed, which declares the directive method of the Vue. When the directive method is called, the directives are added to the vue.options. directives generated previously.
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type= > {
Vue[type] = function (
id: string,
definition: Function | Object
) :Function | Object | void {
if(! definition) {return this.options[type + 's'][id]
} else{...if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
Copy the code
Local methods
After reviewing the initGlobalAPI, we go back to instance/index.js and declare the Vue. Prototype. _init method in initMixin, which calls the mergeOptions method to generate $options:
Vue.prototype._init = function (options? :Object) {... vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) }Copy the code
In the mergeOptions method, the directives information within the component is processed and merged.
export function mergeOptions (
parent: Object,
child: Object, vm? : Component) :Object {... normalizeDirectives(child) ...const options = {}
...
for (key in child) {
if(! hasOwn(parent, key)) { mergeField(key) } }function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
function normalizeDirectives (options: Object) {
const dirs = options.directives
if (dirs) {
for (const key in dirs) {
const def = dirs[key]
if (typeof def === 'function') {
dirs[key] = { bind: def, update: def }
}
}
}
}
// Set the merge logic of directives
function mergeAssets (
parentVal: ?Object,
childVal: ?Object, vm? : Component, key: string) :Object {
const res = Object.create(parentVal || null)
if(childVal) { process.env.NODE_ENV ! = ='production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
Copy the code
As you can see here, the component’s internal directives are combined with those generated by the global directives through Object-create, so the custom directives within the component take precedence over the global directives.
The template parsing
The instructions on the template are parsed into arrays, similar to the following format:
with(this) {
return _c('div', {
directives: [{
name: "down".rawName: "v-down".value: 'value'. })}}]Copy the code
The information in directives is the data for the binding parameter in the instruction hook function.
Hook trigger
There are specific directives in Vue, which are updateDirectives.
In the process of rendering a node, there will be many hook functions called, including the instruction create, Update, destroyy3 hooks. All three of these hooks call the updateDirectives method.
// src/core/vdom/modules/directives.js
export default {
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode)
}
}
Copy the code
As you can see, all three hooks actually call the _update method, which we’ll take a look at.
Get the instruction hook function in the Vue instance
function _update (oldVnode, vnode) {
const isCreate = oldVnode === emptyNode
const isDestroy = vnode === emptyNode
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
...
}
function normalizeDirectives (
dirs: ?Array<VNodeDirective>,
vm: Component
) :{ [key: string]: VNodeDirective } {
const res = Object.create(null)...let i, dir
for (i = 0; i < dirs.length; i++) {
dir = dirs[i]
...
res[getRawDirName(dir)] = dir
dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)}return res
}
Copy the code
NormalizeDirectives are called in _UPDATE, where the parsed data is passed to the node and according to the directive name, goes to the $options.directives to obtain the corresponding directive hook function and add it to the current directive template data in the following format: — DIRECTIVES — normalizeDirectives
directives: [{
name: "down".rawName: "v-down".def: {bind(){... },... Other hooks}}]Copy the code
The instruction hook function in the Vue instance fires
When we get the instruction hook functions defined in the Vue instance, we start calling them separately.
function _update (oldVnode, vnode) {...const dirsWithInsert = []
const dirsWithPostpatch = []
let key, oldDir, dir
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
if(! oldDir) {// new directive, bind
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir)
}
} else {
// existing directive, update
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir)
}
}
}
if (dirsWithInsert.length) {
const callInsert = () = > {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
callInsert()
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch'.() = > {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
}
})
}
if(! isCreate) {for (key in oldDirs) {
if(! newDirs[key]) {// no longer present, unbind
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}
Copy the code
Inserted and componentUpdated here the bind, update, and unbind hook functions are easy to understand.
Because inserted needs to be called when the bound element is inserted into its parent, componentUpdated needs to be called after the VNode of the component where the directive is inserted and its child VNodes have all been updated, So add it to your node’s insert hook and postpatch hook via mergeVNodeHook.
// src/core/vdom/helpers/merge-hook.js
export function mergeVNodeHook (def: Object, hookKey: string, hook: Function) {
if (def instanceof VNode) {
def = def.data.hook || (def.data.hook = {})
}
let invoker
const oldHook = def[hookKey]
function wrappedHook () {
hook.apply(this.arguments)
remove(invoker.fns, wrappedHook)
}
if (isUndef(oldHook)) {
// no existing hook
invoker = createFnInvoker([wrappedHook])
} else {
/* istanbul ignore if */
if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
// already a merged invoker
invoker = oldHook
invoker.fns.push(wrappedHook)
} else {
// existing plain hook
invoker = createFnInvoker([oldHook, wrappedHook])
}
}
invoker.merged = true
def[hookKey] = invoker
}
// src/core/vdom/helpers/update-listeners.js
export function createFnInvoker (fns: Function | Array<Function>, vm: ? Component) :Function {
function invoker () {
const fns = invoker.fns
if (Array.isArray(fns)) {
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(cloned[i], null.arguments, vm, `v-on handler`)}}else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null.arguments, vm, `v-on handler`)
}
}
invoker.fns = fns
return invoker
}
Copy the code
reference
- Use vue Directive for element-level permission control
- The vue VNode
- Blog.csdn.net/weixin_3901…
- Vue Directive source code parsing
- [Vue Principles] Directives – Source edition