theme: arknights
《classnames源码》阅读笔记
-
我正在参与掘金会员专属活动-源码共读第一期,点击参与
-
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与
-
这是源码共读的第26期,链接:https://juejin.cn/post/7087439740416294942
GitHub地址
源码目录总览
参考官方文档中的内容,我们可以知道
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 npm
npm 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, classB
classnames('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, classB classnames('classA', { classB: false }, true ? 'classB' : '') => classA, classB classnames('classA', { classB: true }, false ? 'classB' : '') => classA, classB
-
使用
dedupe
classnames('classA', true ? 'classB' : '', { classB: false }) => classA classnames('classA', { classB: false }, true ? 'classB' : '') => classA, classB classnames('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 name
define('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 方法
// 遍历入参对象的 key
for (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 !== Object.prototype.toString && !object.toString.toString().includes('[native code]')) {
resultSet[object.toString()] = true;
return;
}
// 没有自定义的 toString 方法
// 遍历入参对象
for (var k in object) {
// 检查当前的 key 是否是入参对象自身的 key
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
// 将 当前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, 值默认为 true
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) {
// 如果 存储对象 当前 key 的值是真值,就推到 类名数组中
if (classSet[k]) {
list.push(k)
}
}
// 最后返回一个类名字符串
return list.join(' ');
}
return _classNames;
})();
// 通用代码...
}());
对比
关于类的处理包除了 classnames
以外,还有一个最近在项目里用到的是 clsx
;在我看来,两者的差别就在于 clsx
直接省去了数组转字符串
这个步骤,直接定义了一个字符串,然后把符合条件的(真值)的类名加到后面:
总结
这次去看classnames
的源码,确实了解了它的工作原理,没有想象的这么复杂;针对通用代码,还去学习了一下从IIFE
到CJS
,再从AMD
,CMD
到ES Module
的模块化过程,下次再把这个笔记也输出一下!
冲!
暂无评论内容