Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

elementui-clickoutside分析 #31

Open
@CodeDreamfy

Description

场景

当页面需要一个浮层操作,需要点击浮层以外的地方将浮层关闭,如果使用vue的话,可以将其注册成directives

研究

通过给全局document绑定mousedownmouseup事件,监听事件方法的event.target是否包含在浮层内,做对应的操作

可以通过vuedirectives指令来实现,参考了element-uiiview后,发现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;
},

其他理解,后续如果有问题再进行补充。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

        AltStyle によって変換されたページ (->オリジナル) /