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 相关文章推荐
URL地址中的#符号使用说明
Feb 12 Javascript
jquery索引在使用中的一些困惑
Oct 24 Javascript
JS 屏蔽按键效果与改变按键效果的示例代码
Dec 24 Javascript
chrome下jq width()方法取值为0的解决方法
May 26 Javascript
浅析node.js中close事件
Nov 26 Javascript
jquery删除指定子元素代码实例
Jan 13 Javascript
javascript中sort()的用法实例分析
Jan 30 Javascript
es6+angular1.X+webpack 实现按路由功能打包项目的示例
Aug 16 Javascript
react native与webview通信的示例代码
Sep 25 Javascript
vue watch深度监听对象实现数据联动效果
Aug 16 Javascript
JS实现指定区域的全屏显示功能示例
Apr 25 Javascript
javascript设计模式 ? 享元模式原理与用法实例分析
Apr 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
给多个地址发邮件的类
2006/10/09 PHP
Win2003下APACHE+PHP5+MYSQL4+PHPMYADMIN 的简易安装配置
2006/11/18 PHP
PHP ajax 异步执行不等待执行结果的处理方法
2015/05/27 PHP
Laravel5权限管理方法详解
2016/07/26 PHP
简单的JavaScript互斥锁分享
2014/02/02 Javascript
jQuery插件pagination实现无刷新分页
2016/05/21 Javascript
探讨跨域请求资源的几种方式(总结)
2016/12/02 Javascript
bootstrap vue.js实现tab效果
2017/02/07 Javascript
JS控件bootstrap suggest plugin使用方法详解
2017/03/25 Javascript
AngularJS自定义指令实现面包屑功能完整实例
2017/05/17 Javascript
详解react-router如何实现按需加载
2017/06/15 Javascript
基于JavaScript实现无限加载瀑布流
2017/07/21 Javascript
浅谈JavaScript中的属性:如何遍历属性
2017/09/14 Javascript
JQuery 选择器、DOM节点操作练习实例
2017/09/28 jQuery
微信小程序实现animation动画
2018/01/26 Javascript
three.js实现炫酷的全景3D重力感应
2018/12/30 Javascript
JavaScript判断浏览器版本的方法
2019/11/03 Javascript
JS函数进阶之继承用法实例分析
2020/01/15 Javascript
javascript实现拖拽碰撞检测
2020/03/12 Javascript
python处理文本文件并生成指定格式的文件
2014/07/31 Python
python去除所有html标签的方法
2015/05/05 Python
Python基于pygame实现图片代替鼠标移动效果
2015/11/11 Python
Python使用pickle模块存储数据报错解决示例代码
2018/01/26 Python
python实现Windows电脑定时关机
2018/06/20 Python
Python中正则表达式的用法总结
2019/02/22 Python
基于Python fminunc 的替代方法
2020/02/29 Python
浅谈selenium如何应对网页内容需要鼠标滚动加载的问题
2020/03/14 Python
python装饰器三种装饰模式的简单分析
2020/09/04 Python
python IP地址转整数
2020/11/20 Python
使用css3实现的tab选项卡代码分享
2014/12/09 HTML / CSS
英国知名的皮手套品牌:Dents
2016/11/13 全球购物
泰国的头号网上婴儿用品店:Motherhood.co.th
2019/04/09 全球购物
Geekbuying波兰:购买中国电子产品
2019/10/20 全球购物
网络管理员岗位职责
2015/02/12 职场文书
幼儿教师师德师风自我评价
2015/03/05 职场文书
Python爬虫之用Xpath获取关键标签实现自动评论盖楼抽奖(二)
2021/06/07 Python