在element-ui
源码中运用了四个指令,分别为点击元素外,滚轮事件优化,单击事件优化,获取ref指令。这些指令在平时的开发中也会经常用到,下面就来一一介绍这些指令的实现方式以及用途。
1.什么是指令
在理解element-ui
中相关的指令前,先来了解下什么是指令,内置指令以及怎么创建自定义指令。
1.1 指令概念
vue
中指令都是以v-
开头,作用于html
标签,提供一些特殊的特性,当指令被绑定到html
元素的时候,指令会为被绑定的元素添加一些特殊的行为,可以将指令看成html
的一种属性,用于操作DOM
。
1.2 内置指令
vue
中提供了一些内置指令,如下所示:
v-text
:更新元素的textContent
。v-html
:更新元素的innerHTML
。v-show
:根据表达式之真假值,切换元素的display CSS property
。v-if
:条件渲染,用于判断是否显示元素。在切换时元素及它的数据绑定 / 组件被销毁并重建v-else
:配合v-if
一起使用。v-else-if
:配合v-if
一起使用。v-for
:基于源数据多次渲染元素或模板块。v-on
:绑定事件监听器。v-bind
:动态地绑定一个或多个attribute
,或一个组件prop
到表达式。v-model
:在表单控件或者组件上创建双向绑定。v-slot
:提供具名插槽或需要接收 prop 的插槽。v-pre
:跳过这个元素和它的子元素的编译过程。可以用来显示原始Mustache
标签。跳过大量没有指令的节点会加快编译。v-cloak
:这个指令保持在元素上直到关联实例结束编译。v-once
:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
1.3 自定义指令
Vue
推崇数据驱动视图的理念(数据交互,状态管理),但并非所有情况都适合数据驱动( DOM 的操作)。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的 DOM 操作,并且是可复用的。
1.3.1 指令定义
使用Vue.directive(id,definition)
可以进行指令定义。
- id:指令
id
,定义好后,可以直接通过v-{id}
来使用。- definition:对象,该对象提供了一些钩子函数
1.3.2 钩子函数
一个指令定义对象可以提供如下几个钩子函数:
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
- componentUpdated:指令所在组件的
VNode
及其子VNode
全部更新后调用。- unbind:只调用一次,指令与元素解绑时调用。
1.3.3 钩子函数参数
指令钩子函数会被传入以下参数:
-
el
:指令所绑定的元素,可以用来直接操作 DOM。 -
binding
:一个对象,包含以下 property:-
name
:指令名,不包括v-
前缀。 -
value
:指令的绑定值。 -
oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。 -
expression
:字符串形式的指令表达式。 -
arg
:传给指令的参数,可选。 -
modifiers
:一个包含修饰符的对象。
-
-
vnode
:Vue 编译生成的虚拟节点。 -
oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。
2.点击元素边界外
该指令主要是用于判断点击的点是否在绑定元素的范围内。该指令一般用在弹窗中,如经常用到的Popover
组件,下拉搜索
等。具体实现思路如下所示:
- 添加
v-clickoutside="close"
指令- 2.为
document
添加鼠标按下和弹起的事件。- 3.绑定元素,并创建相应的鼠标事件函数。
- 监听鼠标弹起事件,判断点击的点是否在元素外。
- 执行
v-clickoutside
绑定的close
事件。
import Vue from 'vue';
import { on } from 'element-ui/src/utils/dom';
const nodeList = [];
const ctx = '@@clickoutsideContext';
let startClick;
let seed = 0;
// 添加鼠标按下的事件,并缓存event
!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));
// 添加鼠标点击后弹起的事件,遍历nodeList,执行nodeList中元素添加的事件
!Vue.prototype.$isServer && on(document, 'mouseup', e => {
nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
// 创建元素的点击事件
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();
}
};
}
/**
* v-clickoutside
* @desc 点击元素外面才会触发的事件
* @example
* ```vue
*
* ```
*/
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;
for (let i = 0; i < len; i++) {
if (nodeList[i][ctx].id === el[ctx].id) {
nodeList.splice(i, 1);
break;
}
}
delete el[ctx];
}
};
3.单击事件优化
在src/directives
目录下有一个repeat-click.js
文件,该文件就是一个用于优化单击事件的指令,我们平时点击时,正常的点击逻辑是这样的:当用户按住鼠标左键时,会触发mousedown
的回调。但当一直按住鼠标左键不松手时,就不会触发mousedown
的回调,使用该指令就是为了实现一直按住鼠标左键不松手时,也能执行对应的事件,这指令主要用在InputNumber
组件中,当鼠标点击-
或+
不松开时,数字可以持续的进行加减。下面就来看看指令是怎么实现的:
-
- 引入指令文件,
import RepeatClick from 'element-ui/src/directives/repeat-click';
-
- 在
directives
中注册指令。
-
- 使用指令,
v-repeat-click="decrease"
-
- 在
bind
事件中定义一个clear
的函数,用于清除定时器。
- 5.为绑定指令的元素添加
moursedown
事件。
- 6.在
moursedown
回调事件中,定义一个定时器,每100秒执行一次回调函数。
-
- 鼠标弹起时,执行
clear
函数清除定时器。
import { once, on } from '@/utils/dom';
export default {
bind(el, binding, vnode) {
let interval = null;
let startTime;
// 获取指令绑定的事件函数
const handler = () => vnode.context[binding.expression].apply();
// 定义一个清除定时器的函数
const clear = () => {
// 间隔时间小于100毫秒时,继续执行回调函数
if (Date.now() - startTime < 100) {
handler();
}
clearInterval(interval);
interval = null;
};
// 添加点击事件
on(el, 'mousedown', (e) => {
if (e.button !== 0) return;
// 缓存点击时的时间
startTime = Date.now();
// 添加鼠标弹起的事件
once(document, 'mouseup', clear);
// 清除定时器
clearInterval(interval);
// 100毫秒执行一次回调函数
interval = setInterval(handler, 100);
});
}
};
4.滚轮事件优化
在src/directives
目录下有一个mousewheel.js
文件,该指令主要是对鼠标滚动事件进行了优化,使用normalize-wheel
这个库来解决不同浏览器之间的兼容性来获取x
方向和y
方向的滚动偏移量。
import normalizeWheel from 'normalize-wheel';
const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const mousewheel = function(element, callback) {
if (element && element.addEventListener) {
element.addEventListener(isFirefox ? 'DOMMouseScroll' : 'mousewheel', function(event) {
const normalized = normalizeWheel(event);
callback && callback.apply(this, [event, normalized]);
});
}
};
export default {
bind(el, binding) {
mousewheel(el, binding.value);
}
};
5.获取ref指令
在packages/popover/src/
目录下有一个directive.js
文件,该指令主要是用于Popover
组件,用于获取Popover
组件的ref
,
const getReference = (el, binding, vnode) => {
const _ref = binding.expression ? binding.value : binding.arg;
const popper = vnode.context.$refs[_ref];
if (popper) {
if (Array.isArray(popper)) {
popper[0].$refs.reference = el;
} else {
popper.$refs.reference = el;
}
}
};
export default {
bind(el, binding, vnode) {
getReference(el, binding, vnode);
},
inserted(el, binding, vnode) {
getReference(el, binding, vnode);
}
};
总结
element-ui
的指令基本上都介绍完了,平时开发的时候除了使用vue
内置的指令外,应该尽可能多封装一些组件来提升工作效率。