ElementUI源码工具函数解析

2022 年 8 月 29 日 星期一
/
7

ElementUI源码工具函数解析

  1. menu
  2. popup
  3. after-leave
  4. aria-dialog
  5. aria-utils
  6. clickoutside
  7. date-util
  8. date
  9. dom
  10. merge ✔
  11. popper
  12. resize-event
  13. scroll-into-view ✔
  14. scrollbar-width ✔
  15. shared
  16. types ✔
  17. util ✔
  18. vdom
  19. 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构造函数和isStringisObject类型检测方法,并且将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;
  }
}

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...