Vue自定义指令也分全局指令和局部指令两种,全局指令和局部指令的使用方法分别为:

// 全局指令使用方法
Vue.directive('test', {
  bind: function(){
    ...
  }
})

// 局部指令使用方法
Vue({
  directives: {
    test: {
      bind: function(){
    	...
  	  }
    }
  } 
})

上面写的bind方法其实就是自定义指令的几个钩子函数之一,可以视情况添加其他的钩子函数,官方的介绍:

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中),实际应用场景:被绑定元素插入父节点后,做一些初始化的操作,比如改变颜色,字体大小等等

update:所在组件的VNode 更新时调用,但是可能发生在其子 VNode 更新之前,实际应用场景:被绑定元素状态/样式、内容发生改变时触发

componentUpdated:指令所在组件的 VNode及其子VNode全部更新后调用

unbind:只调用一次,指令与元素解绑时调用,实际应用场景:当指令绑定的元素从 dom 中删除时触发,可在这个时候做一些需要的操作

指令钩子函数会被传入以下参数:

el:指令所绑定的元素,可以用来直接操作 DOM 。

binding:一个对象,包含以下属性:

  • name:指令名,不包括 v- 前缀。
  • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
  • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
  • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
  • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }

vnode:Vue 编译生成的虚拟节点

oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

在文件src/core/global-api/index.js中可以看到initAssetRegisters,这个方法会用来初始化component以及directive

export function initGlobalAPI (Vue: GlobalAPI) {
  .
  .
  .

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

initGlobalAPI会接受两个参数,第一个参数代表指令的名字,第二个参数如果是函数,则会创建一个

{ bind: definition, update: definition}对象,并将他赋值给definition,这个对象会在生成Vue实例的时候会合并到vm.$options.directives上。顺便说一下这里面也对自定义component进行一个处理,他会根据传入的组件定义创建一个Vue子类,设置好name或者id,最后返回。

  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })

指令的解析在src/compiler/parser/index.js中完成,用正则匹配指令,在解析完v-bindv-on等等原生的指令后,调用addDirective,将指令属性添加到当前节点的directives上。其中的正则modifierRE会去掉修饰符,dirRE是去掉前面的v-argRE匹配冒号及后面的值

 else { // normal directives
   name = name.replace(dirRE, '')
   // parse arg
   const argMatch = name.match(argRE)
   const arg = argMatch && argMatch[1]
   if (arg) {
     name = name.slice(0, -(arg.length + 1))
   }
   addDirective(el, name, rawName, value, arg, modifiers, list[i])
   if (process.env.NODE_ENV !== 'production' && name === 'model') {
     checkForAliasModel(el, value)
   }
 }


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

编译阶段,在src/compiler/codegen/index.js中,调用genDirectives,循环遍历directives对象,最终生成对应的属性,生成的形式为

directives:[{
  name: '',
  rawName: '',
  value: '',
  expression: '',
  arg: '',
  modifiers: {}
}
...
]

其代码为

...      
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${
        dir.arg ? `,arg:"${dir.arg}"` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
...

最后,会在src/core/vdom/modules/directives.js中调用_update,这个方法的主要作用遍历指令对象,将上面的属性通过回调函数调用对应的钩子函数,然后处理,钩子函数即上面提到的bindupdateinserted

function _update (oldVnode, vnode) {
  // 第一次实例化组件时,oldVnode是emptyNode
  const isCreate = oldVnode === emptyNode
  // 销毁组件时,vnode是emptyNode
  const isDestroy = vnode === emptyNode
  //normalizeDirectives函数是从组件的vm.$options.directives中获取指令的定义
  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) {
    //循环新vnode上绑定的指令
    oldDir = oldDirs[key]
    dir = newDirs[key]
    if (!oldDir) {
      // new directive, bind   => 如果第一次绑定,则直接调用bind钩子函数
      callHook(dir, 'bind', vnode, oldVnode)
      if (dir.def && dir.def.inserted) {
        //若同时还添加了inserted钩子,则会先把它添加到dirsWithInsert数组中。
        dirsWithInsert.push(dir)
      }
    } else {
      // existing directive, update   =>  如果不是第一次绑定,则调用update钩子函数
      dir.oldValue = oldDir.value
      dir.oldArg = oldDir.arg
      callHook(dir, 'update', vnode, oldVnode)
      if (dir.def && dir.def.componentUpdated) {
         //若同时定义了componentUpdated钩子,则会先把它添加到dirsWithPostpatch数组中。
        dirsWithPostpatch.push(dir)
      }
    }
  }

  if (dirsWithInsert.length) {
    const callInsert = () => {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    if (isCreate) {
       //如果是vnode是第一次创建,
      //则会把dirsWithInsert数组中的回调追加到vnode.data.hook.insert中执行
      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  
        // 如果不是第一次创建,就调用旧vnode中新vnode不存在的指令的unbind钩子函数
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
      }
    }
  }
}