- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第26期,链接:classnames。
说明
想必读者或多或少都使用过,或者知道有这么一个帮助开发者取类名的插件:classnames。
当然,Vue开发者可能很少使用,毕竟Vue本身就动态自定义class类名,而React中会麻烦点,因此这个插件会用的比较多,今天我们就来看看classnames内部是如何实现的。
一、资源准备
# 克隆项目
https://github.com/JedWatson/classnames
# 进入
cd classnames
index.js是入口文件,但是,其实这里一共有三个模块:index.js、bind.js、dedupe.js;
- index.js:普通的处理class类名;
- bind.js:在index.js基础上支持指定this;
- dedupe.js:在index.js基础上支持去重;
实际三种引入:都支持commonjs和esmodule。
import classNames from 'classnames';
var classNames = require('classnames');
import classNames from 'classnames/bind';
var classNames = require('classnames/bind');
var classNames = require('classnames/dedupe');
import classNames from 'classnames/dedupe';
二、源码分析
我们先分析index.js的,bind.js和index.js几乎一样,而dedupe就是把index.js的多个判断抽离成单独函数。
考虑到代码量不多,我这边直接把全部代码复制来,在代码中注释说明。
1、index
(function () {
'use strict';
// 单独抽离获取属性是否在对象自身上的方法
var hasOwn = {}.hasOwnProperty;
// classNames方法
function classNames() {
// 存储最后的类名
var classes = [];
// 遍历参数
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
// 所有转换为布尔值为false的一律跳过不添加:如undefined/null/0/''/false
if (!arg) continue;
// 获取类型
var argType = typeof arg;
// 字符串和数字直接添加
if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
if (arg.length) {
// 有元素的数组的话递归classNames,注意这里的inner为最后数组根据split为空格拆分的字符串
var inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
} else if (argType === 'object') {
// 对象的话,剔除重写了toString的数据
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
classes.push(arg.toString());
continue;
}
// 其余对象遍历,如果是自身属性,且值转为布尔值为true,则添加键名
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
// 数组拆分成字符串
return classes.join(' ');
}
// commonjs模块时挂载
if (typeof module !== 'undefined' && module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// amd模块时挂在
// register as 'classnames', consistent with npm package name
define('classnames', [], function () {
return classNames;
});
} else {
// 浏览器时挂载
window.classNames = classNames;
}
}());
2、bind
bind的话就是在index.js基础上,对于每一个push的操作,进行判断是否在this上为true而已,此处不再赘述。
(function () {
'use strict';
var hasOwn = {}.hasOwnProperty;
function classNames () {
var classes = [];
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) continue;
var argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes.push(this && this[arg] || arg);
} else if (Array.isArray(arg)) {
classes.push(classNames.apply(this, arg));
} else if (argType === 'object') {
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
classes.push(arg.toString());
continue;
}
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(this && this[key] || key);
}
}
}
}
return classes.join(' ');
}
if (typeof module !== 'undefined' && module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// register as 'classnames', consistent with npm package name
define('classnames', [], function () {
return classNames;
});
} else {
window.classNames = classNames;
}
}());
3、dedupe
考虑到需要去重,此处通过Object.create(null),创建了一个比较干净的对象作为存储容器进行去重。
(function () {
'use strict';
var classNames = (function () {
// don't inherit from Object so we can skip hasOwnProperty check later
// http://stackoverflow.com/questions/15518328/creating-js-object-with-object-createnull#answer-21079232
// 创建原型对象为null的构造函数,创建的实例用来作为去重对象存储容器
function StorageObject() {}
StorageObject.prototype = Object.create(null);
// 处理数组
function _parseArray (resultSet, array) {
var length = array.length;
for (var i = 0; i < length; ++i) {
_parse(resultSet, array[i]);
}
}
var hasOwn = {}.hasOwnProperty;
// 处理数字
function _parseNumber (resultSet, num) {
resultSet[num] = true;
}
// 处理对象
function _parseObject (resultSet, object) {
if (object.toString !== Object.prototype.toString && !object.toString.toString().includes('[native code]')) {
resultSet[object.toString()] = true;
return;
}
for (var k in object) {
if (hasOwn.call(object, k)) {
// set value to false instead of deleting it to avoid changing object structure
// https://www.smashingmagazine.com/2012/11/writing-fast-memory-efficient-javascript/#de-referencing-misconceptions
resultSet[k] = !!object[k];
}
}
}
// 处理字符串(有空字符的拆分,当成多个类名)
var SPACE = /\s+/;
function _parseString (resultSet, str) {
var array = str.split(SPACE);
var length = array.length;
for (var i = 0; i < length; ++i) {
resultSet[array[i]] = true;
}
}
// 统一解析方法:根据类型分发到对应类型的解析方法执行
function _parse (resultSet, arg) {
if (!arg) return;
var argType = typeof arg;
// 'foo bar'
if (argType === 'string') {
_parseString(resultSet, arg);
// ['foo', 'bar', ...]
} else if (Array.isArray(arg)) {
_parseArray(resultSet, arg);
// { 'foo': true, ... }
} else if (argType === 'object') {
_parseObject(resultSet, arg);
// '130'
} else if (argType === 'number') {
_parseNumber(resultSet, arg);
}
}
// classnames方法
function _classNames () {
// don't leak arguments
// https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
var len = arguments.length;
var args = Array(len);
for (var i = 0; i < len; i++) {
args[i] = arguments[i];
}
var classSet = new StorageObject();
_parseArray(classSet, args);
var list = [];
for (var k in classSet) {
if (classSet[k]) {
list.push(k)
}
}
return list.join(' ');
}
return _classNames;
})();
if (typeof module !== 'undefined' && module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// register as 'classnames', consistent with npm package name
define('classnames', [], function () {
return classNames;
});
} else {
window.classNames = classNames;
}
}());
三、总结
通过分析,其实classname的实现并不复杂,只要熟练掌握各种数据类型,和对应的内置方法,我们完全可以自己实现。