vue自定义指令–directive

21次阅读

共计 5755 个字符,预计需要花费 15 分钟才能阅读完成。

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。
对于这几个钩子函数,了解的可以自行跳过,不了解的我也不介绍,自己去官网看,没有比官网上说的更详细的了:钩子函数
项目中的 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