-
Notifications
You must be signed in to change notification settings - Fork 6
Open
Labels
@CodeDreamfy
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; },
其他理解,后续如果有问题再进行补充。