ElementUI源码工具函数解析
- menu
- popup
- after-leave
- aria-dialog
- aria-utils
- clickoutside
- date-util
- date
- dom
- merge ✔
- popper
- resize-event
- scroll-into-view ✔
- scrollbar-width ✔
- shared
- types ✔
- util ✔
- vdom
- vue-popper
Types.js
开头先导入了Vue,因为在该模块中需要通过Vue来判断一些状态
import Vue from 'vue';
这里定义了一些类型检测的方法,让我们先一起看看
判断是否是字符串
export function isString(obj) {
return Object.prototype.toString.call(obj) === '[object String]';
}
判断参数是否是对象
export function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
判断参数是否是元素节点
export function isHtmlElement(node) {
//nodeType为元素节点时值为1
return node && node.nodeType === Node.ELEMENT_NODE;
}
判断参数是否是函数
let isFunction = (functionToCheck) => {
var getType = {};
//此处定义了一个空对象,利用原型链查找
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
};
if (typeof /./ !== 'function' && typeof Int8Array !== 'object' && (Vue.prototype.$isServer || typeof document.childNodes !== 'function')) {
isFunction = function(obj) {
return typeof obj === 'function' || false;
};
}
该检测方法参考underscore,用于兼容一些老的v8,IE11,Safari 8,PhantomJS 中typeof
方法的bug。
检查参数是否是undefined
export const isUndefined = (val)=> {
return val === void 0;
};
使用void 0
来替代undefined
,用于避免undefined
被赋值导致的错误。
检查参数是否定义
export const isDefined = (val) => {
return val !== undefined && val !== null;
};
utils.js
该模块定义了一些实用的工具函数,让我们来看看。
import Vue from 'vue';
import { isString, isObject } from 'element-ui/src/utils/types';
const hasOwnProperty = Object.prototype.hasOwnProperty;
开头引入了Vue构造函数和isString
、isObject
类型检测方法,并且将hasOwnProperty
缓存,这在频繁用到该方法时减少对象查找时间。
空函数
export function noop() {};
空函数在一些判断逻辑中会用到,比如检测是否是服务端渲染,用于避免一些在服务端不必要的操作。
检查是否是自身属性
export function hasOwn(obj, key) {
//传入对象obj,键key,检测key是否只在自身属性中,避免查找原型链
return hasOwnProperty.call(obj, key);
};
对象混入(内部方法)
function extend(to, _from) {
for (let key in _from) {
//_from对象中的所有键值对都会覆盖到to,包括原型链
to[key] = _from[key];
}
return to;
};
flat对象数组
export function toObject(arr) {
//只允许传对象数组
var res = {};
for (let i = 0; i < arr.length; i++) {
if (arr[i]) {
//使用了上述对象混入方法,
extend(res, arr[i]);
}
}
return res;
};
根据对象路径获取对象值
export const getValueByPath = function(object, prop) {
//格式化
prop = prop || '';
//分割成数组
const paths = prop.split('.');
//缓存当前访问深度
let current = object;
let result = null;
for (let i = 0, j = paths.length; i < j; i++) {
const path = paths[i];
if (!current) break;
//如果是最后一个路径
if (i === j - 1) {
//获取到最终值
result = current[path];
break;
}
//缓存当前访问深度
current = current[path];
}
return result;
};
根据对象和对象路径获取最终的key,value和存储该键值对的对象
export function getPropByPath(obj, path, strict) {
let tempObj = obj;
// 该正则用于替换`a[b]`这样的动态路径转成`a.b`形式
path = path.replace(/\[(\w+)\]/g, '.$1');
// 该正则用于去掉开头的`.`
path = path.replace(/^\./, '');
//分割字符串
let keyArr = path.split('.');
//这里将i定义在外部用于返回值
let i = 0;
// 迭代器迭代至数组倒数第二个值且 此时i为倒数第一个值
for (let len = keyArr.length; i < len - 1; ++i) {
//strict为false直接跳出
if (!tempObj && !strict) break;
let key = keyArr[i];
if (key in tempObj) {
//层层递进
tempObj = tempObj[key];
} else {
//如果没有找到该key对应的值且为严格模式就报错
if (strict) {
throw new Error('please transfer a valid prop path to form item!');
}
break;
}
}
return {
o: tempObj,
k: keyArr[i],
v: tempObj ? tempObj[keyArr[i]] : null
};
};
生成随机id
export const generateId = function() {
//生成0-10000随机值
return Math.floor(Math.random() * 10000);
};
判断值是否相等(数组大致相等)
export const valueEquals = (a, b) => {
// 值相等
if (a === b) return true;
// 实例不是数组 不相等
if (!(a instanceof Array)) return false;
// 实例不是数组 不相等
if (!(b instanceof Array)) return false;
// 长度是否相等
if (a.length !== b.length) return false;
for (let i = 0; i !== a.length; ++i) {
//检查数组是否每一项都相等,否则为false
if (a[i] !== b[i]) return false;
}
return true;
};
转义正则表达式字符串
export const escapeRegexpString = (value = '') => String(value).replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
寻找数组中符合要求的值的索引
//传入pred回调函数,pred对数组中每一项进行检查,直到碰见第一个符合要求的值
export const arrayFindIndex = function(arr, pred) {
for (let i = 0; i !== arr.length; ++i) {
if (pred(arr[i])) {
return i;
}
}
return -1;
};
寻找符合要求的值
export const arrayFind = function(arr, pred) {
const idx = arrayFindIndex(arr, pred);
return idx !== -1 ? arr[idx] : undefined;
};
将传入参数作为数组返回
export const coerceTruthyValueToArray = function(val) {
//如果参数本身为数组,返回本身
if (Array.isArray(val)) {
return val;
} else if (val) {
//否则如果值为truthy,使用数组包裹返回
return [val];
} else {
return [];
}
};
检查是否是IE环境
export const isIE = function() {
return !Vue.prototype.$isServer && !isNaN(Number(document.documentMode));
};
关键在后半段document.documentMode
,在IE环境下其值为 IE版本号,其为IE特有的属性,为Number
类型。非IE环境Number(undefined)值为NaN,所以可以以此来判断。
检查是否是Edge环境
export const isEdge = function() {
return !Vue.prototype.$isServer && navigator.userAgent.indexOf('Edge') > -1;
};
检查是否是fireFox环境
export const isFirefox = function() {
return !Vue.prototype.$isServer && !!window.navigator.userAgent.match(/firefox/i);
};
对transform,transition,animation附加样式前缀
export const autoprefixer = function(style) {
if (typeof style !== 'object') return style;
const rules = ['transform', 'transition', 'animation'];
const prefixes = ['ms-', 'webkit-'];
rules.forEach(rule => {
const value = style[rule];
if (rule && value) {
prefixes.forEach(prefix => {
//包括了已有的值
style[prefix + rule] = value;
});
}
});
return style;
};
分割驼峰命名并以"-"链接
export const kebabCase = function(str) {
//匹配前面为非短横杠的A-Z的字母
const hyphenateRE = /([^-])([A-Z])/g;
return str
.replace(hyphenateRE, '$1-$2')
.replace(hyphenateRE, '$1-$2')
.toLowerCase();
};
将字符串首字母大写
export const capitalize = function(str) {
if (!isString(str)) return str;
//取首字母大写并拼接
return str.charAt(0).toUpperCase() + str.slice(1);
};
判断对象是否大致相等(不比较地址)
export const looseEqual = function(a, b) {
const isObjectA = isObject(a);
const isObjectB = isObject(b);
if (isObjectA && isObjectB) {
//如果都是对象 则使用JSON.stringify转成字符串比较
return JSON.stringify(a) === JSON.stringify(b);
} else if (!isObjectA && !isObjectB) {
//否则转成普通字符串比较
return String(a) === String(b);
} else {
return false;
}
};
判断数组是否大致相等(不比较地址)
export const arrayEquals = function(arrayA, arrayB) {
arrayA = arrayA || [];
arrayB = arrayB || [];
if (arrayA.length !== arrayB.length) {
return false;
}
for (let i = 0; i < arrayA.length; i++) {
if (!looseEqual(arrayA[i], arrayB[i])) {
return false;
}
}
return true;
};
判断两个参数是否大致相等
export const isEqual = function(value1, value2) {
if (Array.isArray(value1) && Array.isArray(value2)) {
//如果是数组 比较数组
return arrayEquals(value1, value2);
}
//否则比较值
return looseEqual(value1, value2);
};
判断一个值是否为”空“
export const isEmpty = function(val) {
// null or undefined
// 如果是null或者是undefined
if (val == null) return true;
//如果是布尔值 不为空
if (typeof val === 'boolean') return false;
//如果是数字类型除了0 都为空
if (typeof val === 'number') return !val;
// 如果是Error的实例,则message为空的情况下为空
if (val instanceof Error) return val.message === '';
switch (Object.prototype.toString.call(val)) {
// String or Array
//数组或是字符串情况下判断长度
case '[object String]':
case '[object Array]':
return !val.length;
// Map or Set or File
// File、Map、Set情况下判断size
case '[object File]':
case '[object Map]':
case '[object Set]': {
return !val.size;
}
// Plain Object
// 对象判断
case '[object Object]': {
return !Object.keys(val).length;
}
}
return false;
};
节流函数,每帧执行一次函数
export function rafThrottle(fn) {
let locked = false;
return function(...args) {
if (locked) return;
locked = true;
//下次重绘前调用一次Fn
window.requestAnimationFrame(_ => {
fn.apply(this, args);
locked = false;
});
};
}
将对象转化成数组对象
export function objToArray(obj) {
if (Array.isArray(obj)) {
return obj;
}
return isEmpty(obj) ? [] : [obj];
}
scrollbar-width.js
该模块导出了一个函数,这个函数的作用顾名思义,就是获取当前页面滚动条的宽度。
import Vue from 'vue';
let scrollBarWidth;
开头引入了Vue构造函数用于判断是否是服务端渲染,并使用一个变量来缓存滚动条宽度,所以获取到宽度之后都是使用这个变量来返回。
export default function() {
//服务端渲染宽度为0
if (Vue.prototype.$isServer) return 0;
//获取上一次缓存
if (scrollBarWidth !== undefined) return scrollBarWidth;
//创建一个外部div
const outer = document.createElement('div');
//定义类名
outer.className = 'el-scrollbar__wrap';
//隐藏该元素
outer.style.visibility = 'hidden';
//宽度为100px
outer.style.width = '100px';
//脱离文档流
outer.style.position = 'absolute';
//不妨碍点击事件
outer.style.top = '-9999px';
document.body.appendChild(outer);
//计算此时元素布局宽度(包含滚动条)
const widthNoScroll = outer.offsetWidth;
//允许滚动
outer.style.overflow = 'scroll';
//创建内部元素
const inner = document.createElement('div');
//为父亲宽度(不包含滚动条)
inner.style.width = '100%';
outer.appendChild(inner);
//计算内部布局宽度
const widthWithScroll = inner.offsetWidth;
//移除outer
outer.parentNode.removeChild(outer);
//计算滚动条宽度
scrollBarWidth = widthNoScroll - widthWithScroll;
return scrollBarWidth;
};
offsetWidth 是测量包含元素的边框 (border)、水平线上的内边距 (padding)、竖直方向滚动条 (scrollbar)(如果存在的话)、以及 CSS 设置的宽度 (width) 的值。
merge.js
该模块导出了一个函数,这个函数的作用顾名思义,就是合并对象,该对象第一个参数为合并目标,其余参数为合并源
export default function(target) {
for (let i = 1, j = arguments.length; i < j; i++) {
let source = arguments[i] || {};
for (let prop in source) {
if (source.hasOwnProperty(prop)) {
//排除原型属性
let value = source[prop];
if (value !== undefined) {
target[prop] = value;
}
}
}
}
return target;
};
scroll-into-view.js
该模块导出了一个函数,这个函数的作用顾名思义,就是合并对象,该对象第一个参数为合并目标,其余参数为合并源
export default function scrollIntoView(container, selected) {
if (!selected) {
container.scrollTop = 0;
return;
}
const offsetParents = [];
//取到包裹这个元素的最近定位元素
let pointer = selected.offsetParent;
//当这个元素存在但不是container元素且container底下含有这个元素
while (pointer && container !== pointer && container.contains(pointer)) {
//把这个定位元素推入数组
offsetParents.push(pointer);
//再往上取
pointer = pointer.offsetParent;
}
//直到container === pointer
//取到selected相对于其 offsetParent 元素的顶部的距离+offsetParents数组每个元素相对于其 offsetParent 元素的顶部的距离
const top = selected.offsetTop + offsetParents.reduce((prev, cur) => (prev + cur.offsetTop), 0);
//总的顶部距离+该元素的布局高度
const bottom = top + selected.offsetHeight;
//计算容器当前滚动高度
const viewRectTop = container.scrollTop;
//计算容器当前滚动高度+容器内容高度
const viewRectBottom = viewRectTop + container.clientHeight;
if (top < viewRectTop) {
//selected到顶部的距离比容器滚动的高度小,换句话说就是滚掉了
//滚回到select元素处
container.scrollTop = top;
} else if (bottom > viewRectBottom) {
//selected到顶部加自身的距离比容器滚动的高度+自身距离大,换句话说就是没滚到那
container.scrollTop = bottom - container.clientHeight;
}
}