vue feedback 指令开发

功能需求

1
2
3
4
5
6
7
8
<template>
<span v-feedback="{activeClass:'span-active'}"></span>
</template>
<style>
.span-active {
background: #000;
}
</style>

当该span被点时,.span-active生效。此处span可以是任意的html元素或组件

以鼠标操作举例,所谓的点是指左键被按下,按下时样式生效,松开后失效

为什么要使用指令方式

1. 直接使用组件

1
2
3
<feedback :active="{activeClass: 'span-active'}">
<span></span>
</feedback>

由于vue的组件必须包含一个根节点,实际上是

1
2
3
<div class="span-active">
<span></span>
</div>

此时出现了尴尬,span本身是一个inline元素,加上feedback后变成了block,并且feedback被使用的时候并不知道slot进来的元素是什么,如果要知道势必又要增加复杂度。

2. 使用混合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<span></span>
</template>
<script>
import feedback from '@/components/feedback.js'
export default {
mixin: {
feedback
},
data () {
return {
activeClassName: 'span-active'
}
}
}
</script>

mixin是当前使用的方式,但是对使用人员要求更高,需要同时知道两个组件的内部实现。

实现思路

该指令需要干以下几件事:

  1. 为绑定指令的组件添加点击的监听,包括mouseupmousedowntouchestarttouchendtouchcancel
  2. 当事件发生时,为元素添加相应的activeClass
  3. 当元素本身是disabled的时候,去除样式的响应,实际上是去除监听

初始化的时候,先获得绑定的元素,并且添加监听器。在监听到有关事件的时候,添加、删除样式

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
bind (el, binding) {
const className = binding.value.activeClass
const disabled = !!binding.value.disabled
if (!disabled) {
el.addEventListener('mousedown', ()=> {el.classList.add(className)})
el.addEventListener('touchstart', ()=> {el.classList.add(className)})
el.addEventListener('touchcancel', ()=> {el.classList.remove(className)})
el.addEventListener('touchend', ()=> {el.classList.remove(className)})
el.addEventListener('mouseup', ()=> {el.classList.remove(className)})
}
}
}

到此时,一切都很美好。

在实际应用中,这个组件极有可能点击后发生了改变,比如从!disabled转为了disabled状态,需要我们对其进行进一步处理。这个看起来很简单,!disabled看起来并不需要处理,原来的事件应该保留。只有disabled的状况需要把原有的监听器去除掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
bind (el, binding) {
const className = binding.value.activeClass
const disabled = !!binding.value.disabled
if (!disabled) {
el.addEventListener('mousedown', ()=> {el.classList.add(className)})
el.addEventListener('touchstart', ()=> {el.classList.add(className)})
el.addEventListener('touchcancel', ()=> {el.classList.remove(className)})
el.addEventListener('touchend', ()=> {el.classList.remove(className)})
el.addEventListener('mouseup', ()=> {el.classList.remove(className)})
}
},
componentUpdated (el, binding) {
const className = binding.value.activeClass
const disabled = !!binding.value.disabled
if (disabled) {
el.removeEventListener('mousedown', ()=> {el.classList.add(className)})
el.removeEventListener('touchstart', ()=> {el.classList.add(className)})
el.removeEventListener('touchcancel', ()=> {el.classList.remove(className)})
el.removeEventListener('touchend', ()=> {el.classList.remove(className)})
el.removeEventListener('mouseup', ()=> {el.classList.remove(className)})
}
}
}

运行后,发现监听器没有被去除掉,因为 removeEventListener 应该移除之前添加的监听器,因为使用了箭头函数,所以后面的监听器并不是之前的监听器。那我们把箭头函数换成统一的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function addClass (el, className) {
el.classList.add(className)
}

function removeClass (el, className) {
el.classList.remove(className)
}

export default {
bind (el, binding) {
const className = binding.value.activeClass
const disabled = !!binding.value.disabled
if (!disabled) {
el.addEventListener('mousedown', addClass(el, className))
el.addEventListener('touchstart', addClass(el, className))
el.addEventListener('touchcancel', removeClass(el, className))
el.addEventListener('touchend', removeClass(el, className))
el.addEventListener('mouseup', removeClass(el, className))
}
},
componentUpdated (el, binding) {
const className = binding.value.activeClass
const disabled = !!binding.value.disabled
if (disabled) {
el.removeEventListener('mousedown', addClass(el, className))
el.removeEventListener('touchstart', addClass(el, className))
el.removeEventListener('touchcancel', removeClass(el, className))
el.removeEventListener('touchend', removeClass(el, className))
el.removeEventListener('mouseup', removeClass(el, className))
}
}
}

报错了,想当然是不行的。addEventListener 的第二个参数

listener 必须是一个实现了 EventListener 接口的对象,或者是一个函数

如果只传入函数,那又产生了一个问题,addClassremoveCLass无法取得elclassName的。这里面还存在一个作用域的问题。最后想了一个办法,就是利用eventTarget来替代el,并且把className放在Dom上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function addClass (event) {
const el = event.currentTarget
const className = event.currentTarget.getAttribute('data-active-class')
el.classList.add(className)
}

function removeClass (event) {
const el = event.currentTarget
const className = event.currentTarget.getAttribute('data-active-class')
el.classList.remove(className)
}

function feedback (el, className, option) {
el.setAttribute('data-active-class', className)
if (option === 'add') {
el.addEventListener('mousedown', addClass)
el.addEventListener('touchstart', addClass)
el.addEventListener('touchcancel', removeClass)
el.addEventListener('touchend', removeClass)
el.addEventListener('mouseup', removeClass)
} else if (option === 'remove') {
el.removeEventListener('mousedown', addClass)
el.removeEventListener('touchstart', addClass)
el.removeEventListener('touchcancel', removeClass)
el.removeEventListener('touchend', removeClass)
el.removeEventListener('mouseup', removeClass)
}
}
export default {
bind (el, binding) {
const className = binding.value.activeClass
const disabled = !!binding.value.disabled
if (!disabled) {
feedback(el, className, 'add')
}
},
componentUpdated (el, binding) {
const className = binding.value.activeClass
const disabled = !!binding.value.disabled
const oldDisabled = !!binding.oldValue.disabled
if (oldDisabled !== disabled) {
if (disabled) {
feedback(el, className, 'remove')
} else {
feedback(el, className, 'add')
}
}
},
unbind (el, binding) {
const className = binding.value.activeClass
feedback(el, className, 'remove')
}
}

优化及未尽事宜

  1. disabled是不是可以不通过指令传入
  2. 把监听和添加样式从el转到vnode