详解Element 指令clickoutside源码分析


Posted in Javascript onFebruary 15, 2019

clickoutside是Element-ui实现的一个自定义指令,顾名思义,该指令用来处理目标节点之外的点击事件,常用来处理下拉菜单等展开内容的关闭,在Element-ui的Select选择器、Dropdown下拉菜单、Popover 弹出框等组件中都用到了该指令,所以这个指令在实现一些自定义组件的时候非常有用。

要分析该源码,首先要了解一下Vue的自定义指令。自定义指令的定义方式如下:

// 注册一个全局自定义指令 
Vue.directive('directiveName', {
 bind: function(el, binding, vnode){
  // 当指令第一次绑定到元素时调用,常用来进行一些初始化设置
 	...
 },
 inserted: function(el, binding, vnode){
  // 当被绑定的元素插入到 DOM 中时……
 	...
 },
 update: function(el, binding, vnode, oldVnode){
  // 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前
 	...
 },
 componentUpdated: function(el, binding, vnode, oldVnode){
  // 指令所在组件的 VNode 及其子 VNode 全部更新后调用
 	...
 },
 unbind: function(el, binding, vnode){
  // 只调用一次,指令与元素解绑时调用,类似于beforeDestroy的功能
 	...
 }
});

可以看到在配置对象中只有5个可选的钩子函数,他们的参数有4个,分别是 el、binding、vnode、oldVnode

  • el :指令所绑定的元素,可以用来直接操作DOM
  • binding : 一个包含了自定义详细信息的对象,内部收集了使用自定义指令时传入的值、修饰符、参数等数据,详细信息可以在官方文档见到,已经说的十分详细了
  • vnode : Vue编译生成的虚拟节点
  • oldVnode: 本次Vnode更新之前,上一次产生的虚拟节点,仅在  update  和  componentUpdated  钩子中可用。

看完了自定义指令的内容,接下来我们就来分析clickoutside的具体实现。

import Vue from 'vue';
import { on } from 'element-ui/src/utils/dom';

const nodeList = [];
const ctx = '@@clickoutsideContext';

let startClick;
let seed = 0;

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

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

function createDocumentHandler(el, binding, vnode) {
 return function(mouseup = {}, mousedown = {}) {
  ...
 };
}

let startClick;
let seed = 0;

export default {
 bind(el, binding, vnode) {
  ...
 },

 update(el, binding, vnode) {
  ...
 },

 unbind(el) {
  ...
 }
};

上面是简化后的源码,可以看到首先引入Vue和一个用来进行事件绑定的工具函数on,然后定义了两个全局常量 nodeListctx 。nodeList 是一个 元素搜集器 ,会将页面中所有绑定了clickoutside指令的dom元素存储起来,而ctx定义了一个命名空间(必须比较特殊,防止和其它特性重名), 后面会将它添加为元素el的properties ,具体后面会分析到。

接着利用之前引入的Vue进行判断,非服务端则给文档对象添加 mousedownmouseup 事件,在 mousedown 事件回调中,将事件对象存储到 startClick 全局变量中,在 mouseup 事件回调中遍历 nodeList ,然后 分别执行每一个node( 即之前存储起来的clickoutside指令绑定的元素el ) ctx 特性中存储的 documentHandler 函数 。关于ctx property的值会在后面介绍。

最后就是导出了一个 clickoutside 的配置对象,在用到 clickoutside 指令的组件中导入该配置对象,然后在组件中局部注册后就可以使用了。

该配置对象中使用了 bind、update、unbind 三个钩子函数来定义clickoutside指令,主要做的事情就是搜集该自定义指令的相关信息,然后存储到 el 的 ctx 特性上。接下来具体来看一下这个搜集过程。

首先是bind钩子函数:

bind(el, binding, vnode) {
 nodeList.push(el);
 const id = seed++;
 el[ctx] = {
  id,
  documentHandler: createDocumentHandler(el, binding, vnode),
  methodName: binding.expression,
  bindingFn: binding.value
 };
}

这里首先将el直接push到nodeList中,这样每次有clickoutside指令绑定到页面上,都会将绑定元素存储到nodeList当中去,即前面说过的 元素搜集器 。接下来将全局变量seed++,并且赋值给一个临时变量id,最后就是给el的ctx特性赋值了,它的值是一个对象,内部包括了:

id :前面生成的全局唯一id,用来标识该clickoutside指令

