iView 源码解析 - Util 篇

React、Angular、Vue 前端三大框架中,目前工作接触最多的就是 Vue 了。对于 Vue 的几个有名的 UI 组件:Element、iView,我都使用来搭建过几个后台系统。相对来说,因为 iView 最早支持 render 函数的语法,与 TypeScript 配合较好;iView 团队对组件的更新、bug 的修复更积极;再加上我个人更喜欢 iView 的组件样式。因此,在我自己的项目中,我更偏向于使用 iView。

对于自学框架的时期,我觉得应该多去学习学习其生态中的优秀组件,通过学习他的源码来了解其他人是怎么使用这个框架的,理解一些高级特性的使用。针对我目前的工作,我准备从 iView 的源码入手,了解一些平时没怎么用到的 Vue 高级语法特性,然后再去尝试阅读 Vue 的源码。

本系列“iView 源码解析”,纯粹是记录我阅读 iView 源码时的一些心得感受,还谈不上从更高的角度来完成“解析“二字。本篇从 iView 的公共通用方法 Util 开始进入 iView 的世界。)

(iView 版本: 2.8; Vue 版本:2.5.13)

utils

utils 文件夹下有五个文件:

  • assist.js
  • calcTextareaHeight.js
  • csv.js
  • date.js
  • dom.js

其中 csv.js 主要和 csv 文件的处理有关,本文将不对其做介绍讨论。

assist.js

类型检查

一般看某个文档的源码,我个人比较喜欢先去找类型检查部分的函数,往往越底层的代码越能学到一些新的东西。目前标准的类型检查方式都是利用方法 Object.prototype.toString.call(obj) 得到的值去判断(比如:underscore)。iView 通过封装了一个 typeof 函数用于类型检查的处理,而判断的实质依旧是通过上述的方法来实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function typeOf(obj) {
const toString = Object.ptototype.toString;
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object'
};
return map[toString.call(obj)];
}

获取元素样式

我们知道 element.style 只能获取元素标签内通过 style 属性声明的样式,无法获取该元素的 CSS 样式。之前需要兼容低版本 IE 浏览器时,一般设计的获取样式的函数如下:

1
2
3
function getStyle(obj, styleName) {
return obj.currentStyle ? obj.currentStyle[styleName] : getComputedStyle(obj, null)[styleName]
}

来分析下 iView 的获取样式函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
export function getStyle(element, styleName) {
if (!element || !styleName) return null;
styleName = camelCase(styleName);
if (styleName === 'float') {
styleName = 'cssFloat';
}
try {
const computed = document.defaultView.getComputedStyle(element, '');
return element.style[styleName] || computed ? computed[styleName] : null;
} catch(e) {
return element.style[styleName];
}
}

iView 中增加了一些安全性检测,camelCase 是其编写的将字符串转换为“驼峰形式”的函数。document.defaultView.getComputed 实际就是 window.getComputed 的写法,然而前者的写法可以避免 Firefox 3.6 上无法获取 iframe 样式的问题(参考链接)。

iView 是从 IE 9 版本开始进行支持的,而 getComputed 刚好可以兼容到 IE9,因此没有再考虑通过 currentStyle 对 IE8 及以下版本的浏览器进行支持了。

深拷贝

iView 的深拷贝函数其实很简洁、易理解,通过判断需要拷贝的元素类型,如果是数组、对象则再对其子元素进行递归处理即可;如果不是,则直接返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function deepCopy(data) {
const t = typeOf(data);
let o;

if (t === 'array') {
o = [];
} else if (t === 'object') {
o = {};
} else {
return data;
}

if (t === 'array') {
for (let i = 0; i < data.length; i++) {
o.push(deepCopy(data[i]));
}
} else if (t === 'object') {
for (let i in data) {
o[i] = deepCopy(data[i]);
}
}
return o;
}

获取滚动条宽度

