【源码共读】第26期 | classnames

1. 前言

2. 什么是classnames

2.1 解决了什么问题

classnames 这个库解决了在 JavaScript 或 TypeScript 代码中处理类名拼接的问题。

在前端开发中,我们经常需要根据不同的条件动态地添加或移除 HTML 元素的类名。这些类名通常用于样式控制或元素状态的识别。然而,手动进行类名字符串的拼接和管理可能会变得冗长、复杂且容易出错。

2.2 有什么优势

classnames 库提供了一个简洁且方便的方式来处理类名的拼接。它接受多个参数,并根据不同的情况生成最终的类名字符串。该库具有以下特点和优势:

  1. 多种参数传递方式:例如字符串、数字、数组或对象等, 自动过滤空值。
javascript 复制代码
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
  1. 支持嵌套和递归:当传入数组类型的参数时,classnames 会递归调用自身,以处理嵌套的类名数组,从而实现更复杂的类名组合。
php 复制代码
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
  1. 支持对象驱动:通过传入对象类型的参数,我们可以根据对象的属性值来动态选择需要添加的类名。
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!

参考文章:

1

相关推荐
Easonmax13 分钟前
【CSS3】css开篇基础(1)
前端·css
大鱼前端32 分钟前
未来前端发展方向:深度探索与技术前瞻
前端
昨天;明天。今天。37 分钟前
案例-博客页面简单实现
前端·javascript·css
天上掉下来个程小白38 分钟前
请求响应-08.响应-案例
java·服务器·前端·springboot
周太密1 小时前
使用 Vue 3 和 Element Plus 构建动态酒店日历组件
前端
时清云1 小时前
【算法】合并两个有序链表
前端·算法·面试
小爱丨同学2 小时前
宏队列和微队列
前端·javascript
持久的棒棒君2 小时前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_857297912 小时前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋3 小时前
Web和UE5像素流送、通信教程
前端·ue5