Skip to content

elementui-clickoutside分析 #31

@CodeDreamfy

Description

@CodeDreamfy

场景

当页面需要一个浮层操作,需要点击浮层以外的地方将浮层关闭,如果使用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