documentHandler :利用 createDocumentHandler 生成的一个回调函数。前面的分析中说到,给页面绑定的mouseup事件回调中,会遍历nodeList,分别执行每一个绑定元素el的ctx特性上的documentHandler函数, 这个函数就是在这里生成的 ,至于这个回调函数究竟是做了什么,后面再详细分析。

methodName :binding.expression,查看自定义指令的文档可以知道, binding.expression 的值是字符串形式的指令表达式。例如有   <div v-my-directive="1 + 1"></div> ,则 binding.expression 的值为  1 + 1

bindingFn : binding.value,指令的绑定值,还是上面的例子,则 binding.value 的值是 2 (1 + 1等于2),即 指令的值为js表达式的情况下, **binding.expresssion** 为表达式本身,是一个字符串,而 **binding.value** 是该表达式的值。

接着我们看下 update 钩子:

update(el, binding, vnode) {
	el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
	el[ctx].methodName = binding.expression;
	el[ctx].bindingFn = binding.value;
}

可以看到update钩子的内容很简单,就是当组件更新的时候,更新 绑定元素 el 的特性 ctx 中的值。

再接着我们看看最后一个钩子 unbind :

unbind(el) {
 let len = nodeList.length;

 for (let i = 0; i < len; i++) {
  if (nodeList[i][ctx].id === el[ctx].id) {
   nodeList.splice(i, 1);
   break;
  }
 }
 delete el[ctx];
}

这个钩子也很简单,就是当 clickoutside 指令与元素el解绑的时候,遍历 nodeList ,通过ctx特性上的id找到 nodeList 中存储的当前解绑元素el,将它从nodeList中删除,并且删除el上的ctx特性。

以上就是clickoutside指令配置对象中做的所有操作,总结起来就是:

当指令与元素绑定以及组件更新的时候,搜集并设置绑定元素的ctx特性,同时将绑定元素添加到nodeList当中去,当指令与元素解绑的时候,删除nodeList中存储的对应的绑定元素,并将之前设置在绑定元素上之前设置的ctx特性删除掉。

前面说过,给页面绑定的mouseup事件回调中,会遍历nodeList,分别执行搜集起来的每一个绑定元素el上的ctx特性中的 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[el[ctx].methodName]();
  } else {
   el[ctx].bindingFn && el[ctx].bindingFn();
  }
 };
}

可以看到,这个函数利用了闭包将传入的参数缓存起来,然后返回一个函数。在这个返回的函数中,会进行一系列判断,首先在第一个if里面,判断了:

  • vnode.context 是否存在,不存在退出
  • mouseup.target 是否存在,不存在退出
  • mousedown.target 是否存在,不存在退出
  • 绑定对象el是否包含 mouseup.target/mousedown.target 子节点,如果包含说明点击的是绑定元素的内部,则不执行 clickoutside 指令内容
  • 绑定对象el是否等于 mouseup.target ,等于说明点击的就是绑定元素自身,也不执行 clickoutside 指令内容
  • 最后 vnode.context.popperElm 这部分内容则是 : 判断是否点击在下拉菜单的上,如果是,也是没有点击在绑定元素外部,不执行clickoutside指令内容

详解Element 指令clickoutside源码分析

如图,如果点击在红色区域内,则全部不触发 clickoutside 指令的逻辑。

如果以上条件全部符合,则判断闭包缓存起来的值,如果 methodName 存在则执行这个方法,如果不存在则执行 bindingFn 。例如:

<template>
	<div v-clickoutside="handleClose"></div>
</template>

<script>
 export default {
  data(){
   return {
    visible: false
   };
  },

  methods: {
   handleClose(){
    this.visible = false;
   }
  }
 }
</script>

在这个例子中, methodName 或者 bindingFn 就是通过指令传入的 handleClose 方法。执行该方法,就可以执行 clickoutside 指令的逻辑了

以上就是 documentHandler 方法的生成以及内部逻辑。通过这个方法和之前的分析,我们就可以知道,当页面绑 mouseup 事件触发的时候,会遍历 nodeList ,依次执行每一个绑定元素el的ctx特性上的 documentHandler 方法。而在这个方法内部可以访问到指令传入的表达式,在进行一系列判断之后会执行该表达式,从而达到点击目标元素外部执行给定逻辑的目的,而这个给定逻辑是通过自定义指令的值,传到绑定元素el的ctx特性上的。

至此 clickoutside 的源码就分析完了,可以看到 clickoutside 指令的源码并不复杂,不过涉及到的内容还是挺多的,有许多东西值得我们学习,比如利用dom元素的特性来存储额外信息,使用闭包缓存变量,如何判断点击在目标元素外部和Vue自定义指令的使用等等。

