-
Notifications
You must be signed in to change notification settings - Fork 6
Open
Labels
Description
场景
当页面需要一个浮层操作,需要点击浮层以外的地方将浮层关闭,如果使用vue的话,可以将其注册成directives
研究
通过给全局document绑定mousedown和mouseup事件,监听事件方法的event.target是否包含在浮层内,做对应的操作
可以通过vue的directives指令来实现,参考了element-ui与iview后,发现element-ui的要好一些,iview的使用了一个clickouotside-x的库,使用后未达到效果,最后还是用了element-ui的。
源码分析
第一步
因为可能会点开多个弹层,这个时候需要针对每一个进行区分,我们通过给全局设置一个累加器来作为每一个浮层的id,然后将每一个弹层的el添加到一个全局的数组中,方便随时销毁。
const nodeList = []; // 存储每次点击触发浮层的el
const ctx = '@@clickoutsideContext'; // 标识el下对应的对象名
let startClick; // 是否按下mouseup
let seed = 0; // 累加器,id生成器vue的directives包含了几个钩子函数,这里主要有以下几个
- bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted: 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
- componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind: 只调用一次,指令与元素解绑时调用。
我们只用了三个:bind,update,unbind
第二步
指令的核心方法,就是判断当前点击位置是否是在浮层的内部还是外部。
核心判断除了进行容错判断外主要判断了当前el是否包含鼠标点击的dom,如果存在则返回,不存在再进行一次容错判断,主要是为了执行指令所绑定的value
里面一些判断用到了vnode的api,但是目前发现popperElm官方源码里面并未存在,应该可以删除掉对应判断
// 监听document的mousedown事件
(!isServer) && on(document, 'mousedown', e => (startClick = e));
// 监听document的mouseup事件,回调里面依次将nodeList所有el进行一次执行与匹配
(!isServer) && on(document, 'mouseup', (e) => {
nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
// on 为封装的事件绑定
export const on = (function() {
if (!isServer && document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();
// 一个闭包函数
function createDocumentHandler(el, binding, vnode) {
return function (mouseup = {}, mousedown = {}) { // 传入鼠标按下和按上的el
if (!vnode
|| !vnode.context
|| !mouseup.target
|| !mousedown.target
|| el.contains(mouseup.target)
|| el.contains(mousedown.target)
|| el === mouseup.target
|| (vnode.context.popperElm
&& (vnode.context.popperElm.contains(mouseup.target)
|| vnode.context.popperElm.contains(mousedown.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();
}
};
}第三步
指令主体步骤: 绑定时候的初始化与销毁时候的处理操作
bind(el, binding, vnode) {
nodeList.push(el); // 将当前浮层dom元素添加到全局数组中
const id = seed++; // 累加器,id生成器
el[ctx] = { // ctx标识了是clickoutside对象
id,
documentHandler: createDocumentHandler(el, binding, vnode), // 用来进行判断的方法,返回一个函数
methodName: binding.expression, // 指令绑定值得字符串表达式
bindingFn: binding.value, // 指令绑定的值
};
},unbind(el) {
const len = nodeList.length;
for (let i = 0; i < len; i++) { // 遍历el数组,匹配到对应的el进行销毁删除
if (nodeList[i][ctx].id === el[ctx].id) {
nodeList.splice(i, 1);
break;
}
}
delete el[ctx];
},因为对update不是很熟悉,所以只贴出代码
update(el, binding, vnode) {
el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
el[ctx].methodName = binding.expression;
el[ctx].bindingFn = binding.value;
},其他理解,后续如果有问题再进行补充。