Directive instruction

If you think it is good, please send me a Star at GitHub

Instruction registration and use

As with components, directives can be registered in two ways: global and local.

Global registration

Global directives can be registered using the global API method: vue.directive (). After registration all directives are in the vue.options [‘directives’] option.

In the Vue source code, the vue.directive () global API method handles the following:

// component, directive, filter
import { ASSET_TYPES } from 'shared/constants'
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 {
        / /... Omit the other
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}
Copy the code

Suppose we have code like this:

import Vue from 'vue'
Vue.directive('focus', {
  inserted: function (el) {
    el.focus()
  }
})
Copy the code

In the above code, we globally registered a directive named Focus that automatically focuses when the element to which the directive is bound is inserted into the DOM. After registration, we print vue. options[‘directives’] and the result is as follows:

{
  show: {... },model: {... },focus: {
    inserted: function (el) {
      el.focus()
    }
  }
}
Copy the code

We can see that in addition to the focus directive we defined ourselves, there are two show and model directives that are provided by default by Vue and we can use directly.

After introducing the registration method of the full local directive, let’s see how we define the focus directive should be used:

<template>
  <input v-focus />
</template>
Copy the code

Local registration

Like the local registry of a component, the local registry of directives needs to be written in the component’s directives option:

<template>
  <div>
    <input v-focus />
  </div>
</template>
<script>
export default {
  name: 'App',
  directives: {
    focus: {
      inserted: function (el) {
        el.focus()
      }
    }
  }
}
</script>
Copy the code

When we print the $options[‘directives’] property of the App component instance, we get the following:

{
  focus: {
    inserted: function (el) {
      el.focus()
    }
  },
  __proto__: {
    model: {},
    show: {}}}Copy the code

We can see that there is a difference between the locally registered and globally registered directives for child components, and the globally registered directives are mounted to the prototype of the component directives object.

Custom instruction

In addition to the global default directives V-show and V-model we mentioned earlier, we can also choose to register custom directives. As in the instruction registration and Use section, the V-focus directive we registered is a custom directive.

Hook function

Before introducing custom directives, we need to look at the hook functions of directives.

  • bind: only called once, when the directive is first bound to an element, where one-time initialization can be done.
  • inserted: called when the bound element is inserted into the parent node (the parent node is guaranteed to exist, but not necessarily inserted into the document).
  • update: is called when the component’s VNode is updated, but may occur before its child VNodes are updated.
  • componentUpdated: invoked after the VNode of the component where the directive resides and its child VNodes are all updated.
  • unbind: called only once, when directives are unbound from elements.

All of the above hook functions are optional. With different hook functions, we can do more things in different states.

Hook function arguments

In the course of daily development, you may see several ways of using commands, as follows:

<input v-model="inputValue" />
<input v-model:value="inputValue" />
<div v-show="age > 18"></div>
<div v-focus:foo="bar"></div>
<div v-font.color.fontSize="{ color: '#f60', fontSize: '14px' }"></div>
Copy the code

All of the above arguments are represented in the binding object, the second argument to the hook function:

{
  inserted: function (el, binding) {
    console.log(binding.name)
    console.log(binding.value)
    console.log(binding.oldValue)
    console.log(binding.expression)
    console.log(binding.arg)
    console.log(binding.modifiers)
  }
}
Copy the code

The Binding object contains the following attributes:

  • name: Command name, withoutvPrefix, for example:model,showAs well asfocus.
  • value: the binding value of the instruction, which is a valid valueJavaScriptExpressions, such as:age > 18According to theageThe binding value istrueorfalse. Or bind an object directly{ color: '#f60', fontSize: '14px' }
  • oldValue: Specifies the last binding value of the directiveupdateandcomponentUpdatedThese two hook functions are available.
  • expression: a string expression for an instruction, for example:v-show="age > 18"The expression isage > 18.
  • arg: Command parameters, for example:v-model:value="inputValue", the parameters forvalue.
  • modifiers: modifier object, such as:v-font.color.fontSize, the modifier object is:{color: true, fontSize: true}.

Instruction parsing and instruction running

We take the following code as an example to analyze the analysis and operation of instructions.

