- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第26期,链接: juejin.cn/post/708743…
前言
classnames 最为React 官方推荐的工具库, 在平时开发React项目, 都避免不了会使用到它.
它主要功能就是用来判定 classname 的属性名是否动态添加.
例如开发一个点击切换, 选中模块需要展示高亮效果功能时, 使用 classnames 就显得十分方便.
使用安装
安装
npm install classnames --save
引用
import classnames from 'classnames';
使用
Ï<div classname={classnames("default",{"active":true})} >
hello world
</div>
classnames 函数第一个参数传入默认的class 名称, 第二个参数是一个对象, key 值是 class名称, 如果value 为 true, 则存在,否则去掉. 这应该是平时用的最多的一种写法. 除此之外classnames 还支持以下写法:
classNames('foo', 'bar'); // => 'foo bar'
classNames({ `foo-${bar}`: true }); // => 'foo-bar'
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
// 存在多个参数判定场景
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
// 自动忽略为空的参数包括 0
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
// 传入数组格式, 自动递归平铺
const arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'
源码导读
仓库地址: github.com/JedWatson/c…
目录结构
.
├── CONTRIBUTING.md
├── HISTORY.md
├── LICENSE
├── README.md
├── benchmarks // 测试文件目录
│ ├── benchmarks.html
│ ├── fixtures.js // 测试数据
│ ├── package-lock.json
│ ├── package.json
│ ├── run.js// 跑测试文件的入口
│ ├── runChecks.js
│ ├── runInBrowser.js
│ └── runSuite.js
├── bind.d.ts
├── bind.js // bind 方法实现
├── dedupe.d.ts
├── dedupe.js // dedupe 版本实现
├── index.d.ts
├── index.js // classnames方法的实现
├── package-lock.json
├── package.json // 查看依赖和单测调试的入口
├── tests
│ ├── bind.js
│ ├── dedupe.js
│ ├── index.js
│ └── types.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
classNames 实现
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
<http://jedwatson.github.io/classnames>
*/
/* global define */
(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(arg); // 常规类型直接放入数组里面
} else if (Array.isArray(arg)) {
if (arg.length) {
var inner = classNames.apply(null, arg); // 数组就递归调用,使用apply,避免直接传入参数
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;
}
// 没有重写, 就直接遍历对象,获取value值为true 的key值
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
return classes.join(' ');
}
// 模块化加载
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;
}
}());
小结:
classNames 的核心功能实现就是判定传入参数的类型,根据类型做不同的处理,然后都统一放入到自定义的数组中, 最后直接返回空格拼接的字符串。
参数类型的处理主要分以下三种情况:
- 基本类型
string和number直接push到数组里面 - 如果是数组类型, 通过递归的方式, 再次调用
classNames方法, 而调用的方式采用了apply, 原因是因为apply的第二个参数是一个 类数组, 这样可以传递未知的参数。 - 如果是对象的话, 先判定是否重写原型链上
toString方法,重写了,就直接传入方法返回的值;没有则遍历对象,判定值为true,则传入对应的key。
dedupe 版本
dedupe 版本用来去除重复和对象值为false的 classNames
const classNames = require('classnames/dedupe');
classNames('foo', 'foo', 'bar'); // => 'foo bar'
classNames('foo', { foo: false, bar: true }); // => 'bar'
源码实现
(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]); // 遍历数组, 继续递归调用解析方法 _parse
}
}
var hasOwn = {}.hasOwnProperty;
// 如果是数字默认就是true
function _parseNumber(resultSet, num) {
resultSet[num] = true;
}
function _parseObject(resultSet, object) {
// 判定是否重新对象上的 toString 方法
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+/; // \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() {
// don't leak arguments
// <https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments>
// 为了避免 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;
})();
...
}());
小结:
dedupe 版本的实现和 classname 的整体实现上大同小异. 对于基本类型和复杂类型的处理判定逻辑基本是一致的.
而为了实现类名的去重, 作者将类数组 argument 先转换成了数组, 目的是为了避免内存泄露. 然后定义一个新的对象来保存每次遍历解析后的值, 如果存在重复的key, 那么后面的key就会覆盖掉前面的key, 最终实现类型名的去重.
bind 版本
使用方式
import classNames from 'classnames/bind'
import styles from './submit-button.module.css';
let cx = classNames.bind(styles);
var className = cx('foo', ['bar'], { baz: true }); // => "abc def xyz"
bind 主要适用于 [css-module](<https://github.com/css-modules/css-modules>) 的场景 ,本质上其实是解决了重复写css对象名称的操作。 如果使用 css-module 的时候,不采用bind的方式, 而是直接使用 classNames, 那么上面的代码就是这样写的
import classNames from 'classnames'
import styles from './submit-button.module.css';
var className = classNames(styles['foo'],[styles['bar']],{
[styles['baz']]:true
}); // => "abc def xyz"
最终功能效果是一样的, 但是从视觉效果上来看, 确实不够美观优雅,而且显得很臃肿, 给人一种重复编码的感觉。
源码实现
(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); //增加对绑定对象value的获取
} 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);// 增加对绑定对象value的获取
}
}
}
}
return classes.join(' ');
}
...
}());
小结
bind 版本的实现就是在原来 classNames 版本的基础上, 在取值的时候,添加了从 this 对象上获取value的操作,如果 this 对象上不存在该值, 就直接将传入的key值,作为class的属性名传入。
从整体源码分析来看, classNames 的实现并不算很复杂,三种版本的实现都是大同小异的,但确实有很多地方值得我们学习,尤其是对于一些边界场景的考虑。 但是作为一个github 上 star 16.8k 的优秀项目来说,单测肯定是必不可少的。 因此 classNames 的单测实现也是值得学习和借鉴的
单测实现
benchmarks 是单测文件文件所在的目录. 单测试执行的入口文件 benchmarks/run.js
首先是使用 assert 断言, 测试工具函数是否能够和预设的值相等
assert.equal(sortClasses(local.apply(null, fixture.args)), sortClasses(fixture.expected));
assert.equal(sortClasses(dedupe.apply(null, fixture.args)), sortClasses(fixture.expected));
assert.equal(sortClasses(npm.apply(null, fixture.args)), sortClasses(fixture.expected));
assert.equal(sortClasses(npmDedupe.apply(null, fixture.args)), sortClasses(fixture.expected));
其次是使用 benchmarks 来做性能基准测试
function runSuite (local, npm, dedupe, npmDedupe, fixture, log) {
var suite = new benchmark.Suite();
suite.add('local#' + fixture.description, function () {
local.apply(null, fixture.args);
});
suite.add(' npm#' + fixture.description, function () {
npm.apply(null, fixture.args);
});
suite.add('local/dedupe#' + fixture.description, function () {
dedupe.apply(null, fixture.args);
});
suite.add(' npm/dedupe#' + fixture.description, function () {
npmDedupe.apply(null, fixture.args);
});
// after each cycle
suite.on('cycle', function (event) {
log('* ' + String(event.target));
});
// other handling
suite.on('complete', function () {
log('\n> Fastest is' + (' ' + this.filter('fastest').map(result => result.name).join(' | ')).replace(/\s+/, ' ') + '\n');
});
suite.on('error', function (event) {
log(event.target.error.message);
throw event.target.error;
});
suite.run();
}
测试结果:
Debugger attached.
* local#strings x 11,059,300 ops/sec ±0.62% (93 runs sampled)
* npm#strings x 11,364,089 ops/sec ±0.51% (97 runs sampled)
* local/dedupe#strings x 4,185,074 ops/sec ±0.60% (89 runs sampled)
* npm/dedupe#strings x 4,256,622 ops/sec ±0.46% (97 runs sampled)
> Fastest is npm#strings
* local#object x 13,133,142 ops/sec ±0.39% (98 runs sampled)
* npm#object x 12,983,708 ops/sec ±0.75% (95 runs sampled)
* local/dedupe#object x 7,953,071 ops/sec ±1.26% (96 runs sampled)
* npm/dedupe#object x 8,052,713 ops/sec ±0.96% (91 runs sampled)
> Fastest is local#object
* local#strings, object x 10,240,852 ops/sec ±0.71% (95 runs sampled)
* npm#strings, object x 9,988,439 ops/sec ±0.47% (97 runs sampled)
* local/dedupe#strings, object x 4,352,901 ops/sec ±0.52% (98 runs sampled)
* npm/dedupe#strings, object x 4,210,615 ops/sec ±0.99% (95 runs sampled)
> Fastest is local#strings, object
* local#mix x 5,997,558 ops/sec ±0.41% (95 runs sampled)
* npm#mix x 5,978,421 ops/sec ±0.72% (95 runs sampled)
* local/dedupe#mix x 1,752,118 ops/sec ±1.45% (95 runs sampled)
* npm/dedupe#mix x 1,723,078 ops/sec ±1.78% (92 runs sampled)
> Fastest is local#mix | npm#mix
* local#arrays x 2,168,758 ops/sec ±0.44% (96 runs sampled)
* npm#arrays x 2,125,009 ops/sec ±1.30% (96 runs sampled)
* local/dedupe#arrays x 1,831,783 ops/sec ±0.53% (91 runs sampled)
* npm/dedupe#arrays x 1,817,788 ops/sec ±0.50% (96 runs sampled)
> Fastest is local#arrays
ops/sec 测试结果以每秒钟执行测试代码的次数(Ops/sec)显示,这个数值越大越好