每个浏览器的滚动条样式都是不同的,此函数是用于获取当前浏览器的滚动条宽度,并将其用一个变量存储起来,可以用于需要全屏遮罩时的一些样式处理上。

该函数的思路是生成一个外层的定宽、定高的 outer 元素,为防止破坏页面,令其绝对定位、visibility = 'hidden'。在生成一个 outer 的子元素 inner,令其高度比父元素高,宽度为 100%。然后通过调整父元素的 overflow 属性,测量有、无滚动条时子元素的 offsetWidth,再通过差值计算即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let cached;
export function getScrollBarSize(fresh) {
if (isServer) return 0;
if (fresh || cached === undefined) {
const inner = document.createElement('div');
inner.style.width = '100%';
inner.style.height = '200px'

const outer = document.createElement('div');
const outerStyle = outer.style;
outerStyle.position = 'absolute';
outerStyle.top = 0;
outerStyle.left = 0;
outerStyle.pointerEvents = 'none';
outerStyle.visibility = 'hidden';
outerStyle.width = '200px';
outerStyle.height = '150px';
outerStyle.overflow = 'hidden';

outerStyle.appendChild(inner);

document.body.appendChild(outer);

const widthContained = inner.offsetWidth;
outer.style.overflow = 'scroll';
let widthScroll = inner.offsetWidth;

if (widthContained === widthScroll) {
widthScroll = outer.clientWidth;
}

document.body.removeChild(outer);

cached = widthContained - widthScroll;
}
return cached;
}

这里请注意第 28 行 widthContained === widthScroll,这部分比较的是有无滚动条两个情况下 inner.offsetWidth 值。作者特意判断了一下相等的情况,那就说明有可能是存在兼容性问题的。我一开始是以为 offsetWidth 的兼容性问题,以为在某种情况下会将滚动条的宽度也计入。但自己写了一个 demo 后,测试了 IE8~11、Edge、Chrome、Firefox、Safari 几款浏览器,最终竟然在 Safari 上找到了问题的所在。该兼容性导致的原因是 width: 100% 这个属性,在其他浏览器中,子元素声明该属性后,获取的宽度值是父元素下除去滚动条以外的宽度;而在 Safari 中,子元素声明该属性后,它获取的宽度就是父元素的宽度值,因此会产生上述值相等的情况。

class 类的处理

iView 中封装了三个处理 class 的方法:hasClassaddClassremoveClass。这里值得一提的是,这三个函数都是优先对元素的 classList 属性进行检测,如果存在,直接调用 classList 的方法即可实现相应的函数。

classList 是 W3C 提供的用于获取元素类属性的实时 DOMTokenList 集合,封装了五个操作 class 的方法,基本可以实现对 class 操作的所有需求。但是在浏览器中,IE 10 以上是不兼容的,在使用用还需进行兼容性判断。

函数中其中有这么一行引起了我的注意:

1
2
3
4
5
// hasClass(el, cls)
// @param el 需要判断的DOM元素
// @param cls 需要判断是否存在的类名称
// ...
return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1;

以上是函数 hasClass 在判断 classList 不存在时采取的判断方法,我开始有疑惑的部分是为什么在类名前后各加一个空格。后来才意识到这是为了完成对整个字符串的检验,比如:el 中存在类 backgroundColor,传参过来需要检验的类是 background。如果不加空格,上述的返回结果将是正确的,而与实际需求不符。在两个类名前后各加一个空格即可避免这种情况,实现字符的全判断。

其它

assist.js 中还封装了几个函数,但个人认为都是一眼能看懂的,因此就不再此表述了。

date.js

封装了一些对时间处理的函数,此部分结合后续的日期组件进行讨论。

dom.js

该文件内主要封装了两个跟事件有关的函数:onoff。在兼容性判断上也是比较了两对函数:addEventListenerattachEventremoveEventListenerdetachEvent。这两个方法比较常见了,故不在此进行讨论了。