element ui源码中 v-clickoutside指令学习

328 阅读2分钟
  • 前置知识

directive指令

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

vue内置

  • Vue.prototype.$isServer   vue内置属性 当前 Vue 实例是否运行于服务器。

// element ui 公用函数  on
/* istanbul ignore next */
 const on = (function() {
    //  isServer 是否运行于服务器。 且支持document.addEventListener
  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) {
        //   ie支持attachEvent
        element.attachEvent('on' + event, handler);
      }
    };
  }
})();

// 监听点击空白区域触发效果的节点数组
const nodeList = []
// 存储鼠标按下(mousedown) 时 携带的系统参数
let startClick
// 给每一个使用此指令的节点添加id标识
let seed = 0;
// 在每个节点上存储一个对象  ctx:'@clickoutsideContext'

let ctx = '@clickoutsideContext'
// ctx 此属性含有 
 {
      id,//唯一标识
      documentHandler: createDocumentHandler(el, binding, vnode),//此函数用于判断是否点击了页面空白处。 'docuent.addEventListener[mouseup]'回调中遍历nodeList调用每个节点中的documentHandler函数 若当前组件存在且点击了页面空白处 则执行 bindingFn方法(绑定指令时绑定的回调函数)
      methodName: binding.expression,
      bindingFn: binding.value//绑定指令时绑定的回调函数,点击组件之外触发
}

!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));

!Vue.prototype.$isServer && on(document, 'mouseup', e => {
  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});

// documentHandler绑定createDocumentHandler
function createDocumentHandler(el, binding, vnode) {
    // 指令执行时  先将el, binding, vnode通过闭包手段存了起来保证后续能拿到对应信息

  return function(mouseup = {}, mousedown = {}) {
    //   mouseup = {}, mousedown = {} 一开始就赋值为空对象可以很好的避免参数为undefined时读取属性名报错 mousedown对象通过startClick进行传递
    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();
    }
  };
}


/**
 * v-clickoutside
 * @desc 点击元素外面才会触发的事件
 * @example
 * ```vue
 * <div v-element-clickoutside="handleClose">
 * ```
 */
export default {
  bind(el, binding, vnode) {
    nodeList.push(el);
    const id = seed++;
    el[ctx] = {
      id,
      documentHandler: createDocumentHandler(el, binding, vnode),
      methodName: binding.expression,
      bindingFn: binding.value
    };
  },

  update(el, binding, vnode) {
    //   节点更新了 重新赋值
    el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
    el[ctx].methodName = binding.expression;
    el[ctx].bindingFn = binding.value;
  },

  unbind(el) {
    let len = nodeList.length;
    // 解绑时意味着组件销毁   将node从nodeList中剔除
    for (let i = 0; i < len; i++) {
      if (nodeList[i][ctx].id === el[ctx].id) {
        nodeList.splice(i, 1);
        break;
      }
    }
    delete el[ctx];
  }
};