• 翻了翻element-ui源码,发现一个很实用的指令clickoutside


    前言

    指令(directive)在 vue 开发中是一项很实用的功能,指令可以绑定到某一元素或组件,使功能的颗粒度更精细。今天在翻 element-ui 的源码时,发现一个还挺实用的工具指令,跟大伙分享一下。

    clickoutside 的使用及效果

    该指令的源码在 src/utils 下的 clickoutside.js。它功能是指令需要接收一个函数,当用户鼠标点击的区域在绑定指令的元素之外时,会触发该函数。

    那么使用这个指令能够实现什么功能呢?我想到一个功能,就像我们常用的抽屉组件,在点击抽屉之外的区域时,抽屉就会消失(但 elementui 中不是用这种方式,而是用一个遮罩层实现)。

    接下来我们来看看怎么玩这个指令,很简单,只需要引入这个文件注册指令就好了。

    // main.js
    import Vue from 'vue'
    import clickoutside from 'element-ui/src/utils/clickoutside'
    
    Vue.directive('clickoutside', clickoutside)
    

    使用:

    <div v-show="show" v-clickoutside="handler"><div>
    
    export default {
        data() {
            return {
                show: true
            }
        },
        methods: {
            handler() {
                this.show = false
            }
        }
    }
    

    效果:

    源码分析

    clickoutside 看起来还挺不错,下面看看它是如何实现的。首先是它的指令钩子定义:

    const nodeList = [];
    const ctx = '@@clickoutsideContext';
    
    let seed = 0;
    
    export default {
      // 指令绑定时触发
      bind(el, binding, vnode) {
        // 每次绑定时会把dom元素存放到 nodeList 中
        nodeList.push(el);
        // 创建递增id标识
        const id = seed++;
        // 在dom元素上设置一些属性和方法
        // ctx的作用是一个标识,为了不和原生的属性冲突
        el[ctx] = {
          id,
          // 这个是点击元素区域外时会执行的函数,后面会提到
          documentHandler: createDocumentHandler(el, binding, vnode),
          // 绑定的值表达式,值相当于上面例子中的 "handler" 字符串
          methodName: binding.expression,
          // 绑定的值,值相当于上面例子中的 handler 函数
          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;
        // 找到对应的dom元素,从 nodeList 移除它
        for (let i = 0; i < len; i++) {
          if (nodeList[i][ctx].id === el[ctx].id) {
            nodeList.splice(i, 1);
            break;
          }
        }
        // 移除之前添加的自定义属性
        delete el[ctx];
      }
    };
    

    源码内部会对 docuemnt 鼠标事件进行监听:

    let startClick;
    
    // 鼠标按下时 记录按下元素的事件对象
    !Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));
    
    // 鼠标松开时 遍历 nodeList 中的元素,执行 documentHandler
    !Vue.prototype.$isServer && on(document, 'mouseup', e => {
      nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
    });
    

    接下来最核心的就是 documentHandler 函数,它是由 createDocumentHandler 创建出来的:

    function createDocumentHandler(el, binding, vnode) {
      // 接收参数为:鼠标松开和鼠标按下的事件对象
      return function(mouseup = {}, mousedown = {}) {
        // 这里一系列的判断点击区域是否在元素内,如果在区域内则跳出
        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 是组件实例上下文
          // 就像开头的例子,methodName 是 "handler",通过索引上下文的属性找到 methods 中定义的 handler 函数
          vnode.context[el[ctx].methodName]();
        } else {
          el[ctx].bindingFn && el[ctx].bindingFn();
        }
      };
    }
    

    至此整个指令流程分析就完了。

    小插曲

    在经过一些demo的使用后,发现该指令在某些场景下会出现不理想的效果。例如:抽屉内有 el-select 选择栏时,选择栏的 dom 是挂载到 body 下,导致在点击完选择项后被判断为区域外点击。

    其实这也符合逻辑,因为点击的地方也确实在区域外,只是在这种场景下看起来像是“bug”一样。然后我发现源码里提供了一个选项解决这种问题。可以在使用指令的组件 data 里定义 popperElm 属性,它的值是一个 dom

    export default {
        mounted() {
            this.popperElm = document.querySelector('.el-select-dropdown.el-popper')
        }
    }
    

    在源码里会通过 popperElm 进行判断:

    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;
    

    如果 popperElm 包含鼠标点击的 dom 则跳出逻辑。

    然后我又想到了一个问题,popperElm 只能设置一个,当有多个选择栏组件时,还是会出现上面所说的情况。我的想法是,把 clickoutsidecopy 一份下来,把 popperElm 改成可以接受数组类型,判断时去循环判断,这样应该可以解决问题。

    结语

    clickoutside 不止抽屉的场景,只要你想在点击某个元素区域之外做些事情,都可以考虑它。

    除了这个,还有很多优秀的第三方指令,例如 element-ui 中的 v-loading 可以实现局部的加载动画,常用的 vue-lazyload 中的 v-lazy 可以实现图片的懒加载。

    个人认为指令属于那种用得少但很实用的东西,可能在开发功能时都没有考虑到用指令来实现,如果你还不了解指令,赶快学起来。

    感谢阅读

    欢迎关注公众号【奔跑的前端er】,专注于分享前端技术文章,和大家一起进步。

  • 相关阅读:
    过河卒(Noip2002)
    暑假学习日记2013/7/18
    暑假学习日记2013/7/16
    iOS中利用CoreTelephony获取用户当前网络状态(判断2G,3G,4G) by徐文棋
    隐藏键盘的N种方法
    cell重用
    关于viewControllers之间的传值方式
    iOS开发之工欲善其事,必先利其器
    NSLog输出格式及随机数
    iOS团队代码规范
  • 原文地址:https://www.cnblogs.com/chanwahfung/p/13796966.html
Copyright © 2020-2023  润新知