new Vue({
  el: '#app'.directives: {
    focus: {
      inserted: function (el) {
        el.focus()
      }
    }
  },
  template: '<input v-focus />'
})
Copy the code

Instruction parsing

In the Compilation Principles section, we looked at some of the processes associated with compilation. As an example, the contents of the template template are compiled into an AST by calling parse:

const ast = parse(template.trim(), options)
Copy the code

When parse compiles, V-focus is first parsed into the AST’s properties array:

const ast = {
  tag: 'input'.attrsList: [{name: 'v-focus'}].attrMap: {
    'v-focus': ' '}}Copy the code

ProcessAttrs is then called from the processElement function to handle the properties when the input tag triggers the compilation of the end hook function:

export function processElement (element: ASTElement, options: CompilerOptions) {
  / /... Omit the other
  processAttrs(element)
  return element
}
Copy the code

The omission code for the processAttrs method is as follows:

export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\.|^#/ : /^v-|^@|^:|^#/
export const bindRE = /^:|^\.|^v-bind:/
export const onRE = /^@|^v-on:/
function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, syncGen, isDynamic
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
      / /... Omit code
      if (bindRE.test(name)) {
        / / v - bind logic
      } else if (onRE.test(name)) {
        / / v - on logic
      } else {
        name = name.replace(dirRE, ' ')
        // parse arg
        const argMatch = name.match(argRE)
        let arg = argMatch && argMatch[1]
        isDynamic = false
        if (arg) {
          name = name.slice(0, -(arg.length + 1))
          if (dynamicArgRE.test(arg)) {
            arg = arg.slice(1, -1)
            isDynamic = true
          }
        }
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
        if(process.env.NODE_ENV ! = ='production' && name === 'model') {
          checkForAliasModel(el, value)
        }
      }
    }
  }
Copy the code

ProcessAttrs code analysis:

  • First of all bydirRERegular expression matchingv-focusSatisfies the form of the instruction.
  • And then go throughbindREThe regular expression checks whether it isv-bind.
  • If not, continue to useonREThe regular expression checks whether it isv-on
  • If neither of them is true, it isnormal directives.

In the else branch, we add the V-focus directive from attrsList to the AST object’s cache by calling the addDirective method, which reads as follows:

export function addDirective (el: ASTElement, name: string, rawName: string, value: string, arg: ? string, isDynamicArg: boolean, modifiers: ? ASTModifiers, range? : Range) {
  (el.directives || (el.directives = [])).push(rangeSetItem({
    name,
    rawName,
    value,
    arg,
    isDynamicArg,
    modifiers
  }, range))
  el.plain = false
}
Copy the code

After processing, our AST values are as follows:

const ast = {
  tag: 'input'.attrsList: [{name: 'v-focus'}].attrMap: {
    'v-focus': ' '
  },
  directives: [{name: 'v-focus'.value: ' '}}]Copy the code

After parse, generate is called to generate the render function. Since this process is already covered in compilation principles, let’s go straight to the generated render function:

const code = generate(ast, options)

// code prints the result
{
  render: "with(this){ return _c('input', {directives: [{ name: 'focus', rawName: 'v-focus' }]})}".staticRenderFns: []}Copy the code

Operation of instruction

After the render function is generated, the render function will be called to generate the virtual DOM when the component is patched.

Next, let’s review the patch method and the hook function of the virtual DOM:

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code

When we call the createPatchFunction method, we pass in a Modules array. In this module, we focus on baseModules:

// src/core/vdom/modules/index.js
import directives from './directives'
export default [ directives ]

// src/core/vdom/modules/directives.js
export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
  }
}
function updateDirectives () {
  / /... Omit code
}
Copy the code

In the cache. js file, we export an object that has three keys: Create, update, and destroy. These three correspond to the hook functions of the component, that is, the updateDirectives or unbindDirectives methods are automatically called when the component triggers CREATE, UPDATE, and destroy. Directives

Then, let’s review how the createPatchFunction method handles these hook functions:

const hooks = ['create'.'activate'.'update'.'remove'.'destroy']
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  / /... Omit code
}

// CBS prints the result{... .create: [...updateDirectives(oldVnode, vnode){}].update: [...updateDirectives(oldVnode, vnode){}].destroy: [...unbindDirectives(vnode){}]}Copy the code

