classList.js 源码解析
在低版本的浏览器中,对于标签元素的类型的处理大多都是通过 className 来实现的,element.className 返回的是一个字符串,当我们要添加某个类名,删除某个类名的时候,都需要在该字符串上进行很多的处理,这无异是一个很麻烦的问题。所以,在 HTML5 新规范中,添加了 classList 属性,该属性封装了很多已有的方法来操作标签的类,但是在我们的项目中,至少目前来看,还有很多用户的浏览器停留在低版本的状态下,并不支持该属性,所以我还是需要使用一些现有的脚本来实现该功能。在本文中,分析的是
GitHub 上 classList.js 项目,通过学习源码,来体会其中代码的精妙之处。
首先放上 classList.js | Github 链接地址,有兴趣的同学可以直接去学习源代码。
下面,我们开始进行源代码的学习:
首先,创建一个元素,判断该元素是否存在 classList 这个属性。在 IE < Edge 版本的浏览器下,SVG 标签也没有 classList 属性。
// Full polyfill for browsers with no classlist support
// Including IE < Edge missing SVGElement.classlist
if (!("classList" in document.createElement("_"))
|| document.createElementNS && !("classList" in document.createElementNS("http://www.w3.org/2000/svg","g")))
在判断了当前标签元素不存在 classList 属性后,创建一个立即执行函数,并且将 self,在浏览器中,也就是 window 作为参数传递给 view,下面,我们来看看在这个立即执行函数中做了什么事情。
在这个立即执行函数中,定义了实现 classList 属性所需要的函数以及一些参数,因为浏览器实现的原因,有的必须的函数需要自己实现,如:
trim 函数:
strTrim = String[protoProp].trim || function () {
return this.replace(/^\s+|\s+$/g, "");
}
实现的是将开头和结尾的多数空格(\s+)替换成 ""
indexOf 函数:
arrIndexOf = Array[protoProp].indexOf || function (item) {
var
i = 0
, len = this.length
;
for (; i < len; i++) {
if (i in this && this[i] === item) {
return i;
}
}
return -1;
}
checkTokenAndGetIndex 函数,这个函数在 arrIndexOf 函数的基础上,判断了输入的要被寻找的字符串是否为 "",或者是否含有空格:
checkTokenAndGetIndex = function (classList, token) {
if (token === "") {
throw new DOMEx(
"SYNTAX_ERR"
, "An invalid or illegal string was specified"
);
}
if (/\s/.test(token)) {
throw new DOMEx(
"INVALID_CHARACTER_ERR"
, "String contains an invalid character"
);
}
return arrIndexOf.call(classList, token);
}
设置了 classListProto,代表 ClassList 属性继承于数组,关于所有 className 处理得到的每个类名都放在这个数组中。
classListProto = ClassList[protoProp] = []
ClassList 函数,这个函数获得,并处理 class,使用正则表达式 /\s+/ 将其分成多个单独的类名,并且因为 ClassList 继承于数组,所以可以将获得的单个类名保存在 ClassList 生成的对象自身:
ClassList = function (elem) {
var
trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
, i = 0
, len = classes.length
;
for (; i < len; i++) {
this.push(classes[i]);
}
this._updateClassName = function () {
elem.setAttribute("class", this.toString());
};
}
然后在 classListProto 中添加了对象共有的方法,如 add、contains、remove、toggle、toString、item。我们可以看看它的实现方法:
item 方法,返回第 i 个 class:
classListProto.item = function (i) {
return this[i] || null;
};
contains 方法,通过遍历 ClassList 数组,来返回是否包含某个类名。
classListProto.contains = function (token) {
token += "";
return checkTokenAndGetIndex(this, token) !== -1;
};
add 方法,给元素添加一个类:
classListProto.add = function () {
var
tokens = arguments
, i = 0
, l = tokens.length
, token
, updated = false
;
do {
token = tokens[i] + ""; // 可以在形参中传入多个参数,都添加到类中
if (checkTokenAndGetIndex(this, token) === -1) {
this.push(token);
updated = true;
}
}while (++i < l);
if (updated) {
this._updateClassName();
}
};
注意,在上面的 add 函数中,有一个 bug,就是在调用 elem.add() 的时候,如果没有传递参数,则会在当前标签中生成 "undefined" 类名并添加到标签的 className 中,关于这个问题我已经向作者提了一个 Issue,期待他的回答。
remove 方法,删除一个类名:
classListProto.remove = function () {
var
tokens = arguments
, i = 0
, l = tokens.length
, token
, updated = false
, index
;
do {
token = tokens[i] + "";
index = checkTokenAndGetIndex(this, token);
// 这一点还是蛮有意思的,因为之前的 className 可能不是通过 add 添加,而是已经存在于标签上,则有可能会在标签上有多个相同的类名,所以需要注意
while (index !== -1) {
this.splice(index, 1);
updated = true;
index = checkTokenAndGetIndex(this, token);
}
} while (++i < l);
if (updated) {
this._updateClassName();
}
};
toggle 方法,如果有该类名,就删除该类名,没有则添加该类名:
classListProto.toggle = function (token, force) {
// 同样,不判断输入,则可能会在类名上添加 "undifined"
token += "";
var
result = this.contains(token)
, method = result ?
force !== true && "remove"
:
force !== false && "add"
;
if (method) {
this[method](token);
}
if (force === true || force === false) {
return force;
} else {
return !result;
}
};
toString 方法,返回类名的形式:
classListProto.toString = function () {
return this.join(" ");
};
在定义完了 ClassList 之后,我们如何将 ClassList 属性添加到所有的标签对象的属性中呢?我们使用的方法是在 window.Element 构造方法的 prototype 中添加 ClassList 属性,由于所有的标签元素都是 Element 的对象,则他们都具有了 ClassList 属性:
if (objCtr.defineProperty) {
var classListPropDesc = {
get: classListGetter
, enumerable: true
, configurable: true
};
try {
// 使用 Object.defineProperty 来为 Element.propotype 添加属性,让所有 Element 对象都具有该属性
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
} catch (ex) { // IE 8 doesn't support enumerable:true
// adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36
// modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected
if (ex.number === undefined || ex.number === -0x7FF5EC54) {
classListPropDesc.enumerable = false;
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
}
}
} else if (objCtr[protoProp].__defineGetter__) {
elemCtrProto.__defineGetter__(classListProp, classListGetter);
}
上面一段代码实现的就是将 ClassList 添加到 Elememnt.prototype 对象中。其中,classListGetter 函数返回了一个 ClassList 对象,在调用 element.classList 时候就会自动生成该对象:
classListGetter = function () {
return new ClassList(this);
}
通过以上的代码,我们就实现了 classList 属性添加的功能,最后,我们再总结一下为所有元素添加 classList 并且提供相关函数的实现方案:
- 创建
ClassList类,并且在该类中添加对类名进行处理的add、remove、contains等方法 - 使用
Object.defineProperty来为window.Element.propotype添加classList属性,则所有Element的对象都会继承该属性。在访问属性的时候,会调用classListGetter,然后返回一个ClassList对象
在 classList.js 结尾的地方,针对 IE 10/11 and Firefox < 26 版本的浏览器,本身提供了add、remove 方法,但是提供的方法只能处理一个参数的情况,进行了修复,修复的大致逻辑是,首先测试在一次函数调用中添加两个类名,如果添加的第二个类名没有成功,则使用一个新函数来代替原有的 add、remove 函数,新函数的作用是判断参数的个数,然后反复调用原有 add、remove 函数:
testElement.classList.add("c1", "c2");
// Polyfill for IE 10/11 and Firefox <26, where classList.add and
// classList.remove exist but support only one argument at a time.
if (!testElement.classList.contains("c2")) {
var createMethod = function(method) {
var original = DOMTokenList.prototype[method];
DOMTokenList.prototype[method] = function(token) {
var i, len = arguments.length;
for (i = 0; i < len; i++) {
token = arguments[i];
original.call(this, token);
}
};
};
createMethod('add');
createMethod('remove');
}
--EOF--发表于2017-09-16并被添加「 classList JavaScript」标签,最后修改于2017-09-17。
专题「JavaScript专题」相关的其他文章»
- JavaScript 中的作用域和闭包(Jun 03, 2017)
- JavaScript 的类和对象(Jun 06, 2017)
- classList.js 源码解析(Sep 16, 2017)