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


Posted in Javascript onApril 07, 2019

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>

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

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

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

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

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

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

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

组件进行初始化的时候,也就是第一次运行指令的时候,会执行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函数中了。

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

下面我们就来详细看下这个_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)
 })
})

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

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

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>

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

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

Javascript 相关文章推荐
JS和jquery获取各种屏幕的宽度和高度的代码
Aug 02 Javascript
jQuery控制元素显示、隐藏、切换、滑动的方法总结
Apr 16 Javascript
JS实现方向键切换输入框焦点的方法
Aug 19 Javascript
浅谈node.js中async异步编程
Oct 22 Javascript
Js类的静态方法与实例方法区分及jQuery拓展的两种方法
Jun 03 Javascript
AngularJS基础 ng-value 指令简单示例
Aug 03 Javascript
微信小程序 swiper组件详解及实例代码
Oct 25 Javascript
jquery实现下拉框左右选择功能
Feb 21 Javascript
AngularJS表格样式简单设置方法示例
Mar 03 Javascript
JS改变页面颜色源码分享
Feb 24 Javascript
利用vue重构有赞商城的思路以及总结整理
Feb 21 Javascript
JS算法教程之字符串去重与字符串反转
Dec 15 Javascript
浅谈express.js框架中间件(middleware)
Apr 07 #Javascript
详解vue中this.$emit()的返回值是什么
Apr 07 #Javascript
浅谈javascript中的prototype和__proto__的理解
Apr 07 #Javascript
javascrit中undefined和null的区别详解
Apr 07 #Javascript
详解服务端预渲染之Nuxt(介绍篇)
Apr 07 #Javascript
vue设计一个倒计时秒杀的组件详解
Apr 06 #Javascript
js字符串处理之绝妙的代码
Apr 05 #Javascript
You might like
解决GD中文乱码问题
2007/02/14 PHP
PHP循环语句笔记(foreach,list)
2011/11/29 PHP
php生成带logo二维码方法小结
2016/04/08 PHP
php mysql_real_escape_string addslashes及mysql绑定参数防SQL注入攻击
2016/12/23 PHP
PHP实现的Redis多库选择功能单例类
2017/07/27 PHP
PHP递归算法的简单实例
2019/02/28 PHP
JavaScript高级程序设计
2006/12/29 Javascript
JavaScript 学习笔记(四)
2009/12/31 Javascript
高性能web开发 如何加载JS,JS应该放在什么位置?
2010/05/14 Javascript
背景图跟随鼠标移动的Mootools插件实现代码
2011/12/12 Javascript
js实现无需数据库的县级以上联动行政区域下拉控件
2013/08/14 Javascript
js将控件隐藏及display属性的使用介绍
2013/12/30 Javascript
JavaScript闭包实例讲解
2014/04/22 Javascript
php读取sqlite数据库入门实例代码
2014/06/25 Javascript
AngularJS 所有版本下载地址
2016/09/14 Javascript
Angular的模块化(代码分享)
2016/12/26 Javascript
Canvas放置反弹效果随机图形(实例)
2017/08/17 Javascript
JavaScript 中定义函数用 var foo = function () {} 和 function foo()区别介绍
2018/03/01 Javascript
JavaScript ES6常用基础知识总结
2019/02/09 Javascript
Vue.js组件通信之自定义事件详解
2019/10/19 Javascript
[01:09:13]DOTA2-DPC中国联赛 正赛 CDEC vs XG BO3 第三场 1月19日
2021/03/11 DOTA
python动态加载变量示例分享
2014/02/17 Python
Python使用SocketServer模块编写基本服务器程序的教程
2016/07/12 Python
python实现比较类的两个instance(对象)是否相等的方法分析
2019/06/26 Python
Python ellipsis 的用法详解
2020/11/20 Python
日本最新流行服饰网购:Nissen
2016/07/24 全球购物
Keds官方网站:购买帆布运动鞋和经典皮鞋
2016/11/12 全球购物
Sunglasses Shop荷兰站:英国最大的太阳镜独立在线零售商和供应商
2017/01/08 全球购物
STUBHUB日本:购买和出售全球活动门票
2018/07/01 全球购物
教师现实表现材料
2014/02/14 职场文书
2015年公司新年寄语
2014/12/08 职场文书
个人求职信格式范文
2015/03/20 职场文书
小学教学工作总结2015
2015/05/13 职场文书
2019年个人工作总结范文
2019/03/25 职场文书
3050和2060哪个好 性能差多少 差距有多大 谁更有性价比
2022/06/17 数码科技