Javascript 相关文章推荐
javascript中callee与caller的用法和应用场景
Dec 08 Javascript
javascript 通用loading动画效果实例代码
Jan 14 Javascript
JavaScript中获取高度和宽度函数总结
Oct 08 Javascript
使用RequireJS优化JavaScript引用代码的方法
Jul 01 Javascript
原生JavaScript编写canvas版的连连看游戏
May 29 Javascript
javaScript给元素添加多个class的简单实现
Jul 20 Javascript
BootStrap实现响应式布局导航栏折叠隐藏效果(在小屏幕、手机屏幕浏览时自动折叠隐藏)
Nov 30 Javascript
JavaScript实现获取远程的html到当前页面中
Mar 26 Javascript
jquery实现简单实用的轮播器
May 23 jQuery
Vue中之nextTick函数源码分析详解
Oct 17 Javascript
微信小程序实现Session功能及无法获取session问题的解决方法
May 07 Javascript
vue实现标签云效果的方法详解
Aug 28 Javascript
Node.js原生api搭建web服务器的方法步骤
Feb 15 #Javascript
jQuery实现简单的Ajax调用功能示例
Feb 15 #jQuery
vue与bootstrap实现简单用户信息添加删除功能
Feb 15 #Javascript
微信小程序实现工作时间段选择
Feb 15 #Javascript
微信小程序实现展示评分结果功能
Feb 15 #Javascript
微信小程序时间标签和时间范围的联动效果
Feb 15 #Javascript
微信小程序实现商品属性联动选择
Feb 15 #Javascript
You might like
汉字转化为拼音(php版)
2006/10/09 PHP
php 论坛采集程序 模拟登陆,抓取页面 实现代码
2009/07/09 PHP
PHP实现多图片上传类实例
2014/07/26 PHP
php+mysqli数据库连接的两种方式
2015/01/28 PHP
PHP引用的调用方法分析
2016/04/25 PHP
php实现的redis缓存类定义与使用方法示例
2017/08/09 PHP
newxtree.js代码
2007/03/13 Javascript
身份证号码前六位所代表的省,市,区, 以及地区编码下载
2007/04/12 Javascript
jquery json 实例代码
2010/12/02 Javascript
用js实现控件的隐藏及style.visibility的使用
2013/06/14 Javascript
cookie中的path与domain属性详解
2013/12/18 Javascript
JavaScript实现的使用键盘控制人物走动实例
2014/08/27 Javascript
基于jQuery+PHP+Mysql实现在线拍照和在线浏览照片
2015/09/06 Javascript
微信小程序多张图片上传功能
2017/06/07 Javascript
浅谈react.js中实现tab吸顶效果的问题
2017/09/06 Javascript
JavaScript实现打印星型金字塔功能实例分析
2017/09/27 Javascript
Vuex实现计数器以及列表展示效果
2018/03/10 Javascript
Angular实现模版驱动表单的自定义校验功能(密码确认为例)
2018/05/17 Javascript
Vue EventBus自定义组件事件传递
2018/06/25 Javascript
jquery实现搜索框功能实例详解
2018/07/23 jQuery
使用webpack将ES6转化ES5的实现方法
2019/10/13 Javascript
小程序自定义模板实现吸顶功能
2020/01/08 Javascript
Vue Object.defineProperty及ProxyVue实现双向数据绑定
2020/09/02 Javascript
Django中几种重定向方法
2015/04/28 Python
python使用pygame实现笑脸乒乓球弹珠球游戏
2019/11/25 Python
PyQt5中多线程模块QThread使用方法的实现
2020/01/31 Python
Python基于pyjnius库实现访问java类
2020/07/31 Python
IE8下CSS3选择器nth-child() 不兼容问题的解决方法
2016/11/16 HTML / CSS
css3编写浏览器背景渐变背景色的方法
2018/03/05 HTML / CSS
html5的canvas元素使用方法介绍(画矩形、画折线、圆形)
2014/04/14 HTML / CSS
H5离线存储Manifest原理及使用
2020/04/28 HTML / CSS
美国婚礼装饰和活动用品批发供应商:Event Decor Direct
2018/10/12 全球购物
求职自荐信怎么写
2014/03/06 职场文书
2014副镇长民主生活会个人对照检查材料思想汇报
2014/09/30 职场文书
创业计划书之DIY自助厨房
2019/09/06 职场文书
Java中CyclicBarrier和CountDownLatch的用法与区别
2021/08/23 Java/Android