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 parametersclickHere you can go through[xx]Format to dynamically pass parameters.
  • v-myon:click.top.bar="clickHandle"Pass the modifiertopandbar.

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 namev-Prefix.
    • Value: the binding value of the directive, for examplev-my-directive="1 + 1", the binding value is2.
    • OldValue: The value preceding the instruction binding, only inupdateandcomponentUpdatedHooks 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:fooWhere, the parameter is"foo".
    • Modifiers: An object that contains modifiers. For example,v-my-directive:foo.bar, the modifier object is{ bar: true }.
  • Vnode: virtual node generated by Vue compilation
  • OldVnode: the last virtual nodeupdateandcomponentUpdatedHooks 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 tagvnodeAnd stored in thechildren, and itstagA value ofundefined
  • Data, current node data (VNodeData type),class,idAll the HTML attributes are in theredataIn 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