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上面绑定一个事件,在内部通过循环来处理多个事件处理程序,性能有很大提升(在题外篇会介绍性能提升的原因),而且还增加了一些类似修饰符的功能。
题外篇:
要点一:
这里要说的第一点就是,在使用事件时例如click,需要注意事件处理程序的this指向的是事件触发的元素,也就是本身,但是内部的event.target指向的是事件触发的目标元素,不一定就是本身,因此我们在做clickoutside的时候要用event.target去判断,而不能用this,举个例子:
<div id="box" style="padding: 20px;background-color:#ccc;">
<button id="button">按钮</button>
</div>
<script>
var box = document.getElementById("box");
var button = document.getElementById("button");
button.addEventListener( "click", function(event){
console.log("我是button事件一!");
Promise.resolve().then(function(){
console.log( "我是buttton中的Promise实例1!");
});
});
button.addEventListener( "click", function(event){
console.log("我是button事件二!");
var that = this;
Promise.resolve().then(function(){
console.log( "我是buttton中的Promise实例2!");
console.log( "button: " + (that === event.target));
});
});
box.addEventListener( "click", function(event){
console.log("我是box事件!");
var that = this;
Promise.resolve().then(function(){
console.log( "我是box中的Promise实例!");
console.log( "box: " + (that === event.target));
});
});
// 我是button事件一!
// 我是buttton中的Promise实例1!
// 我是button事件二!
// 我是buttton中的Promise实例2!
// button: true
// 我是box事件!
// 我是box中的Promise实例!
// box: false
</script>
当我们点击button时,button是click事件触发的目标元素,因此this === event.target为true。我们也知道这个click事件会默认冒泡到父元素box上面,box上的click事件也会触发,内部this指向box,但是event.target指向的目标元素,事件触发的元素,因此的event.target永远指向button,所以box里面的this === event.target为false。
同时我们还要注意一个细节,就是我们在button监听了两个事件,当button上的两个事件都触发之后,才会冒泡到父元素box上面,触发它的click事件。我们也知道用户操作触发的事件是一个宏任务,而Promise.then是一个微任务。当前click产生的微任务是在本轮事件循环中执行的,那么我们就可以看出,button上的两个事件产生了两个宏任务,box上面产生了一个宏任务!两个事件产生了两个宏任务,如果事件里面有对应的dom操作,还会在事件循环的末尾增加一次UI渲染的操作,其实是对性能极大的浪费。
此时,我们想到了click-outsde-x插件上面的处理,它定义了一个事件处理程序的数组,一种事件类型只会document上面绑定一个处理事件,循环的处理事件的核心程序,这样无论在多少个元素上面添加自定义指令,一种事件类型触发时也只会产生一个宏任务,这对性能来说是一个极大的提升,可能会减少很多次的事件循环!这也是个人推荐使用click-outside-x这个插件的主要原因!
要点二:
第二点,v-click-outside-x这个插件可通过Vue.use,变成全局使用
import Vue from 'vue';
import * as vClickOutside from 'v-click-outside-x';
Vue.use(vClickOutside);
<script>
export default {
methods: {
onClickOutside(event) {
console.log('Clicked outside. Event: ', event);
},
},
};
</script>
<template>
<div v-click-outside="onClickOutside"></div>
</template>
接下来了解一下Vue.use,二话不说,上源码
Vue.use = function (plugin) {
var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if (installedPlugins.indexOf(plugin) > -1) {
return this;
}
// additional parameters
var args = toArray(arguments, 1);
args.unshift(this);
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
plugin.apply(null, args);
}
installedPlugins.push(plugin);
return this;
};
use是Vue构造函数上的一个静态方法,作用就是注册全局插件。
这段代码好像也没啥好解释的,如果插件的plugin.install是方法就执行,否则如果plugin是方法也去执行。
export function install(Vue) {
Vue.directive('click-outside', directive);
}
Vue.use(vClickOutside);
这样就可以看出,调用use这个方法就是全局添加了click-outside
这个自定义指令。
再看一个例子:
Vue.use(VueRouter);
Vue.mixin({
beforeCreate: function beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed: function destroyed () {
registerInstance(this);
}
});
Object.defineProperty(Vue.prototype, '$router', {
get: function get () { return this._routerRoot._router }
});
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
Vue.component('router-view', View);
Vue.component('router-link', Link);
var strats = Vue.config.optionMergeStrategies;
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}
Vue实例上的$router、$route
,就是从这而来!