Vue中内置了不少的指令,如v-model、v-show、v-html等,可是有时候这些指令并不能知足咱们,或者说咱们想为元素附加一些特别的功能,这时候,咱们就须要用到vue中一个很强大的功能了—自定义指令。html
在开始以前,咱们须要明确一点,自定义指令解决的问题或者说使用场景是对普通 DOM 元素进行底层操做,因此咱们不能盲目的胡乱的使用自定义指令。vue
就像vue中有全局组件和局部组件同样,他也分全局自定义指令和局部指令。node
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改成一个函数。app
let Opt = function(el,binding,vnode){ }
对于自定义指令的使用是很是简单的,若是你对vue有必定了解的话。dom
咱们能够像v-text=”’test’”
同样,把咱们须要传递的值放在‘=’号后面传递过去。ide
咱们能够像v-on:click=”handClick”
同样,为指令传递参数’click’。函数
咱们能够像v-on:click.stop=”handClick”
同样,为指令添加一个修饰符。源码分析
咱们也能够像v-once
同样,什么都不传递。
每一个指令,他的底层封装确定都不同,因此咱们应该先了解他的功能和用法,再去使用它。
上面咱们也介绍了,自定义指令一共有5个钩子函数,他们分别是:bind、inserted、update、componentUpdate和unbind。
对于这几个钩子函数,了解的能够自行跳过,不了解的我也不介绍,本身去官网看,没有比官网上说的更详细的了:钩子函数
在项目中,咱们自定义一个全局指令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,咱们应该如何去解决呢?
一、事件解绑,从新绑定
咱们在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) }) })
能够看到,数据已经变成咱们想要的数据了。
二、把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 } })
这样也能达到咱们想要的效果。
三、更新父元素
若是咱们为父元素ul绑定一个变化的key值,这样,当数据变动的时候就会更新父元素,从而从新建立子元素,达到从新绑定指令的效果。
<ul :key="Date.now()"> <li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li> </ul>
这样也能达到咱们想要的效果。