在低版本的浏览器中,对于标签元素的类型的处理大多都是通过 className
来实现的,element.className
返回的是一个字符串,当我们要添加某个类名,删除某个类名的时候,都需要在该字符串上进行很多的处理,这无异是一个很麻烦的问题。所以,在 HTML5 新规范中,添加了 classList
属性,该属性封装了很多已有的方法来操作标签的类,但是在我们的项目中,至少目前来看,还有很多用户的浏览器停留在低版本的状态下,并不支持该属性,所以我还是需要使用一些现有的脚本来实现该功能。在本文中,分析的是 GitHub 上 classList.js 项目,通过学习源码,来体会其中代码的精妙之处。
首先放上 classList.js | Github 链接地址,有兴趣的同学可以直接去学习源代码。
下面,我们开始进行源代码的学习:
首先,创建一个元素,判断该元素是否存在 classList
这个属性。在 IE < Edge 版本的浏览器下,SVG 标签也没有 classList
属性。
1 | // Full polyfill for browsers with no classlist support |
在判断了当前标签元素不存在 classList
属性后,创建一个立即执行函数,并且将 self
,在浏览器中,也就是 window
作为参数传递给 view
,下面,我们来看看在这个立即执行函数中做了什么事情。
在这个立即执行函数中,定义了实现 classList
属性所需要的函数以及一些参数,因为浏览器实现的原因,有的必须的函数需要自己实现,如:
trim
函数:
1 | strTrim = String[protoProp].trim || function () { |
实现的是将开头和结尾的多数空格(\s+
)替换成 ""
indexOf
函数:
1 | arrIndexOf = Array[protoProp].indexOf || function (item) { |
checkTokenAndGetIndex
函数,这个函数在 arrIndexOf
函数的基础上,判断了输入的要被寻找的字符串是否为 ""
,或者是否含有空格:
1 | checkTokenAndGetIndex = function (classList, token) { |
设置了 classListProto
,代表 ClassList
属性继承于数组,关于所有 className 处理得到的每个类名都放在这个数组中。
1 | classListProto = ClassList[protoProp] = [] |
ClassList
函数,这个函数获得,并处理 class
,使用正则表达式 /\s+/
将其分成多个单独的类名,并且因为 ClassList 继承于数组,所以可以将获得的单个类名保存在 ClassList 生成的对象自身:
1 | ClassList = function (elem) { |
然后在 classListProto
中添加了对象共有的方法,如 add、contains、remove、toggle、toString、item
。我们可以看看它的实现方法:
item
方法,返回第 i 个 class:
1 | classListProto.item = function (i) { |
contains
方法,通过遍历 ClassList 数组,来返回是否包含某个类名。
1 | classListProto.contains = function (token) { |
add
方法,给元素添加一个类:
1 | classListProto.add = function () { |
注意,在上面的 add 函数中,有一个 bug,就是在调用 elem.add()
的时候,如果没有传递参数,则会在当前标签中生成 "undefined"
类名并添加到标签的 className 中,关于这个问题我已经向作者提了一个 Issue,期待他的回答。
remove
方法,删除一个类名:
1 | classListProto.remove = function () { |
toggle
方法,如果有该类名,就删除该类名,没有则添加该类名:
1 | classListProto.toggle = function (token, force) { |
toString
方法,返回类名的形式:
1 | classListProto.toString = function () { |
在定义完了 ClassList
之后,我们如何将 ClassList
属性添加到所有的标签对象的属性中呢?我们使用的方法是在 window.Element
构造方法的 prototype
中添加 ClassList
属性,由于所有的标签元素都是 Element
的对象,则他们都具有了 ClassList
属性:
1 | if (objCtr.defineProperty) { |
上面一段代码实现的就是将 ClassList
添加到 Elememnt.prototype
对象中。其中,classListGetter
函数返回了一个 ClassList
对象,在调用 element.classList
时候就会自动生成该对象:
1 | classListGetter = function () { |
通过以上的代码,我们就实现了 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
函数:
1 | testElement.classList.add("c1", "c2"); |