vue自定义指令directive的使用方法

Vue中内置了很多的指令,如v-model、v-show、v-html等,但是有时候这些指令并不能满足我们,或者说我们想为元素附加一些特别的功能,这时候,我们就需要用到vue中一个很强大的功能了—自定义指令。

在开始之前,我们需要明确一点,自定义指令解决的问题或者说使用场景是对普通 DOM 元素进行底层操作,所以我们不能盲目的胡乱的使用自定义指令。

如何声明自定义指令?

就像vue中有全局组件和局部组件一样,他也分全局自定义指令和局部指令。

let Opt = {
 bind:function(el,binding,vnode){ },
 inserted:function(el,binding,vnode){ },
 update:function(el,binding,vnode){ },
 componentUpdated:function(el,binding,vnode){ },
 unbind:function(el,binding,vnode){ },
}

对于全局自定义指令的创建,我们需要使用 Vue.directive接口

Vue.directive('demo', Opt)

对于局部组件,我们需要在组件的钩子函数directives中进行声明

Directives: {
 Demo:  Opt
}

Vue中的指令可以简写,上面Opt是一个对象,包含了5个钩子函数,我们可以根据需要只写其中几个函数。如果你想在 bind 和 update 时触发相同行为,而不关心其它的钩子,那么你可以将Opt改为一个函数。

let Opt = function(el,binding,vnode){ }

如何使用自定义指令?

对于自定义指令的使用是非常简单的,如果你对vue有一定了解的话。

我们可以像v-text=”'test'”一样,把我们需要传递的值放在‘='号后面传递过去。

我们可以像v-on:click=”handClick” 一样,为指令传递参数'click'。

我们可以像v-on:click.stop=”handClick” 一样,为指令添加一个修饰符。

我们也可以像v-once一样,什么都不传递。

每个指令,他的底层封装肯定都不一样,所以我们应该先了解他的功能和用法,再去使用它。

自定义指令的 钩子函数

上面我们也介绍了,自定义指令一共有5个钩子函数,他们分别是:bind、inserted、update、componentUpdate和unbind。

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

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

  • el:指令所绑定的元素,可以用来直接操作 DOM 。
  • binding:一个对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • 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 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

对于这几个钩子函数,了解的可以自行跳过,不了解的我也不介绍,自己去官网看,没有比官网上说的更详细的了:钩子函数

项目中的bug

在项目中,我们自定义一个全局指令my-click

Vue.directive('my-click',{
 bind:function(el, binding, vnode, oldVnode){
  el.addEventListener('click',function(){
   console.log(el, binding.value)
  })
 }
})

同时,有一个数组arr:[1,2,3,4,5,6],我们遍历数组,生成dom元素,并为元素绑定指令:

<ul>
 <li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li>
</ul>

可以看到,当我们点击元素的时候,成功打印了元素,以及传递过去的数据。

可是,当我们把最后一个元素动态的改为8之后(6 --> 8),点击元素,元素是对的,可是打印的数据却仍然是6.

或者,当我们删除了第一个元素之后,点击元素

黑人问号脸,这是为什么呢????带着这个疑问,我去看了看源码。在进行下面的源码分析之前,先来说结论:

组件进行初始化的时候,也就是第一次运行指令的时候,会执行bind钩子函数,我们所传入的参数(binding)都进入到了这里,并形成了一个闭包。

当我们进行数据更新的时候,vue虚拟dom不会销毁这个组件(如果说删除某个数据,会从后往前销毁组件,前面的总是最后销毁),而是进行更新(根据数据改变),如果指令有update钩子会运行这个钩子函数,但是对于元素在bind中绑定的事件,在update中没有处理的话,他不会消失(依然引用初始化时形成的闭包中的数据),所以当我们更改数据再次点击元素后,看到的数据还是原数据。

源码分析

函数执行顺序:createElm/initComponent/patchVnode --> invokeCreateHooks (cbs.create) --> updateDirectives --> _update

在createElm方法和initComponent方法和更新节点patchVnode时会调用invokeCreateHooks方法,它会去遍历cbs.create中钩子函数进行执行,cbs.create中的钩子函数如下图所示共8个。我们所需要看的就是updateDirectives这个函数,这个函数会继续调用_update函数,vue中的指令操作就都在这个_update函数中了。

下面我们就来详细看下这个_update函数。

