Skip to content

Latest commit

 

History

History
213 lines (180 loc) · 7.47 KB

自定义指令.md

File metadata and controls

213 lines (180 loc) · 7.47 KB

本篇文章,我们讲的是Vue中,自定义指令的内部实现方式。具体用法官方文档已经解释的很清楚了。

自定义指令,同样有全局和局部两种方式。

全局指令的定义:

Vue.directive('demo', {
  bind: function(){
    ...
  }
})

全局指令的实现方式,和全局组件、全局过滤器一致。

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
  }
}

接收两个参数,第一个是指令名,第二个参数是一个函数或一个对象,如果是函数,则会新建一个对象,并把definition赋值给bindupdate属性。最终,会把指令的定义放在Vue.options.directives上。在实例化组件对象时,会合并到vm.$options上。

局部指令的定义:

new Vue({
  directives: {
    demo: {
      bind: function(){
    	...
  	  }
    }
  } 
})

其实同全局指令类似,demo的值也可以是一个函数,在合并配置项时,会新建一个对象,并把赋值给bindupdate属性。

自定义指令的用法和内置指令的用法一致,通过v-指令名的方式添加到模板的标签上,然后在对应的钩子函数中,执行相应的操作。

模板的解析流程在指令概述中简单概括了一下。这里我们详细的说一下与自定义指令密切相关的内容。

首先还是指令的解析:

modifiers = parseModifiers(name)
if (modifiers) {
  // const modifierRE = /\.[^.]+/g
  name = name.replace(modifierRE, '')
}
...
// const dirRE = /^v-|^@|^:/
name = name.replace(dirRE, '')
// parse arg
// const argRE = /:(.*)$/
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)

假如我们的指令时这样的v-demo:foo.a.b = 'demo_value',其中data_demo绑定的数据为test。初始情况下namerawName都是``v-demo:foo.a.bmodifierRE`会去掉修饰符,`dirRE`是去掉前面的`v-`,`argRE`匹配冒号及后面的值,最终`arg`为`foo`,`name`为`demo`,`value`是`demo_value`,`modifiers`为`{a: true, b: true}`。

genDirectives内生成添加到directives属性中的对象的生成过程如下:

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)}` : ''
}},`

该对象最终会传到我们自己定义的bindupdate等函数的第二个参数。每一项的值分别如下:

{
  name: 'demo',
  rawName: 'v-demo:foo.a.b',
  value: 'test',
  expression: 'demo_value',
  arg: 'foo',
  modifiers: {a: true, b: true}
}

概述中我们也提到过,在组件patch的不同阶段,就会修改data数据的钩子函数,进而在_update函数中调用我们定义的各个钩子函数来实现指令的操作。

function _update (oldVnode, vnode) {
  // 第一次实例化组件时,oldVnode是emptyNode
  const isCreate = oldVnode === emptyNode
  // 销毁组件时,vnode是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
      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.data.hook || (vnode.data.hook = {}), 'insert', callInsert)
    } else {
      callInsert()
    }
  }

  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), '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)
      }
    }
  }
}

normalizeDirectives方法是从组件的vm.$options.directives中获取指令的定义,emptyModifiers是一个空对象,最终res的键值是我们上面提到的rawName格式,resolveAsset方法就是获取指令的定义,同之前讲到的获取子组件一样,会转为驼峰、中划线的各种形式来尝试获取。指令的定义,会添加到我们之前生成的对象的def属性中。

function normalizeDirectives (
  dirs: ?Array<VNodeDirective>,
  vm: Component
): { [key: string]: VNodeDirective } {
  const res = Object.create(null)
  if (!dirs) {
    return res
  }
  let i, dir
  for (i = 0; i < dirs.length; i++) {
    dir = dirs[i]
    if (!dir.modifiers) {
      dir.modifiers = emptyModifiers
    }
    res[getRawDirName(dir)] = dir
    dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)
  }
  return res
}

接下来循环新vnode上绑定的指令,如果第一次绑定,则直接调用bind钩子函数。调用函数使用的是callHook函数,在调用时会传入vnode.elm dirvnodeoldVnodeisDestroy五个参数,与文档中提到的钩子函数的参数正好对应。若同时还添加了inserted钩子,则会先把它添加到dirsWithInsert数组中。

function callHook (dir, hook, vnode, oldVnode, isDestroy) {
  const fn = dir.def && dir.def[hook]
  if (fn) {
    fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
  }
}

如果不是第一次绑定,则调用update钩子函数,若同时定义了componentUpdated钩子,则会先把它添加到dirsWithPostpatch数组中。

接着,如果是vnode是第一次创建,则会把dirsWithInsert数组中的回调追加到vnode.data.hook.insert中执行。这是因为vnode.data.hook.insert调用的时机,是在dom插入到页面之后,这个时候可以真正操作dom。否则,比如diff操作复用了之前的元素时,因为元素已经在页面中,此时则直接调用dirsWithInsert数组中的钩子函数。

dirsWithPostpatch的操作与之前类似,不过这次是添加到vnode.data.hook.postpatch钩子函数中,该钩子函数,是在patch之后调用。

最后,如果不是第一次创建,就调用旧vnode中新vnode不存在的指令的unbind钩子函数。

以上,就是Vue中,对于自定义指令的全部处理过程。