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>
这样也能达到我们想要的效果。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新动态
- 雨林唱片《赏》新曲+精选集SACD版[ISO][2.3G]
- 罗大佑与OK男女合唱团.1995-再会吧!素兰【音乐工厂】【WAV+CUE】
- 草蜢.1993-宝贝对不起(国)【宝丽金】【WAV+CUE】
- 杨培安.2009-抒·情(EP)【擎天娱乐】【WAV+CUE】
- 周慧敏《EndlessDream》[WAV+CUE]
- 彭芳《纯色角3》2007[WAV+CUE]
- 江志丰2008-今生为你[豪记][WAV+CUE]
- 罗大佑1994《恋曲2000》音乐工厂[WAV+CUE][1G]
- 群星《一首歌一个故事》赵英俊某些作品重唱企划[FLAC分轨][1G]
- 群星《网易云英文歌曲播放量TOP100》[MP3][1G]
- 方大同.2024-梦想家TheDreamer【赋音乐】【FLAC分轨】
- 李慧珍.2007-爱死了【华谊兄弟】【WAV+CUE】
- 王大文.2019-国际太空站【环球】【FLAC分轨】
- 群星《2022超好听的十倍音质网络歌曲(163)》U盘音乐[WAV分轨][1.1G]
- 童丽《啼笑姻缘》头版限量编号24K金碟[低速原抓WAV+CUE][1.1G]