function _update(oldVnode, vnode) {
 //判断旧节点是不是空节点,是的话表示新建/初始化组件
 var isCreate = oldVnode === emptyNode;
 //判断新节点是不是空节点,是的话表示销毁组件
 var isDestroy = vnode === emptyNode;
 //获取旧节点上的所有自定义指令
 var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
 //获取新节点上的所有自定义指令
 var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);

 //保存inserted钩子函数
 var dirsWithInsert = [];
 //保存componentUpdated钩子函数
 var dirsWithPostpatch = [];

 var key, oldDir, dir;

 //这里先说下callHook$1函数的作用
 //callHook$1有五个参数,第一个参数是指令对象,第二个参数是钩子函数名称,第三个参数新节点,
 //第四个参数是旧节点,第五个参数是是否为注销组件,默认为undefined,只在组件注销时使用
 //在这个函数里,会根据我们传递的钩子函数名称,运行我们自定义组件时,所声明的钩子函数,

 //遍历所有新节点上的自定义指令
 for(key in newDirs) {
  oldDir = oldDirs[key];
  dir = newDirs[key];
  //如果旧节点中没有对应的指令,一般都是初始化的时候运行
  if(!oldDir) {
   //对该节点执行指令的bind钩子函数
   callHook$1(dir, 'bind', vnode, oldVnode);
   //dir.def是我们所定义的指令的五个钩子函数的集合
   //如果我们的指令中存在inserted钩子函数
   if(dir.def && dir.def.inserted) {
    //把该指令存入dirsWithInsert中
    dirsWithInsert.push(dir);
   }
  } else {
   //如果旧节点中有对应的指令,一般都是组件更新的时候运行
   //那么这里进行更新操作,运行update钩子(如果有的话)
   //将旧值保存下来,供其他地方使用(仅在 update 和 componentUpdated 钩子中可用)
   dir.oldValue = oldDir.value;
   //对该节点执行指令的update钩子函数
   callHook$1(dir, 'update', vnode, oldVnode);
   //dir.def是我们所定义的指令的五个钩子函数的集合
   //如果我们的指令中存在componentUpdated钩子函数
   if(dir.def && dir.def.componentUpdated) {
    //把该指令存入dirsWithPostpatch中
    dirsWithPostpatch.push(dir);
   }
  }
 }

 //我们先来简单讲下mergeVNodeHook的作用
 //mergeVNodeHook有三个参数,第一个参数是vnode节点,第二个参数是key值,第三个参数是回函数
 //mergeVNodeHook会先用一个函数wrappedHook重新封装回调,在这个函数里运行回调函数
 //如果该节点没有这个key属性,会新增一个key属性,值为一个数组,数组中包含上面说的函数wrappedHook
 //如果该节点有这个key属性,会把函数wrappedHook追加到数组中

 //如果dirsWithInsert的长度不为0,也就是在初始化的时候,且至少有一个指令中有inserted钩子函数
 if(dirsWithInsert.length) {
  //封装回调函数
  var callInsert = function() {
   //遍历所有指令的inserted钩子
   for(var i = 0; i < dirsWithInsert.length; i++) {
    //对节点执行指令的inserted钩子函数
    callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
   }
  };
  if(isCreate) {
   //如果是新建/初始化组件,使用mergeVNodeHook绑定insert属性,等待后面调用。
   mergeVNodeHook(vnode, 'insert', callInsert);
  } else {
   //如果是更新组件,直接调用函数,遍历inserted钩子
   callInsert();
  }
 }

 //如果dirsWithPostpatch的长度不为0,也就是在组件更新的时候,且至少有一个指令中有componentUpdated钩子函数
 if(dirsWithPostpatch.length) {
  //使用mergeVNodeHook绑定postpatch属性,等待后面子组建全部更新完成调用。
  mergeVNodeHook(vnode, 'postpatch', function() {
   for(var i = 0; i < dirsWithPostpatch.length; i++) {
    //对节点执行指令的componentUpdated钩子函数
    callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
   }
  });
 }

 //如果不是新建/初始化组件,也就是说是更新组件
 if(!isCreate) {
  //遍历旧节点中的指令
  for(key in oldDirs) {
   //如果新节点中没有这个指令(旧节点中有,新节点没有)
   if(!newDirs[key]) {
    //从旧节点中解绑,isDestroy表示组件是不是注销了
    //对旧节点执行指令的unbind钩子函数
    callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
   }
  }
 }
}

callHook$1函数

function callHook$1(dir, hook, vnode, oldVnode, isDestroy) {
 var fn = dir.def && dir.def[hook];
 if(fn) {
  try {
   fn(vnode.elm, dir, vnode, oldVnode, isDestroy);
  } catch(e) {
   handleError(e, vnode.context, ("directive " + (dir.name) + " " + hook + " hook"));
  }
 }
}

解决

看过了源码,我们再回到上面的bug,我们应该如何去解决呢?

