1. 前言
-
本文参加了由 公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
-
这是源码共读的第26期,链接:classnames
2. 什么是classnames
2.1 解决了什么问题
classnames
这个库解决了在 JavaScript 或 TypeScript 代码中处理类名拼接的问题。
在前端开发中,我们经常需要根据不同的条件动态地添加或移除 HTML 元素的类名。这些类名通常用于样式控制或元素状态的识别。然而,手动进行类名字符串的拼接和管理可能会变得冗长、复杂且容易出错。
2.2 有什么优势
classnames
库提供了一个简洁且方便的方式来处理类名的拼接。它接受多个参数,并根据不同的情况生成最终的类名字符串。该库具有以下特点和优势:
- 多种参数传递方式:例如字符串、数字、数组或对象等, 自动过滤空值。
javascript
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
- 支持嵌套和递归:当传入数组类型的参数时,
classnames
会递归调用自身,以处理嵌套的类名数组,从而实现更复杂的类名组合。
php
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
- 支持对象驱动:通过传入对象类型的参数,我们可以根据对象的属性值来动态选择需要添加的类名。
js
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true }); // btn-primary
3. 源码解读
3.1 函数分析
index版本
通过定义数组,实现对传入类型判断收集,最终通过join进行字符串返回。
流程如下:
源码解析:
js
(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') {
// 如果是string或者number直接push
classes.push(arg);
} else if (Array.isArray(arg)) {
// 如果是数组 判断是否有长度 递归遍历
if (arg.length) {
// 当参数是数组[a,b] 调用classNames.apply后传入将数组元素解构成classNames(a,b)
var inner = classNames.apply(null, arg);
// 通过闭包形式加入classes中
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;
}
// 遍历对象
for (var key in arg) {
// 如果存在对象上存在属性,不是继承,调用call重新指定this
//{ a: false, b: undefined } 不需要的排除
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
// 返回数组 如果存在嵌套调用 [a,'b c d'] => 'a b c d'
return classes.join(' ');
}
// 判断是否是CommonJs环境
if (typeof module !== 'undefined' && module.exports) {
classNames.default = classNames;
module.exports = classNames;
// 是否是amd环境
} 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对象上
window.classNames = classNames;
}
}());
dedupe版本
dedupe是通过对象Object键的唯一性来实现去重操作,也就有后来的能覆盖前面属性。
流程如下:
源码解析:
js
var classNames = (function () {
function StorageObject() {}
StorageObject.prototype = Object.create(null);
/** 遍历数组 调用_parse解析 */
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)) {
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);
}
}
function _classNames () {
// 转换成数组
var len = arguments.length;
var args = Array(len);
for (var i = 0; i < len; i++) {
args[i] = arguments[i];
}
// 定义对戏那个
var classSet = new StorageObject();
// 传入classSet
_parseArray(classSet, args);
var list = [];
// 去除无用键
for (var k in classSet) {
if (classSet[k]) {
list.push(k);
}
}
return list.join(" ");
}
return _classNames;
})();
bind版本
bind版本与index版大体相同,此版本是实现bind指定指定读取属性的对象,传入 classNames 的参数先作为 key 到绑定的对象中寻找 value,如果有,就放value 进去,如果没有才放入 key。
举例:
js
var classNames = require('classnames/bind');
var styles = {
foo: 'abc',
bar: 'def',
baz: 'xyz'
};
var cx = classNames.bind(styles);
var className = cx('foo', ['bar'], { baz: true }); // => "abc def xyz"
与index版本差别在于判断strin或者number时,指定this对象上是否存在这个key,且value是否为真
js
classes.push(this && this[arg] || arg)
学习点
对象改写toString对比区别
模块化区分: 可以参考我写的模块化发展
为什么要判断toString改写
在组件中使用 JSX 语法时,会将 JSX 转换为对应的 JavaScript 对象表示。在对这些对象进行渲染时,React 使用了自定义的 toString
方法来输出它们的字符串形式。
4. 总结
总结来说,classnames库就是接受多种参数类型,包括字符串、数字、数组和对象等,并根据不同的情况生成最终的类名字符串。
通过本次的学习,主要学习了:
- classnames的使用方式和优点
- 学习了
apply
第二个参数接收数组形式来实现数组扁平化的效果 - 学习了针对不同环境的声明
- 提供了多个版本的实现,包括 index 版本、dedupe 版本和 bind 版本,以适应不同的使用场景
积少成多,大家加油O^O!