During patch method execution, invokeCreateHooks are called at appropriate times to trigger the CREATE hook function, InvokeDestroyHook is called when appropriate to trigger the destroy hook function and the patchVnode method is used to iterate through the CBS. update array and execute the methods in the update array.

/ / invokeCreateHooks code
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

// patchVnode
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
  / /... Omit code
  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  / /... Omit code
}
Copy the code

Now that we have figured out when to invoke the updateDirectives and unbindDirectives methods, let’s look at the definitions of both directives in the directive.

UnbindDirectives method

// directives.js
export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
  }
}
Copy the code

We can see in the definition of the unbindDirectives method that calls the updateDirectives internally and passes an emptyNode to the second parameter of the method to implement the unbinding of the directive. Directives

UpdateDirectives method

The updateDirectives method is defined simply as follows:

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}
Copy the code

In updateDirectives, it simply calls _UPDATE, let’s look at the code implementation of this core method:

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)

  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

Code analysis:

  • Variable description:
  1. isCreateifoldVnodeIs an empty node, which indicates the current nodevnodeIs the newly created node.
  2. isDestroyIf the currentvnodeIs an empty nodeoldVnodeShould be destroyed.
  3. oldDirsStore old instruction sets.
  4. newDirsStore a new instruction set.
  5. dirsWithInsertNeed to triggerinsertedA collection of instructions for a hook function.
  6. dirsWithPostpatchNeed to triggercomponentUpdatedA collection of instructions for a hook function.
  • Formatting instruction: Formatting instruction callednormalizeDirectivesMethod with the following code:
function normalizeDirectives (
  dirs: ?Array<VNodeDirective>,
  vm: Component
) :{ [key: string]: VNodeDirective } {
  const res = Object.create(null)
  if(! dirs) {// $flow-disable-line
    return res
  }
  let i, dir
  for (i = 0; i < dirs.length; i++) {
    dir = dirs[i]
    if(! dir.modifiers) {// $flow-disable-line
      dir.modifiers = emptyModifiers
    }
    res[getRawDirName(dir)] = dir
    dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)}// $flow-disable-line
  return res
}
Copy the code

Take the V-focus command as an example. The results before and after formatting are as follows:

// Before formatting
const directives = [
  { name: 'focus'.rawName: 'v-focus'}]// After formatting
const directives = {
  'v-focus': {
    name: 'focus'.rawName: 'v-focus'.modifiers: {},
    def: {
      inserted: function (el) {
        el.focus()
      }
    }
  }
}
Copy the code
  • Traverse the new instruction set: use the for loop to traverse the new instruction set, and take the key of each traverse to value in the new and old instruction set. If the current instruction does not exist in the old instruction set, it indicates that it is a new instruction. For the new instruction, we first use the callhook to trigger the instruction’s bind hook function, and then determine whether to add to the dirsWithInsert array based on whether it defines the INSERTED hook function. If the current instruction exists in the old instruction collection, the update hook function of the instruction should be fired and added to the dirsWithPostpatch array depending on whether the instruction defines an UPDATE hook function.

  • Determine dirsWithInsert: If the dirsWithInsert array has a value, the isCreate value is used to determine whether to call callInsert directly or when the virtual DOM’s INSERT hook function is triggered. For new nodes, this is done to ensure that the Inserted hook function is called when the bound element is inserted into the parent node.

  • Determine dirsWithPostpatch: If the dirsWithPostpatch array has a value, then the traversal and trigger instruction componentUpdated hook function is wrapped and incorporated into the PostPatch hook function of the virtual DOM. This is done to ensure that the Component’s VNodes and children are all updated before calling componentUpdated.

  • Trigger the unbind hook function: iterates through the old instruction set if the current node is not a new node. During traversal, any instruction that is not in the new instruction set needs to fire the instruction’s unbind hook function.

summary

In this chapter, we review the registration and use of instructions; Understand the hook function of the instruction and the function of various hook function parameters; Learned how instructions are parsed and when to run; Finally, you learned how to trigger the corresponding hook function inside the instruction according to different situations.

If you think it is good, please send me a Star at GitHub