准备:自定义指令介绍
除了核心功能默认内置的指令 (
v-model
和v-show
等),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。———Vue官网
作为使用Vue
的开发者,我们对Vue
指令一定不陌生,诸如v-model
、v-on
、v-for
、v-if
等,同时Vue也为开发者提供了自定义指令的api,熟练的使用自定义指令可以极大的提高了我们编写代码的效率,让我们可以节省时间开心的摸鱼~
对于Vue的自定义指令相信很多同学已经有所了解,自定义指令的具体写法这里就不细讲了,官方文档很详细。 但是不知道各位同学有没有这种感觉,就是这个技术感觉很方便,也不难,我也感觉学会了,就是不知道如何去应用。这篇文档就是为了解决一些同学的这些问题才写出来的。
PS:这次要讲的自定义指令我们主要使用的是vue2.x
的写法,不过vue3.x
不过是几个钩子函数有所改变,只要理解每个钩子函数的含义,两者的用法差别并不大。
试炼:实现v-mymodel
我的上篇文章说到要自己实现一个v-model
指令,这里使用v-myodel
模拟一个简易版的,顺便再领不熟悉的同学熟悉一下自定义指令的步骤和注意事项。
定义指令
首先梳理思路:原生input
控件与组件的实现方式需要区分,input
的实现较为简单,我们先实现一下input
的处理。
首先我们先定义一个不做任何操作的指令
代码语言:javascript复制Vue.directive('mymodel', {
//只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
bind(el, binding, vnode, oldVnode) {
},
//被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中),需要父节点dom时使用这个钩子
inserted(el, binding, vnode, oldVnode) {
},
//所在组件的 VNode 更新时调用,**但是可能发生在其子 VNode 更新之前**。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
update(el, binding, vnode, oldVnode) {
},
//指令所在组件的 VNode **及其子 VNode** 全部更新后调用。
componentUpdated(el, binding, vnode, oldVnode) {
},
只调用一次,指令与元素解绑时调用。
unbind(el, binding, vnode, oldVnode) {
},
})
上面的注释中详细的说明了各个钩子函数的调用时机,因为我们是给组件上添加input
事件和value
绑定,因此我们在bind
这个钩子函数中定义即可。所以我们把其他的先去掉,代码变成这样。
Vue.directive('mymodel', {
//只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
bind(el, binding, vnode, oldVnode) {
}
})
简单说一下bind
函数的几个回调参数,el
是指令绑定组件对应的dom
,binding
是我们的指令本身,包含name
、value
、expression
、arg
等,vnode
就是当前绑定组件对应的vnode
结点,oldVnode
就是vnode
更新前的状态。
接下来我们要做两件事:
- 绑定
input
事件,同步input
的value
值到外部 value
值绑定,监听value
的变化,更新到input
的value
这对于input
原生组件比较容易实现:
//第一步,添加inout事件监听
el.addEventListener('input', (e) => {
//context是input所在的父组件,这一步是同步数据
vnode.context[binding.expression] = e.target.value;
})
//监听绑定的变量
vnode.context.$watch(binding.expression, (v) => {
el.value = v;
})
这里解释一下上面的代码,vnode.context
是什么呢,他就是我们指令所在组件的上下文环境,可以理解就是指令绑定的值所在的组件实例。不熟悉vnode
结构的同学建议先看一下官方的文档,不过文档描述的比较简单,不是很全面,所以最好在控制台log
一下vnode
的对象看一下它具体的结构,这很有助于我们封装自定义指令,对理解Vue
原理也很有帮助。
我们可以通过context[binding.expression]
获取v-model上到绑定的值,同样可以修改它。上面的代码中我们首先通过在添加的input事件中操作vnode.context[binding.expression] = e.target.value
同步input
的value
值到外部(context
),与使用@input
添加事件监听效果是一样的;然后我们需要做第二件事,做value
值的绑定,监听value
的变化,同步值的变更到input
的value
上,我们想到我们可以使用Vue实例上的额$watch
方法监听值的变化,而context
就是那个Vue
实例,binding.expression
就是我们想要监听的属性,如果我们这样写
<input v-mymodel='message'/>
那么binding.expression
就是字符串'message'
。所以我们想下面的代码这样监听绑定的响应式数据。
//监听绑定的变量
vnode.context.$watch(binding.expression, (v) => {
el.value = v;
})
至此,input
的v-mymodel
的处理就完成了(当然input
组件还有type
为checkbox
,radio
,select
等类型都需要去特别处理,这里就不再一一处理了,感兴趣的同学可以自己尝试去完善一下),但是对于非原生控件的组件,我们要特殊处理。
因此我们完善代码如下:
代码语言:javascript复制Vue.directive('mymodel', {
//只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
bind(el, binding, vnode, oldVnode) {
//原生input组件的处理
if(vnode.tag==='input'){
//第一步,添加inout事件监听
el.addEventListener('input', (e) => {
//context是input所在的父组件,这一步是同步数据
vnode.context[binding.expression] = e.target.value;
})
//监听绑定的变量
vnode.context.$watch(binding.expression, (v) => {
el.value = v;
})
}else{//组件
}
}
})
接下来我们要处理的是自定义组件的逻辑,
代码语言:javascript复制//vnode的结构可以参见文档。不过我觉得最直观的方法就是直接在控制台打印处理
let {
componentInstance,
componentOptions,
context
} = vnode;
const {
_props
} = componentInstance;
//处理model选项
if (!componentOptions.Ctor.extendOptions.model) {
componentOptions.Ctor.extendOptions.model = {
value: 'value',
event: 'input'
}
}
let modelValue = componentOptions.Ctor.extendOptions.model.value;
let modelEvent = componentOptions.Ctor.extendOptions.model.event;
//属性绑定,这里直接修改了属性,没有想到更好的办法,友好的意见希望可以提出
_props[modelValue] = binding.value;
context.$watch(binding.expression, (v) => {
_props[modelValue] = v;
})
//添加事件处理函数,做数据同步
componentInstance.$on(modelEvent, (v) => {
context[binding.expression] = v;
})
声明一下,上面的实现不是vue
源码的实现方式,vue
源码中实现v-model
更加复杂一点,是结合自定义指令、模板编译等去实现的,因为我们是应用级别的封装,所以采用了上述的方式实现。
实现此v-mymodel
需要同学去多了解一下Vnode
和Component
的API
,就像之前说的,最简单的方法就是直接在控制台中直接打印出vnode
对象,组件的vnode
上有Component
的实例componentInstance
。
接下来简单说一下上面的代码,首先我们可以在componentOptions.Ctor.extendOptions
上找到model
的定义,如果没有的话需要设置默认值value
和input
,然后分别对想原生input
的处理一样,分别监听binding.expression
的变化和modelEvent
事件即可。
需要注意的是,我们上面的代码直接给_prop
做了赋值操作,这实际上是不符合规范的,但是我目前没有找到更好的方法去实现,有好思路的同学可以在评论区留言指教。
下面