源码目录总览
参考官方文档中的内容,我们可以知道
classnames
有一个主要版本(index
)和两个替代版本 (分别是dedupe
和bind
)。在看目录的时候也可以发现classnames
具有多个对外暴露的入口。
index.js
是classnames
的主要使用的版本dedupe.js
是classnames
中一个可选的,用于删除重复数据的版本bind.js
是classnames
中另一个可选的,用于css modules
形式的版本*.d.ts
是定义类型的文件bower.json
是包管理工具bower
的配置文件(后面会提到bower
和npm
的区别)tests/
目录下是classnames
三个版本的测试文件以及一个适用于测试代码的类型文件benchmarks/
目录用于存放benchmarks
相关的文件(benchmarks
是一个检测代码性能的工具)
用法概述
安装
# via npmnpm install classnames# via yarn yarn add classnames
一般用法
// 引入包import classnames from 'classnames';// 接收多个字符串作为参数classnames('classA', 'classB') // => classA, classB// 接收字符串 和 对象作为参数(对象的值可以为 true || false)classnames('classA', { classB: true }) // => classA, classB// 接收字符串 和 对象作为参数(对象的值也可以是一个返回 true || false 的表达式)classnames('classA', { classB: 1 < 2 }) // => classA// 接收一个对象作为参数,对象的键名是对应的类名,对象的值的规则与上面一致classnames({ classA: true, classB: false }) // => classA// 接收多个对象作为参数,对象的键与值的规则与上面一致classnames({ classA: true }, { classB: true }) // => classA, classB// 接收一个数组作为参数classnames(['classA', { classB: false }, { classC: 1 }]) // => classA, classC// 接收数组和其它类型参数的组合classnames(['classA', { classB: false }], 'classC') // => classA, classC// 需要注意的特殊例子!// 当存在针对同一个类的不同值时,只要其中一个值是真值,就会应用这个类(与这个值的计算顺序无关)classnames('classA', true ? 'classB' : '', { classB: false }) => classA, classBclassnames('classA', { classB: false }, true ? 'classB' : '') => classA, classB
// 动态计算类名type UserType = "guest" | "host";const user: UserType = 'guest';classnames(`${user}_type_color`); // => guest_type_color
dedupe
用法
dedupe
主要用于解决上面提到的特殊例子中存在的问题
有时候我们会在代码中应用多个样式,同时我们希望是否应用某个样式
取决于这个样式最终的计算结果。还是用上面提到的特殊例子:
- 不使用
dedupe````classnames('classA', true ? 'classB' : '', { classB: false }) => classA, classBclassnames('classA', { classB: false }, true ? 'classB' : '') => classA, classBclassnames('classA', { classB: true }, false ? 'classB' : '') => classA, classB ```* 使用
dedupe````classnames(‘classA’, true ? ‘classB’ : ‘’, { classB: false }) => classAclassnames(‘classA’, { classB: false }, true ? ‘classB’ : ‘’) => classA, classBclassnames(‘classA’, { classB: true }, false ? ‘classB’ : ‘’) => classA, classB ```#####bind
用法
bind
适用于通过css modules
引入样式并需要动态计算的场景。文档中建议在支持ES6
的情况下使用模板字符串
的形式来替代bind
方法。
直接来看例子
.foo {color: red;}.bar {font-size: 30px;}.zoo {border: 1px solid red;}
import styles from './index.css';const cs = classnames.bind(styles)// foo, zoo<div className={cs('foo', { bar: false }, [{ zoo: true }])}></div>
看看源码
通用代码
在index.js
,deeupe.js
和 bind.js
中都使用到了这个通用代码,用于针对不同的环境去导出classNames
方法。
// 适用于 nodejs 的环境,使用 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 namedefine('classnames', [], function () {return classNames;});} else {// 以上情况均不适用,就把 classNames 函数挂载到 window 上window.classNames = classNames;}
index.js
(function () {'use strict';// 将空对象的 hasOwnProerty 保存到hasOwn变量// 防止入参对象修改了同名方法导致判断结果错误var hasOwn = {}.hasOwnProperty;function classNames() {// 定义类名数组var classes = [];// 遍历入参for (var i = 0; i < arguments.length; i++) {// 拿到当前第 i 个入参var arg = arguments[i];// 入参的值类型为 falsy 则跳过这个值if (!arg) continue;// 判断当前入参的类型var argType = typeof arg;// 如果当前入参类型为 字符串 || 数字, 就直接推入类名数组if (argType === 'string' || argType === 'number') {classes.push(arg);// 如果当前入参类型为 数组} else if (Array.isArray(arg)) {// 首先判断数组是否为空// 不为空if (arg.length) {// 将入参的数组作为新的参数 递归调用 classNames 函数var inner = classNames.apply(null, arg);// 如果有返回值, 就将本次递归调用的结果推到 类名数组中if (inner) {classes.push(inner);}// 没有结果则忽略// 如果当前入参的类型是 object}} else if (argType === 'object') {// 如果当前入参对象的 toString 不等于 对象原型链上的 toString&& 入参对象的 toString 方法中不包含 "[native code]" 字符if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {// 调用入参对象自定义的 toString 方法后,再将得到的字符串推入 类名数组// TODO: 感觉这里需要额外增加一个判断,// 对入参的 toString 方法的返回值做判断// 如果返回值是一个 (字符串 || 数字) && 当前入参调用 toString 之后的值是真值,就直接推入 类名数组// 否则应当递归调用 classNames 函数(与数组的处理方法一致)if(typeof arg.toString() === 'string' || typeof arg.toString() === 'number' && arg.toString() ) {// && arg.toString()classes.push(arg.toString());continue;} else {classNames.apply(null, arg)}}// 没有修改过入参对象的 toString 方法// 遍历入参对象的 keyfor (var key in arg) {// 如果当前的key 是入参对象本身的属性 并且这个属性的值是真值if (hasOwn.call(arg, key) && arg[key]) {// 将这个属性名推入 类名数组classes.push(key);}}}}// 将 当前类名数组 用空格拼接并返回return classes.join(' ');}// 通用代码...}());
dedupe.js
简单来说,
dedupe
版本为了解决类名重复的问题,构造了一个对象来保存所有的类名;由于对象的key
无法重复,所以对象中后定义的类名的值
会覆盖之前定义的相同类名的值
。在对象构造完成后,再去遍历取得对象中所有值为true
的key
,并将这些key
返回,最终添加到DOM
上。
(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// 定义一个“存储对象类”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) {// 这里没有对数字的合法性做校验(0, -0, Infinity)// 但是没有关系,在最后一步遍历 list 数组的时候只取真值resultSet[num] = true;}// 用于解析对象的方法function _parseObject(resultSet, object) {// 与 index.js 中类似的判断,检查入参对象是否具有自定义的 toString 方法// 有的话就调用 其自定义的 toString 方法,并将结果添加到 存储对象 中if (object.toString <img src="https://www.smashingmagazine.com/2012/11/writing-fast-memory-efficient-javascript/#de-referencing-misconceptions// 将 当前key 对应的值 转为布尔值之后 存储在 存储对象 中// 这里的逻辑与 index.js 不同,在 index.js 中,是直接将当前的 key 放到最终的 classes 类名数组中// 但是 在 dedupe.js 中,因为去重的需要,所以会先将 key 放在对象中,用于更新它的值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) {// 遍历数组,将每个字符串都作为 存储对象 上的一个 key, 值默认为 trueresultSet[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-argumentsvar 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) {// 如果 存储对象 当前 key 的值是真值,就推到 类名数组中if (classSet[k]) {list.push(k)}}// 最后返回一个类名字符串return list.join(' ');}return _classNames;})();// 通用代码...}());" style="margin: auto" />
对比
关于类的处理包除了 classnames
以外,还有一个最近在项目里用到的是 clsx
;在我看来,两者的差别就在于 clsx
直接省去了数组转字符串
这个步骤,直接定义了一个字符串,然后把符合条件的(真值)的类名加到后面:
总结
这次去看classnames
的源码,确实了解了它的工作原理,没有想象的这么复杂;针对通用代码,还去学习了一下从IIFE
到CJS
,再从AMD
,CMD
到ES Module
的模块化过程,下次再把这个笔记也输出一下!
冲!
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享