1、事件解绑,重新绑定

我们在bind钩子中绑定了事件,当数据更新后,会运行update钩子,所以我们可以在update中先解绑再重新进行绑定。因为bind和update中的内容差不多,所以我们可以把bind和update合并为同一个函数,在用自定义指令的简写方法写成下面的代码:

Vue.directive('my-click', function(el, binding, vnode, oldVnode){
 //点击事件的回调挂在在元素myClick属性上
 el.myClick && el.removeEventListener('click', el.myClick);
 el.addEventListener('click', el.myClick = function(){
  console.log(el, binding.value)
 })
})

可以看到,数据已经变成我们想要的数据了。

2、把binding挂在到元素上,更新数据后更新binding

我们已经知道了,造成问题的根本原因是初始化运行bind钩子的时候为元素绑定事件,事件内获取的数据是初始化的时候传递过来的数据,因为形成了闭包,那么我们不使用能引起闭包的数据,把数据存到某一个地方,然后去更新这个数据。

Vue.directive('my-click',{
 bind: function(el, binding, vnode, oldVnode){
  el.binding = binding
  el.addEventListener('click', function(){
   var binding = this.binding
   console.log(this, binding.value)
  })
 },
 update: function(el, binding, vnode, oldVnode){
  el.binding = binding
 }
})

这样也能达到我们想要的效果。

3、更新父元素

如果我们为父元素ul绑定一个变化的key值,这样,当数据变更的时候就会更新父元素,从而重新创建子元素,达到重新绑定指令的效果。

<ul :key="Date.now()">
 <li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li>
</ul>

