Element源码中的clickoutside
在node_moudles的element-ui中的可以找到clickoutside.js的源码 如果我们在模板中想利用Element中的clickoutside,可以注册成局部指令。
<template>
<div v-clickoutside="handleClose" v-show="flag"></div>
</template>
<script>
import Clickoutside from "element-ui/src/utils/clickoutside"
export default{
data(){
return {
flag: true
}
},
directives: { Clickoutside },
methods: {
handleClose(){
this.flag = false;
}
}
}
</script>
自定义指令clickoutside
下面对Element中clickoutside进行了简化,在没有引入element的时候也可以用。
const clickoutsideContext = '@@clickoutsideContext';
export default {
bind(el, binding, vnode) {
console.log(el);
const documentHandler = (e) => {
if (vnode.context && el !== e.target && !el.contains(e.target)) {
vnode.context[el[clickoutsideContext].methodName]();
}
};
el[clickoutsideContext] = {
documentHandler,
methodName: binding.expression,
arg: binding.arg || 'click',
};
document.addEventListener(el[clickoutsideContext].arg, documentHandler, false);
},
update(el, binding) {
el[clickoutsideContext].methodName = binding.expression;
},
unbind(el) {
document.removeEventListener(
el[clickoutsideContext].arg,
el[clickoutsideContext].documentHandler);
},
install(Vue) {
Vue.directive('clickoutside', {
bind: this.bind,
unbind: this.unbind,
});
},
};
在组件中也是要通过注册指令来使用。
<template>
<div v-clickoutside="outFn" v-show="flag"></div>
</template>
<script>
import clickoutside from "./clickoutside"
export default{
data(){
return {
flag: true
}
},
directives: { clickoutside },
methods: {
outFn(){
this.flag = false;
}
}
}
</script>
v-click-outside-x
npm中也有关于clickoutside的插件,例如v-click-outside、v-click-outside-x,个人推荐使用v-click-outside-x
使用v-click-outside-x,
<template>
<div v-click-outside="onClickOutside"></div>
<!-- 阻止冒泡 -->
<div v-click-outside:mouseup="doThis"></div>
<div v-click-outside:mouseup.stop="doThis"></div>
<!-- 去除默认行为 -->
<div v-click-outside.prevent="doThat"></div>
<!-- 阻止冒泡与默认行为 -->
<div v-click-outside.stop.prevent="doThat"></div>
<!-- 事件捕获 -->
<div v-click-outside.capture="doThis"></div>
<!-- 绑定多个事件类型 -->
<div
v-click-outside.mouseup="onClickOutside1"
v-click-outside:click="onClickOutside2"
v-click-outside:dblclick.capture="onClickOutside3"
></div>
</template>
<script>
import * as vClickOutside from 'v-click-outside-x';
export default {
directives: {
clickOutside: vClickOutside.directive,
},
methods: {
onClickOutside (event) {
console.log('Clicked outside. Event: ', event)
},
onClickOutside1(){},
onClickOutside2(){},
onClickOutside3(){}
}
};
</script>
v-click-outside-x源码:
//version: 4.1.0
const CLICK = 'click';
const captureInstances = Object.create(null); //冒泡事件类型对象{}
const nonCaptureInstances = Object.create(null); //捕获事件类型对象{}
const captureEventHandlers = Object.create(null); //冒泡事件处理程序对象{}
const nonCaptureEventHandlers = Object.create(null); //捕获事件处理程序对象{}
const instancesList = [captureInstances, nonCaptureInstances];
const commonHandler = function onCommonEvent(context, instances, event, arg) {
//获取事件触发的目标元素
const { target } = event;
const itemIteratee = function itemIteratee(item) {
//获取指令绑定的元素,虚拟Dom,VNode
const { el } = item;
//如果事件触发的目标元素,不是当前指令绑定的元素,或子元素
//那么就触发事件处理程序,并绑定上下文
if (el !== target && !el.contains(target)) {
const { binding } = item;
if (binding.modifiers.stop) {
event.stopPropagation();
}
if (binding.modifiers.prevent) {
event.preventDefault();
}
//触发事件处理程序,绑定上下文
binding.value.call(context, event);
}
};
instances[arg].forEach(itemIteratee);
};
/**
* @param {boolean} useCapture,默认值是false表示事件冒泡,设置为true表示事件捕获
*/
//这个方法的返回值,是事件处理的核心程序
//每一种事件类型都对应一个方法,这也是事件触发时执行的方法,
//因此在这个方法中,可以拿到事件默认参数event,以及上下文this,事件触发的目标元素
const getEventHandler = function getEventHandler(useCapture, arg) {
if (useCapture) {
if (captureEventHandlers[arg]) {
return captureEventHandlers[arg];
}
captureEventHandlers[arg] = function onCaptureEvent(event) {
commonHandler(this, captureInstances, event, arg);
};
return captureEventHandlers[arg];
}
if (nonCaptureEventHandlers[arg]) {
return nonCaptureEventHandlers[arg];
}
nonCaptureEventHandlers[arg] = function onNonCaptureEvent(event) {
commonHandler(this, nonCaptureInstances, event, arg);
};
return nonCaptureEventHandlers[arg];
};
export const directive = Object.defineProperties(
{},
{
$captureInstances: {
value: captureInstances,
},
$nonCaptureInstances: {
value: nonCaptureInstances,
},
$captureEventHandlers: { //捕获事件处理程序{}
value: captureEventHandlers,
},
$nonCaptureEventHandlers: { //冒泡事件处理程序{}
value: nonCaptureEventHandlers,
},
bind: {
value: function bind(el, binding) {
//自定义指令绑定的值必须是一个函数
if (typeof binding.value !== 'function') {
throw new TypeError('Binding value must be a function.');
}
//如果自定义指令没有传参默认是click,也就是事件处理程序触发的方式,默认是点击操作
const arg = binding.arg || CLICK;
const normalisedBinding = { //取到binding里面的值
...binding,
...{
arg,
//修饰符对象,共有三个属性,默认冒泡、不阻止默认事件,不阻止冒泡,
//当然了指定定义了修饰符时,会覆盖默认值。
modifiers: {
...{
capture: false,
prevent: false,
stop: false,
},
...binding.modifiers,
},
},
};
const useCapture = normalisedBinding.modifiers.capture;
const instances = useCapture ? captureInstances : nonCaptureInstances;
//在冒泡或捕获对象中,如果arg这个属性值不是一个数组,那就变成数组。
//也就是说,每增加一种自定义指令触发事件处理程序的方式,
//都会多一对键值对,例如默认触发的方式是"click",那么{ "click": []},
//如果又增加一个通过双击触发的方式,就会变成{ "click": [], "dblclick": []}
if (!Array.isArray(instances[arg])) {
instances[arg] = [];
}
//当每种事件类型第一次绑定时,我们会在document上监听相同的事件类型,
//并且,每种事件类型,document只会添加一个事件处理程序,也就是getEventHandler(useCapture, arg)
//这个方法的返回值就是事件处理的核心程序!
if (instances[arg].push({ el, binding: normalisedBinding }) === 1) {
if (typeof document === 'object' && document) {
document.addEventListener(arg, getEventHandler(useCapture, arg), useCapture);
}
}
},
},
unbind: {
value: function unbind(el) {
const compareElements = function compareElements(item) {
return item.el !== el;
};
const instancesIteratee = function instancesIteratee(instances) {
//得到了事件类型的数组,eg: ["click", "dblclick"];
const instanceKeys = Object.keys(instances);
if (instanceKeys.length) {
//是否是捕获
const useCapture = instances === captureInstances;
const keysIteratee = function keysIteratee(eventName) {
//在当前解除的事件类型对应的事件处理数组中,找到那么些未解除绑定的事件处理程序
const newInstance = instances[eventName].filter(compareElements);
//如果存在未解除绑定的事件处理程序,那么就将成为最新的事件处理数组,
//意思就是把解除绑定的事件从事件处理数组中删除掉!
if (newInstance.length) {
instances[eventName] = newInstance;
} else {
//如果不存在未解除绑定的事件处理程序,那么就更简单了,直接让document取消对该事件类型的监听!
//并且在事件处理程序{}对象上,删除该属性
if (typeof document === 'object' && document) {
document.removeEventListener(eventName, getEventHandler(useCapture, eventName), useCapture);
}
delete instances[eventName];
}
};
//循环事件类型的数组
instanceKeys.forEach(keysIteratee);
}
};
//指令与元素解除绑定时,循环冒泡对象与捕获对象,
instancesList.forEach(instancesIteratee);
},
},
},
);
export function install(Vue) {
Vue.directive('click-outside', directive);
}
源码解析:
源码中的注释,是本人对这个插件的理解,directives的源码还在学习中!
总结:
clickoutside的实现的思路大致都是相同的,都是通过addEventListener给document添加事件监听器,然后在利用contains判断是不是当前元素或是其内部元素,来触发事件处理程序。
v-click-outside-x插件中,指令绑定的值,只允许是一个函数。在阅读源码的时候可以看到,这个插件设计的巧妙之处在于,无论在多少个元素上使用该自定义指令(clickOutside),每一种事件类型比如“click、mouseup、dblclick”,都只会给document上面绑定一个事件,在内部通过循环来处理多个事件处理程序,性能有很大提升,而且还增加了一些类似修饰符的功能。