一. 自定义指令 clickoutside 的作用
clickoutside 是点击被绑定元素之外会触发其绑定的事件。
例如:
Select 组件点击组件外部,收起下拉框的功能,就应用到了自定义指令 clickoutside。
<div
class="el-select"
:class="[selectSize ? 'el-select--' + selectSize : '']"
@click.stop="toggleMenu"
v-clickoutside="handleClose">
</div>
二. 自定义指令 clickoutside 源码分析
源代码位置:在 element-ui 源代码的 src/utils/clickoutside.js 文件中。
2.1 clickoutside 指令的实现思路
clickoutside 指令的实现思路主要拆分为如下几步:
-
首先,由于可能会存在多个元素绑定了
clickoutside指令,所以需要设置一个数组变量nodeList来存放绑定指令的元素。 -
bind钩子的处理(指令第一次绑定到元素上时):需要将绑定指令的元素push到nodeList数组中,并且给被绑定元素设置一个特殊的属性,属性下挂载唯一的id、点击时判断是否在元素外部的方法、字符串形式的指令表达式、绑定的方法。 -
update钩子的处理(被绑定元素所在的模板更新时):更新被绑定元素的特殊属性下的除 id 外的其余属性。 -
事件的监听:监听
document的mousedown和mouseup事件,在mousedown事件中,用一个变量存储mousedown事件的event,以便后续判断是否点击的是被绑定元素之外;在mouseup事件中,遍历nodeList数组,判断是否点击的是其元素的外部,如果时,则执行绑定的方法。 -
unbind钩子(指令与元素解绑时):根据 id的对应将nodeList上面对应的id的元素从数组nodeList中移除。
2.2 clickoutside 指令的源码解析
// 引入 vue
import Vue from 'vue';
// Vue.prototype.$isServer 去获取是否是服务端渲染,如果是服务端渲染,则不执行点击事件
const isServer = Vue.prototype.$isServer;
// 定义 nodeList 变量为数组,存放所有绑定 clickoutside 指令的元素
const nodeList = [];
// 应用 el[ctx],给被绑定元素定义 @@clickoutsideContext 属性,将绑定的事件等信息挂载在 el[ctx] 上面
const ctx = '@@clickoutsideContext';
// startClick 变量记录开始点击的事件对象
let startClick;
// seed 变量创建唯一的 ID
let seed = 0;
// 封装监听事件(处理IE浏览器兼容性问题)
const on = (function() {
if (!isServer && document.addEventListener) {
// 判断 document.addEventListener 是否存在,
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
// 不存在,使用 attachEvent 兼容IE浏览器
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();
// 鼠标按下时,将事件对象赋值给 startClick 变量
!isServer && on(document, 'mousedown', e => (startClick = e));
// 鼠标抬起时,遍历 nodeList,调用每个 node 的 documentHandler 方法,并且将鼠标抬起的事件对象和开始点击的事件对象传入
!isServer && on(document, 'mouseup', e => {
nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
// 判断是否点击在绑定元素之外,如果点击发生在元素之外,则执行绑定的方法
function createDocumentHandler(el, binding, vnode) {
return function(mouseup = {}, mousedown = {}) {
// 判断vnode和vnode.context是否存在
// 判断鼠标的按下以及抬起时的元素是否存在
// 判断鼠标的按下或者鼠标的抬起元素是否包含在被绑定元素的内部
// 判断抬起鼠标的元素与被绑定的元素是否是同一元素
// 如果有一处符合的话则 return 掉,不继续执行
if (!vnode ||
!vnode.context ||
!mouseup.target ||
!mousedown.target ||
el.contains(mouseup.target) ||
el.contains(mousedown.target) ||
el === mouseup.target) return;
// 如果全部都不符合,则证明点击的是被绑定元素的外部,执行绑定的方法
if (binding.expression &&
el[ctx].methodName &&
vnode.context[el[ctx].methodName]) {
vnode.context[el[ctx].methodName]();
} else {
el[ctx].bindingFn && el[ctx].bindingFn();
}
};
}
/**
* v-clickoutside
* @desc 点击元素外面才会触发的事件
* @example
* ```vue
* <div v-element-clickoutside="handleClose">
* ```
*/
export default {
// 指令第一次绑定到元素时
bind(el, binding, vnode) {
// 将被绑定的元素 push 到 nodeList 数组中
nodeList.push(el);
// 创建一个唯一的 ID
const id = seed++;
// 被绑定元素挂载 @@clickoutsideContext 属性。将id,元素事件,字符串形式的指令表达式,绑定的值挂载到 @@clickoutsideContext 属性中。
el[ctx] = {
id,
documentHandler: createDocumentHandler(el, binding, vnode),
methodName: binding.expression,
bindingFn: binding.value
};
},
// 被绑定元素所在的模板更新时
update(el, binding, vnode) {
// 更新除了ID外的其他属性,也就是绑定的方法等
el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
el[ctx].methodName = binding.expression;
el[ctx].bindingFn = binding.value;
},
// 指令与元素解绑时调用
unbind(el) {
let len = nodeList.length;
// 遍历 nodeList 数组,如果 nodeList 数组中元素的 ID 与 解绑的元素的 ID 相同,则将该元素从 nodeList 数组中移除
for (let i = 0; i < len; i++) {
if (nodeList[i][ctx].id === el[ctx].id) {
nodeList.splice(i, 1);
break;
}
}
delete el[ctx];
}
};
2.3 clickoutside 指令的应用
<template>
<!-- 使用 v-clickoutside 应用自定义指令 -->
<div
class="el-select"
:class="[selectSize ? 'el-select--' + selectSize : '']"
@click.stop="toggleMenu"
v-clickoutside="handleClose">
</div>
</template>
<script>
// 引入自定义指令
import Clickoutside from 'element-ui/src/utils/clickoutside';
export default {
// 注册自定义指令
directives: { Clickoutside },
}
</script>