这样也能达到我们想要的效果。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • vue.js内部自定义指令与全局自定义指令的实现详解(利用directive)

    前言 大家都知道在Vue中,我们平时数据驱动视图时候,内部自带的指令有时候解决不了一些需求,这时候,Vue给我们一个很好用的东东来实现自定义指令,这就是directive.下面话不多说了,来一起看看详细的介绍: directive 这个单词是我们写自定义指令的关键字哦 自定义指令为我们提供了几个钩子函数,这时候你一定好奇什么是钩子函数,说简单点,就是集中表现状态 bind: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作. inserted: 被绑

  • vue自定义指令directive实例详解

    下面给大家介绍vue自定义指令directive,具体内容如下所示: 官网截图实例 vue除了一些核心的内部定义的指令(v-model,v-if,v-for,v-show)外,vue也允许用户注册自己的一些功能性的指令,有时候你实在是要对Dom操作,这个时候是自定义指令最合适的了. 来直接看例子:当页面加载时使得元素获得焦点(autofocus 在移动版 Safari 是不支持的),就是当页面加载好了,不做任何的操作使得表单自动获得焦点,光标自动在某个表单上代码如下: Vue.directive

  • vue-cli 自定义指令directive 添加验证滑块示例

    vue项目注册登录页面遇到了一个需要滑块的功能,网上看了很多插件发现都不太好用,于是自己写了一个插件供大家参考: 用的是vue的自定义指令direcive,只需要在需要的组件里放入对应的标签嵌套即可: template: <template> <div> <div class="movebox" > <div class="movego"></div> <div class="txt&quo

  • Vue.directive 自定义指令的问题小结

    1.今天复习一下Vue自定义指令的代码,结果出现一个很无语的结果,先贴代码. 2. <div id="example" v-change-by="myColor"></div> <script src="vue.min.js"></script> <script> new Vue({ el:"#example", data:{ msg:"", my

  • Vue.directive自定义指令的使用详解

    很多教程都是在讲自定义指令的概念和语法,看的相当蛋疼.本文不讲语法和概念,只讲用法. 自定义指令基本就是用来操作DOM的,虽然官方推荐数据驱动视图,有时候还是需要自定义指令来操作DOM,指令可复用. 1. 自定义指令实现拖拽 HTML: <div v-drag>我可以拖拽</div> JS: Vue.directive('drag', inserted:function(el){ //inserted 钩子函数:当元素被插入父元素时触发,可省略 let oDiv=el; //el

  • 详解Vue.directive 自定义指令

    一.什么是全局API? 全局API并不在构造器里,而是先声明全局变量或者直接在Vue上定义一些新功能,Vue内置了一些全局API,比如我们今天要学习的指令Vue.directive.说的简单些就是,在构造器外部用Vue提供给我们的API函数来定义新的功能. 二.Vue.directive自定义指令 我们在第一季就学习了内部指令,我们也可以定义一些属于自己的指令,比如我们要定义一个v-jspang的指令,作用就是让文字变成绿色. 在自定义指令前我们写一个小功能,在页面上有一个数字为10,数字的下面

  • Vue自定义指令directive的使用方法分享

    1. 一个指令定义对象可以提供如下几个钩子函数(均为可选) bind:只调用一次,指令第一次绑定到元素时调用.在这里可以进行一次性的初始化设置. inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中). update:只要当前元素不被移除,其他操作几乎都会触发这2个生命周期,先触发update后触发componentUpdate.虚拟DOM什么时候更新:只要涉及到元素的隐藏.显示(display)值的改变.内容的改变等都会触发虚拟DOM更新. component

  • vue自定义指令directive的使用方法

    Vue中内置了很多的指令,如v-model.v-show.v-html等,但是有时候这些指令并不能满足我们,或者说我们想为元素附加一些特别的功能,这时候,我们就需要用到vue中一个很强大的功能了-自定义指令. 在开始之前,我们需要明确一点,自定义指令解决的问题或者说使用场景是对普通 DOM 元素进行底层操作,所以我们不能盲目的胡乱的使用自定义指令. 如何声明自定义指令? 就像vue中有全局组件和局部组件一样,他也分全局自定义指令和局部指令. let Opt = { bind:function(e

  • vue中自定义指令directive的详细指南

    目录 一. 什么是自定义指令 二. 如何自定义指令 钩子函数 三.应用场景 输入框防抖 图片懒加载 一键 Copy的功能 拖拽 总结 一. 什么是自定义指令 我们看到的v-开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能,对普通 DOM元素进行底层操作,这时候就会用到自定义指令.除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令 指令使用的几种方式: //会实例化一个指令,但这个指令没有参数 `v-xxx` // -- 将值传到指令中 `v

  • Vue自定义指令实现checkbox全选功能的方法

    最近做的一个项目需要用到Vue实现全选功能,参考了一下网上的做法,发现用属性计算的复用性不高,于是选用自定义指令,但网上的做法大多是会对原始数据有一定的格式要求,而且没有返回结果,于是做了改进. 上代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> <div id

  • vue自定义指令的创建和使用方法实例分析

    本文实例讲述了vue自定义指令的创建和使用方法.分享给大家供大家参考,具体如下: 一.自定义指令的创建和使用 Vue自带的指令很多,v-for/v-if/v-else/v-else-if/v-model/v-bind/v-on/v-show/v-html/v-text... 但是这些指令都是比较偏向于工具化,有些时候在实现具体的业务逻辑的时候,发现不够用,如何来自定义指令. 1.自定义指令 ① 创建 new Vue({ directives:{ change:{ bind:function(){

  • vue自定义指令实现方法详解

    本文实例讲述了vue自定义指令实现方法.分享给大家供大家参考,具体如下: vue中的指令就是v-on v-bind v-show等等,那么自定义指令是什么呢? 自己定义的指令就是自定义指令. 语法: Vue.directive(id, definition) 这里可以参考vue中的指令 <h1 v-if="yes">Yes</h1> 其中,if就是指令ID,yes是expression Vue.directive()传入接受两个参数,id是指指令ID,defin

  • 实现一个Vue自定义指令懒加载的方法示例

    在项目中如果有大量的图片需要加载的时候,就可以考虑使用懒加载了,懒加载其实就是监听浏览器的滚动,当滚动到一定的范围的时候就将图片的真实路径赋给src,然后取消监听.实现的方法也比较简单,可以通过懒加载的插件实现,也可以手写,手写通过vue自定义指令来实现,一般情况自定义指令用的也不多,比较vue自带的就够用了,大型复杂的项目的可能用的多. 什么是图片懒加载 当我们向下滚动的时候图片资源才被请求到,这也就是我们本次要实现的效果,进入页面的时候,只请求可视区域的图片资源这也就是懒加载. 比如我们加载

  • Vue自定义指令使用方法详解

    Vue自定义指令的使用,具体内容如下 1.自定义指令的语法 Vue自定义指令语法如下: Vue.directive(id, definition) 传入的两个参数,id是指指令ID,definition是指定义对象.其中,定义对象可以提供一些钩子函数 2.钩子函数 定义对象的钩子函数如下: 钩子函数的参数 el: 指令所绑定的元素,可以用来直接操作 DOM . binding: 一个对象,包含以下属性: *name: 指令名,不包括 v- 前缀. *value: 指令的绑定值, 例如: v-my

  • vue 自定义指令自动获取文本框焦点的方法

    HTML: <p><b v-show="show">{{tag}}</b><input v-focus v-model="tag" :hidden="show" type="text"></p> js: 官方例子: directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() } } } 我的

随机推荐