本章讲解elementUI中用到的指令,自定义指令的基本方法可以参考官网-自定义指令。
说明:本文基于element-ui@2.13.0,源码详见element。
在src/directives下有两个vue指令:mousewheel和repeat-click
mousewheel
在element-ui/packages/table/src/table.vue用到v-mousewheel
指令。
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);
}
};
代码的重点是用到normalizeWheel
:
normalize-wheel
以dependencies的形式存在,主要下面三个问题:
- 解决不同浏览器、不同平台的兼容性问题:
// Browsers
var _ie, _firefox, _opera, _webkit, _chrome;
// Actual IE browser for compatibility mode
var _ie_real_version;
// Platforms
var _osx, _windows, _linux, _android;
// Architectures
var _win64;
// Devices
var _iphone, _ipad, _native;
var _mobile;
- 内部通过normalizeWheel.getEventType可以获取到当前浏览器支持的滚动事件:
normalizeWheel.getEventType = function() /*string*/ {
return (UserAgent_DEPRECATED.firefox())
? 'DOMMouseScroll'
: (isEventSupported('wheel'))
? 'wheel'
: 'mousewheel';
};
- 内部通过
isEventSupported
,检测滚轮监控事件
/**
* Checks if an event is supported in the current execution environment.
* Borrows from Modernizr.
* @param {string} eventNameSuffix Event name, e.g. "click".
* @param {?boolean} capture Check if the capture phase is supported.
* @return {boolean} True if the event is supported.
* @internal
* @license Modernizr 3.0.0pre (Custom Build) | MIT
*/
function isEventSupported(eventNameSuffix, capture) {
if (!ExecutionEnvironment.canUseDOM ||
capture && !('addEventListener' in document)) {
return false;
}
var eventName = 'on' + eventNameSuffix;
var isSupported = eventName in document;
if (!isSupported) {
var element = document.createElement('div');
element.setAttribute(eventName, 'return;');
isSupported = typeof element[eventName] === 'function';
}
if (!isSupported && useHasFeature && eventNameSuffix === 'wheel') {
// This is the only way to test support for the `wheel` event in IE9+.
isSupported = document.implementation.hasFeature('Events.wheel', '3.0');
}
return isSupported;
}
- 不同的浏览器,事件的滚动信息可能在detail、wheelDelta、wheelDeltaY或wheelDeltaX中,还有side scrolling的问题,以及滚动值单位问题,该工具库通过
normalizeWheel
进行了统一处理并对外暴露四个值spinX、spinY、pixelX、pixelY
。
// Reasonable defaults
var PIXEL_STEP = 10;
var LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800;
function normalizeWheel(/*object*/ event) /*object*/ {
var sX = 0, sY = 0, // spinX, spinY
pX = 0, pY = 0; // pixelX, pixelY
// Legacy
if ('detail' in event) { sY = event.detail; }
if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; }
if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
// side scrolling on FF with DOMMouseScroll
if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
sX = sY;
sY = 0;
}
pX = sX * PIXEL_STEP;
pY = sY * PIXEL_STEP;
if ('deltaY' in event) { pY = event.deltaY; }
if ('deltaX' in event) { pX = event.deltaX; }
if ((pX || pY) && event.deltaMode) {
if (event.deltaMode == 1) { // delta in LINE units
pX *= LINE_HEIGHT;
pY *= LINE_HEIGHT;
} else { // delta in PAGE units
pX *= PAGE_HEIGHT;
pY *= PAGE_HEIGHT;
}
}
// Fall-back if spin cannot be determined
if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
return { spinX : sX,
spinY : sY,
pixelX : pX,
pixelY : pY };
}
repeat-click
在el-input-number组件中,点+、-时,会用到v-repeat-click
v-repeat-click
会注册mousedown
事件,当用户连续点击+
时:
- 当用户鼠标左键一直按住不松手,只会触发一次触发
mousedown
的回调,但实际测量el-input-number
发现,输入框中的数字会持续变大,原因就在于mousedown
回调中加入了定时器,当鼠标松开,触发一次mouseup
回调方法,取消该定时器;这也许是directive为什么叫repeat-click的缘故吧; - 如果时间间隔大于100毫秒,那么
mousedown
的回调方法里的setInterval回调就会执行(及handler
,本质上就是执行上图的decrease
或increase
方法);
如果时间间隔小于100毫秒,定时器就会取消; -
mousedown
的回调方法(clear
方法)每次执行时,都会通过once
方法注册并执行一次mouseup
回调; - 在
mouseup
回调中,如果发现距离最近一次点击时间小于100ms,就会执行一次handler
方法,并清除定时器;
import { once, on } from 'element-ui/src/utils/dom';
export default {
bind(el, binding, vnode) {
let interval = null;
let startTime;
const handler = () => vnode.context[binding.expression].apply(); // 调用传入的方法
const clear = () => {
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);
interval = setInterval(handler, 100);
});
}
};
repeat-click
依赖element-ui/src/utils/dom
中的两个方法:once
和on
.
- on
很简单,判断是用addEventListener还是attachEvent来注册事件监控器:
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);
}
};
}
})();
- once
从语义上来看,就是注册事件监听器并且只执行一次,然后取消监听方法:
export const once = function(el, event, fn) {
var listener = function() {
if (fn) {
fn.apply(this, arguments);
}
off(el, event, listener); // 跟`on`方法相反,用来取消事件监听器
};
on(el, event, listener);
};
推荐
ElementUI的结构与源码研究
elementUI——mixins
elementUI——locale,国际化方案
elementU——transitions
elementUI——主题